@theokit/sdk 2.13.0 → 2.14.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/index.d.cts CHANGED
@@ -863,6 +863,34 @@ interface DefineProviderOptions {
863
863
  */
864
864
  declare function defineProvider(profile: ProviderProfile, opts?: DefineProviderOptions): Plugin;
865
865
 
866
+ /**
867
+ * Options for {@link sanitizeToolInput}. Trim is the only default-on rung — coercion and JSON
868
+ * repair change a value's representation, so they are opt-in (the SDK's "values are strings; Zod
869
+ * coerces" boundary; see `define-tool.ts` doc-comment).
870
+ *
871
+ * @public
872
+ */
873
+ interface SanitizeOptions {
874
+ /** Trim leading/trailing whitespace from string values. Default `true`. */
875
+ trim?: boolean;
876
+ /** Coerce string values to typed values (`"5"`→`5`, `"true"`→`true`, `"null"`→`null`, JSON). Default `false`. */
877
+ coerce?: boolean;
878
+ /** Repair-then-parse malformed JSON-looking string values (via `jsonrepair`). Default `false`. */
879
+ repairJson?: boolean;
880
+ /**
881
+ * Optional Zod schema. When it is a `z.object(...)`, coercion is schema-aware: each TOP-LEVEL
882
+ * field is coerced only toward a candidate its field-schema accepts (so a `z.string()` field
883
+ * keeps `"5"` as a string). Non-object schemas (union/record) fall back to heuristic coercion.
884
+ * Note: with `deep: true`, nested fields always use heuristic coercion (the schema is not
885
+ * descended into).
886
+ */
887
+ schema?: ZodType;
888
+ /** Recurse into nested objects/arrays. Default `false` (shallow). */
889
+ deep?: boolean;
890
+ /** Max recursion depth when `deep` is set. Default `8`. */
891
+ maxDepth?: number;
892
+ }
893
+
866
894
  /**
867
895
  * Spec accepted by {@link defineTool}. `inputSchema` is a Zod schema; the
868
896
  * `handler` argument type is inferred via `z.infer<T>` — no `as` casts.
@@ -878,6 +906,13 @@ interface DefineToolSpec<T extends ZodType> {
878
906
  inputSchema: T;
879
907
  /** Handler invoked with the parsed input. Type is inferred via `z.infer<T>`. */
880
908
  handler: (input: z.infer<T>) => string | Promise<string>;
909
+ /**
910
+ * Sanitize the raw model-emitted args BEFORE schema validation (`@theokit/sdk/sanitize`).
911
+ * `true` trims whitespace; an object opts into coercion / JSON-repair. Coercion is schema-aware
912
+ * against this tool's `inputSchema`. Absent ⇒ args reach validation untouched. Sanitize is
913
+ * hygiene, not a validity bypass — a genuinely invalid arg still raises `ZodError`.
914
+ */
915
+ sanitize?: boolean | SanitizeOptions;
881
916
  }
882
917
  /**
883
918
  * Type-safe builder for {@link CustomTool}. Converts a Zod schema to JSON
package/dist/index.d.ts CHANGED
@@ -863,6 +863,34 @@ interface DefineProviderOptions {
863
863
  */
864
864
  declare function defineProvider(profile: ProviderProfile, opts?: DefineProviderOptions): Plugin;
865
865
 
866
+ /**
867
+ * Options for {@link sanitizeToolInput}. Trim is the only default-on rung — coercion and JSON
868
+ * repair change a value's representation, so they are opt-in (the SDK's "values are strings; Zod
869
+ * coerces" boundary; see `define-tool.ts` doc-comment).
870
+ *
871
+ * @public
872
+ */
873
+ interface SanitizeOptions {
874
+ /** Trim leading/trailing whitespace from string values. Default `true`. */
875
+ trim?: boolean;
876
+ /** Coerce string values to typed values (`"5"`→`5`, `"true"`→`true`, `"null"`→`null`, JSON). Default `false`. */
877
+ coerce?: boolean;
878
+ /** Repair-then-parse malformed JSON-looking string values (via `jsonrepair`). Default `false`. */
879
+ repairJson?: boolean;
880
+ /**
881
+ * Optional Zod schema. When it is a `z.object(...)`, coercion is schema-aware: each TOP-LEVEL
882
+ * field is coerced only toward a candidate its field-schema accepts (so a `z.string()` field
883
+ * keeps `"5"` as a string). Non-object schemas (union/record) fall back to heuristic coercion.
884
+ * Note: with `deep: true`, nested fields always use heuristic coercion (the schema is not
885
+ * descended into).
886
+ */
887
+ schema?: ZodType;
888
+ /** Recurse into nested objects/arrays. Default `false` (shallow). */
889
+ deep?: boolean;
890
+ /** Max recursion depth when `deep` is set. Default `8`. */
891
+ maxDepth?: number;
892
+ }
893
+
866
894
  /**
867
895
  * Spec accepted by {@link defineTool}. `inputSchema` is a Zod schema; the
868
896
  * `handler` argument type is inferred via `z.infer<T>` — no `as` casts.
@@ -878,6 +906,13 @@ interface DefineToolSpec<T extends ZodType> {
878
906
  inputSchema: T;
879
907
  /** Handler invoked with the parsed input. Type is inferred via `z.infer<T>`. */
880
908
  handler: (input: z.infer<T>) => string | Promise<string>;
909
+ /**
910
+ * Sanitize the raw model-emitted args BEFORE schema validation (`@theokit/sdk/sanitize`).
911
+ * `true` trims whitespace; an object opts into coercion / JSON-repair. Coercion is schema-aware
912
+ * against this tool's `inputSchema`. Absent ⇒ args reach validation untouched. Sanitize is
913
+ * hygiene, not a validity bypass — a genuinely invalid arg still raises `ZodError`.
914
+ */
915
+ sanitize?: boolean | SanitizeOptions;
881
916
  }
882
917
  /**
883
918
  * Type-safe builder for {@link CustomTool}. Converts a Zod schema to JSON
package/dist/index.js CHANGED
@@ -13424,6 +13424,115 @@ function toOllamaTools(tools) {
13424
13424
  }
13425
13425
  }));
13426
13426
  }
13427
+ var cachedJsonrepair;
13428
+ function loadJsonrepair() {
13429
+ if (cachedJsonrepair === void 0) {
13430
+ const req = createRequire(import.meta.url);
13431
+ cachedJsonrepair = req("jsonrepair").jsonrepair;
13432
+ }
13433
+ return cachedJsonrepair;
13434
+ }
13435
+ function isPlainObject(v) {
13436
+ return v !== null && typeof v === "object" && !Array.isArray(v);
13437
+ }
13438
+ function toFiniteNumber(raw) {
13439
+ if (raw === "") return void 0;
13440
+ const n = Number(raw);
13441
+ return Number.isFinite(n) && String(n) === raw ? n : void 0;
13442
+ }
13443
+ function tryJson(raw, repair) {
13444
+ const t = raw.trimStart();
13445
+ if (!(t.startsWith("{") || t.startsWith("["))) return void 0;
13446
+ try {
13447
+ return JSON.parse(repair ? loadJsonrepair()(t) : t);
13448
+ } catch {
13449
+ return void 0;
13450
+ }
13451
+ }
13452
+ function heuristicCoerce(raw, repairJson) {
13453
+ if (raw === "true") return true;
13454
+ if (raw === "false") return false;
13455
+ if (raw === "null") return null;
13456
+ const n = toFiniteNumber(raw);
13457
+ if (n !== void 0) return n;
13458
+ const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
13459
+ return json === void 0 ? raw : json;
13460
+ }
13461
+ function coerceCandidates(raw, repairJson) {
13462
+ const out = [];
13463
+ if (raw === "true") out.push(true);
13464
+ else if (raw === "false") out.push(false);
13465
+ else if (raw === "null") out.push(null);
13466
+ const n = toFiniteNumber(raw);
13467
+ if (n !== void 0) out.push(n);
13468
+ const json = tryJson(raw, false) ?? (repairJson ? tryJson(raw, true) : void 0);
13469
+ if (json !== void 0) out.push(json);
13470
+ out.push(raw);
13471
+ return out;
13472
+ }
13473
+ function objectShape(schema) {
13474
+ const shape = schema?.shape;
13475
+ return shape !== null && typeof shape === "object" ? shape : void 0;
13476
+ }
13477
+
13478
+ // src/sanitize/sanitize-tool-input.ts
13479
+ function applyTrim(key2, value, ctx) {
13480
+ const trimmed = value.trim();
13481
+ if (trimmed !== value) ctx.notes.push(`trimmed "${key2}"`);
13482
+ return trimmed;
13483
+ }
13484
+ function applyCoerce(key2, raw, ctx) {
13485
+ const field = ctx.shape?.[key2];
13486
+ let coerced = raw;
13487
+ if (field) {
13488
+ for (const candidate of coerceCandidates(raw, ctx.repairJson)) {
13489
+ if (field.safeParse(candidate).success) {
13490
+ coerced = candidate;
13491
+ break;
13492
+ }
13493
+ }
13494
+ } else {
13495
+ coerced = heuristicCoerce(raw, ctx.repairJson);
13496
+ }
13497
+ if (coerced !== raw) ctx.notes.push(`coerced "${key2}"`);
13498
+ return coerced;
13499
+ }
13500
+ function applyRepair(key2, value, ctx) {
13501
+ const repaired = tryJson(value, true);
13502
+ if (repaired === void 0) return value;
13503
+ ctx.notes.push(`repaired json "${key2}"`);
13504
+ return repaired;
13505
+ }
13506
+ function sanitizeString(key2, value, ctx) {
13507
+ let out = ctx.trim ? applyTrim(key2, value, ctx) : value;
13508
+ if (ctx.coerce && typeof out === "string") out = applyCoerce(key2, out, ctx);
13509
+ if (ctx.repairJson && !ctx.coerce && typeof out === "string") out = applyRepair(key2, out, ctx);
13510
+ return out;
13511
+ }
13512
+ function walk(input, ctx, depth) {
13513
+ const out = {};
13514
+ for (const [key2, value] of Object.entries(input)) {
13515
+ if (typeof value === "string") out[key2] = sanitizeString(key2, value, ctx);
13516
+ else if (ctx.deep && depth < ctx.maxDepth && isPlainObject(value))
13517
+ out[key2] = walk(value, ctx, depth + 1);
13518
+ else out[key2] = value;
13519
+ }
13520
+ return out;
13521
+ }
13522
+ function sanitizeToolInput(input, options) {
13523
+ if (!isPlainObject(input)) return { value: input, changed: false, notes: [] };
13524
+ const ctx = {
13525
+ trim: options?.trim ?? true,
13526
+ coerce: options?.coerce ?? false,
13527
+ repairJson: options?.repairJson ?? false,
13528
+ deep: options?.deep ?? false,
13529
+ maxDepth: options?.maxDepth ?? 8,
13530
+ shape: objectShape(options?.schema),
13531
+ notes: []
13532
+ };
13533
+ const value = walk(input, ctx, 0);
13534
+ return { value, changed: ctx.notes.length > 0, notes: ctx.notes };
13535
+ }
13427
13536
 
13428
13537
  // src/internal/llm/hermes-tool-extract.ts
13429
13538
  var HERMES_BLOCK = /<function=\s*([^>\s]+)\s*>([\s\S]*?)<\/tool_call>/g;
@@ -13451,7 +13560,7 @@ function parseHermesParams(inner) {
13451
13560
  if (key2 === void 0 || value === void 0) continue;
13452
13561
  input[key2.trim()] = value;
13453
13562
  }
13454
- return input;
13563
+ return sanitizeToolInput(input, { trim: true }).value;
13455
13564
  }
13456
13565
 
13457
13566
  // src/internal/llm/openai.ts
@@ -18172,7 +18281,11 @@ function defineTool(spec) {
18172
18281
  description: spec.description,
18173
18282
  inputSchema,
18174
18283
  handler: async (input) => {
18175
- const parsed = spec.inputSchema.parse(input);
18284
+ const raw = spec.sanitize ? sanitizeToolInput(input, {
18285
+ ...spec.sanitize === true ? {} : spec.sanitize,
18286
+ schema: spec.inputSchema
18287
+ }).value : input;
18288
+ const parsed = spec.inputSchema.parse(raw);
18176
18289
  return await spec.handler(parsed);
18177
18290
  }
18178
18291
  };