@core-ai/core-ai 0.24.0 → 0.25.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.ts CHANGED
@@ -143,10 +143,19 @@ type ToolResultMessage = {
143
143
  */
144
144
  metadata?: Record<string, unknown>;
145
145
  };
146
- type ToolDefinition = {
146
+ type ToolDefinition<TParameters extends z.ZodType = z.ZodType> = {
147
147
  name: string;
148
148
  description: string;
149
- parameters: z.ZodType;
149
+ parameters: TParameters;
150
+ /**
151
+ * `true` opts this tool into provider-enforced schema adherence. The
152
+ * schema must then satisfy the strict-capable schema contract (closed
153
+ * objects, every key required — use `.nullable()` instead of
154
+ * `.optional()` — and the shared keyword subset). Omitted or `false`
155
+ * means non-strict on every provider. Check
156
+ * `model.capabilities.tools.strictSchemas` before opting in.
157
+ */
158
+ strict?: boolean;
150
159
  };
151
160
  type ToolSet = Record<string, ToolDefinition>;
152
161
  type ToolChoice = 'auto' | 'none' | 'required' | {
@@ -169,6 +178,12 @@ type ChatInputModality = 'text' | 'image' | 'file' | 'audio' | 'video';
169
178
  * from `generate` / `stream`, not those dedicated APIs.
170
179
  */
171
180
  type ChatOutputModality = 'text' | 'image' | 'audio' | 'video';
181
+ type ToolSchemaStrictnessCapabilities = {
182
+ supported: false;
183
+ } | {
184
+ supported: true;
185
+ maxStrictTools?: number;
186
+ };
172
187
  type ModelCapabilities = {
173
188
  reasoning: {
174
189
  mode: 'unsupported' | 'optional' | 'always-on';
@@ -189,6 +204,9 @@ type ModelCapabilities = {
189
204
  */
190
205
  output: readonly ChatOutputModality[];
191
206
  };
207
+ tools: {
208
+ strictSchemas: ToolSchemaStrictnessCapabilities;
209
+ };
192
210
  };
193
211
  type ChatModel = {
194
212
  readonly provider: string;
@@ -433,6 +451,27 @@ type GeneratedImage = {
433
451
  revisedPrompt?: string;
434
452
  };
435
453
 
454
+ /**
455
+ * A single violation of the strict-capable schema contract, reported against
456
+ * the JSON Schema derived from the tool's Zod `parameters`.
457
+ */
458
+ type StrictToolSchemaViolation = {
459
+ toolName: string;
460
+ /** Dot path into the tool's JSON Schema, e.g. `properties.limit`. */
461
+ path: string;
462
+ /** What is wrong and how to fix it. */
463
+ message: string;
464
+ };
465
+ /**
466
+ * Checks the JSON Schema of a strict tool against the strict-capable schema
467
+ * contract: closed objects with every key required, the basic type set, and
468
+ * the keyword/format subset every strict-capable provider accepts.
469
+ *
470
+ * Pure and non-throwing — returns every violation found so callers can report
471
+ * them all at once.
472
+ */
473
+ declare function getStrictToolSchemaViolations(toolName: string, schema: Record<string, unknown>): StrictToolSchemaViolation[];
474
+
436
475
  declare class CoreAIError extends Error {
437
476
  readonly cause?: unknown;
438
477
  readonly provider?: string;
@@ -441,6 +480,29 @@ declare class CoreAIError extends Error {
441
480
  declare class ValidationError extends CoreAIError {
442
481
  constructor(message: string, cause?: unknown, provider?: string);
443
482
  }
483
+ type ToolSchemaStrictnessErrorReason = 'unsupported' | 'limit-exceeded' | 'invalid-schema';
484
+ type ToolSchemaStrictnessErrorOptions = {
485
+ providerId: string;
486
+ modelId: string;
487
+ toolNames: readonly string[];
488
+ } & ({
489
+ reason: 'unsupported';
490
+ } | {
491
+ reason: 'limit-exceeded';
492
+ maxStrictTools: number;
493
+ } | {
494
+ reason: 'invalid-schema';
495
+ violations: readonly StrictToolSchemaViolation[];
496
+ });
497
+ declare class ToolSchemaStrictnessError extends ValidationError {
498
+ readonly providerId: string;
499
+ readonly modelId: string;
500
+ readonly toolNames: readonly string[];
501
+ readonly reason: ToolSchemaStrictnessErrorReason;
502
+ readonly maxStrictTools?: number;
503
+ readonly violations?: readonly StrictToolSchemaViolation[];
504
+ constructor(options: ToolSchemaStrictnessErrorOptions);
505
+ }
444
506
  type UnsupportedInputModalityErrorOptions = {
445
507
  modelId: string;
446
508
  providerId: string;
@@ -574,16 +636,42 @@ declare function parseRetryAfterSeconds(headers: Headers | Record<string, string
574
636
  /** Reads `Retry-After` from common SDK error shapes that expose `headers`. */
575
637
  declare function getRetryAfterSecondsFromError(error: unknown): number | undefined;
576
638
 
577
- declare function defineTool(options: ToolDefinition): ToolDefinition;
639
+ declare function defineTool<TParameters extends ToolDefinition['parameters']>(options: ToolDefinition<TParameters>): ToolDefinition<TParameters>;
578
640
 
579
641
  /**
580
642
  * Convert a Zod schema to a JSON Schema object using Zod 4's native
581
643
  * `z.toJSONSchema()`.
582
644
  */
583
645
  declare function zodSchemaToJsonSchema(schema: z.ZodType): Record<string, unknown>;
646
+ /**
647
+ * Normalizes the JSON Schema of a strict tool for providers whose strict mode
648
+ * requires closed objects (OpenAI-style APIs). The transform is semantics
649
+ * preserving with respect to the tool's Zod schema:
650
+ *
651
+ * - drops `$schema` (metadata, not a constraint),
652
+ * - sets `additionalProperties: false` on object nodes where absent, which
653
+ * matches `z.object()` semantics (unknown keys are stripped at parse time),
654
+ * - drops Zod's implicit safe-integer bounds on integer nodes (see
655
+ * {@link isImplicitSafeIntegerBound}),
656
+ * - rewrites `oneOf` to `anyOf`. Zod emits `oneOf` only for
657
+ * `z.discriminatedUnion()`, whose branches are disjoint by construction, so
658
+ * the two keywords accept the same values there — and strict-capable
659
+ * providers accept `anyOf` only.
660
+ *
661
+ * It never widens or narrows what the user's Zod schema accepts; schemas that
662
+ * cannot be expressed in the strict subset are rejected by the contract
663
+ * validator instead of being rewritten.
664
+ */
665
+ declare function normalizeStrictJsonSchema(schema: Record<string, unknown>): Record<string, unknown>;
584
666
 
585
667
  declare function stripModelDateSuffix(modelId: string): string;
586
668
 
669
+ declare const UNSUPPORTED_TOOL_SCHEMA_STRICTNESS: Readonly<{
670
+ readonly supported: false;
671
+ }>;
672
+ declare const SUPPORTED_TOOL_SCHEMA_STRICTNESS: Readonly<{
673
+ readonly supported: true;
674
+ }>;
587
675
  declare function clampReasoningEffort(effort: ReasoningEffort, supportedEfforts: readonly ReasoningEffort[]): ReasoningEffort;
588
676
  /** Text in, text out — the default chat modality profile. */
589
677
  declare const TEXT_ONLY_MODALITIES: {
@@ -616,6 +704,23 @@ type ValidateInputModalitiesOptions = {
616
704
  */
617
705
  declare function validateInputModalities({ messages, capabilities, modelId, providerId, }: ValidateInputModalitiesOptions): void;
618
706
 
707
+ type ValidateToolSchemaStrictnessOptions = {
708
+ tools: ToolSet;
709
+ capabilities: ModelCapabilities;
710
+ providerId: string;
711
+ modelId: string;
712
+ };
713
+ /**
714
+ * Validates the strict tools of a request before it reaches the provider.
715
+ *
716
+ * Strictness is per-tool opt-in: only tools with `strict: true` are checked.
717
+ * Throws {@link ToolSchemaStrictnessError} when the model is known not to
718
+ * support strict schemas (`unsupported`), when more tools opt in than the
719
+ * model allows (`limit-exceeded`), or when a strict tool's schema falls
720
+ * outside the strict-capable schema contract (`invalid-schema`).
721
+ */
722
+ declare function validateToolSchemaStrictness({ tools, capabilities, providerId, modelId, }: ValidateToolSchemaStrictnessOptions): void;
723
+
619
724
  declare const UNKNOWN_MODEL: unique symbol;
620
725
  type ModelCapabilitiesRegistry<TCapabilities extends ModelCapabilities = ModelCapabilities> = Record<string, TCapabilities> & {
621
726
  [UNKNOWN_MODEL]?: TCapabilities;
@@ -692,4 +797,4 @@ type GenerateImageParams = ImageGenerateOptions & {
692
797
  };
693
798
  declare function generateImage(params: GenerateImageParams): Promise<ImageGenerateResult>;
694
799
 
695
- export { AbortedError, type AssistantContentPart, type AssistantMessage, type AssistantTextPart, type AudioPart, type BaseGenerateOptions, type ChatInputModality, type ChatInputTokenDetails, type ChatModel, type ChatModelMiddleware, type ChatOutputModality, type ChatOutputTokenDetails, type ChatStream, type ChatUsage, ContextLengthExceededError, type ContextLengthExceededErrorOptions, CoreAIError, type CreateChatStreamOptions, type EmbedOptions, type EmbedProviderOptions, type EmbedResult, type EmbeddingModel, type EmbeddingModelMiddleware, type EmbeddingUsage, type FilePart, type FinishReason, type GenerateObjectOptions, type GenerateObjectResult, type GenerateOptions, type GenerateProviderOptions, type GenerateResult, type GeneratedImage, type ImageGenerateOptions, type ImageGenerateResult, type ImageModel, type ImageModelMiddleware, type ImagePart, type ImageProviderOptions, MULTIMODAL_INPUT_MODALITIES, type Message, type ModelCapabilities, type ModelCapabilitiesRegistry, ModelOverloadedError, type ModelOverloadedErrorOptions, type ObjectStream, type ObjectStreamEvent, ProviderError, type ProviderErrorOptions, ProviderQuotaExceededError, type ProviderQuotaExceededErrorOptions, RateLimitError, type RateLimitErrorOptions, type ReasoningConfig, type ReasoningEffort, type ReasoningPart, RetryableProviderError, ServiceUnavailableError, type ServiceUnavailableErrorOptions, StreamAbortedError, type StreamEvent, type StreamObjectOptions, StructuredOutputError, StructuredOutputNoObjectGeneratedError, StructuredOutputParseError, StructuredOutputValidationError, type SystemMessage, TEXT_ONLY_MODALITIES, type TextPart, type ToolCall, type ToolCallPart, type ToolChoice, type ToolDefinition, type ToolResultMessage, type ToolSet, UNKNOWN_MODEL, UnsupportedInputModalityError, type UnsupportedInputModalityErrorOptions, type UserContentPart, type UserMessage, type ValidateInputModalitiesOptions, ValidationError, asObject, asRecord, assistantMessage, clampReasoningEffort, createChatStream, createObjectStream, defineTool, embed, generate, generateImage, generateObject, getErrorMessage, getHttpStatusCode, getProviderMetadata, getRegisteredModelCapabilities, getRetryAfterSecondsFromError, getString, isAbortErrorByName, isRateLimitStatus, isTransientUnavailableStatus, parseRetryAfterSeconds, resultToMessage, safeParseJsonObject, stream, streamObject, stripModelDateSuffix, supportsInputModality, supportsOutputModality, validateInputModalities, wrapChatModel, wrapEmbeddingModel, wrapImageModel, zodSchemaToJsonSchema };
800
+ export { AbortedError, type AssistantContentPart, type AssistantMessage, type AssistantTextPart, type AudioPart, type BaseGenerateOptions, type ChatInputModality, type ChatInputTokenDetails, type ChatModel, type ChatModelMiddleware, type ChatOutputModality, type ChatOutputTokenDetails, type ChatStream, type ChatUsage, ContextLengthExceededError, type ContextLengthExceededErrorOptions, CoreAIError, type CreateChatStreamOptions, type EmbedOptions, type EmbedProviderOptions, type EmbedResult, type EmbeddingModel, type EmbeddingModelMiddleware, type EmbeddingUsage, type FilePart, type FinishReason, type GenerateObjectOptions, type GenerateObjectResult, type GenerateOptions, type GenerateProviderOptions, type GenerateResult, type GeneratedImage, type ImageGenerateOptions, type ImageGenerateResult, type ImageModel, type ImageModelMiddleware, type ImagePart, type ImageProviderOptions, MULTIMODAL_INPUT_MODALITIES, type Message, type ModelCapabilities, type ModelCapabilitiesRegistry, ModelOverloadedError, type ModelOverloadedErrorOptions, type ObjectStream, type ObjectStreamEvent, ProviderError, type ProviderErrorOptions, ProviderQuotaExceededError, type ProviderQuotaExceededErrorOptions, RateLimitError, type RateLimitErrorOptions, type ReasoningConfig, type ReasoningEffort, type ReasoningPart, RetryableProviderError, SUPPORTED_TOOL_SCHEMA_STRICTNESS, ServiceUnavailableError, type ServiceUnavailableErrorOptions, StreamAbortedError, type StreamEvent, type StreamObjectOptions, type StrictToolSchemaViolation, StructuredOutputError, StructuredOutputNoObjectGeneratedError, StructuredOutputParseError, StructuredOutputValidationError, type SystemMessage, TEXT_ONLY_MODALITIES, type TextPart, type ToolCall, type ToolCallPart, type ToolChoice, type ToolDefinition, type ToolResultMessage, type ToolSchemaStrictnessCapabilities, ToolSchemaStrictnessError, type ToolSchemaStrictnessErrorOptions, type ToolSchemaStrictnessErrorReason, type ToolSet, UNKNOWN_MODEL, UNSUPPORTED_TOOL_SCHEMA_STRICTNESS, UnsupportedInputModalityError, type UnsupportedInputModalityErrorOptions, type UserContentPart, type UserMessage, type ValidateInputModalitiesOptions, type ValidateToolSchemaStrictnessOptions, ValidationError, asObject, asRecord, assistantMessage, clampReasoningEffort, createChatStream, createObjectStream, defineTool, embed, generate, generateImage, generateObject, getErrorMessage, getHttpStatusCode, getProviderMetadata, getRegisteredModelCapabilities, getRetryAfterSecondsFromError, getStrictToolSchemaViolations, getString, isAbortErrorByName, isRateLimitStatus, isTransientUnavailableStatus, normalizeStrictJsonSchema, parseRetryAfterSeconds, resultToMessage, safeParseJsonObject, stream, streamObject, stripModelDateSuffix, supportsInputModality, supportsOutputModality, validateInputModalities, validateToolSchemaStrictness, wrapChatModel, wrapEmbeddingModel, wrapImageModel, zodSchemaToJsonSchema };
package/dist/index.js CHANGED
@@ -15,6 +15,49 @@ var ValidationError = class extends CoreAIError {
15
15
  this.name = "ValidationError";
16
16
  }
17
17
  };
18
+ var ToolSchemaStrictnessError = class extends ValidationError {
19
+ providerId;
20
+ modelId;
21
+ toolNames;
22
+ reason;
23
+ maxStrictTools;
24
+ violations;
25
+ constructor(options) {
26
+ super(
27
+ getToolSchemaStrictnessErrorMessage(options),
28
+ void 0,
29
+ options.providerId
30
+ );
31
+ this.name = "ToolSchemaStrictnessError";
32
+ this.providerId = options.providerId;
33
+ this.modelId = options.modelId;
34
+ this.toolNames = options.toolNames;
35
+ this.reason = options.reason;
36
+ if (options.reason === "limit-exceeded") {
37
+ this.maxStrictTools = options.maxStrictTools;
38
+ }
39
+ if (options.reason === "invalid-schema") {
40
+ this.violations = options.violations;
41
+ }
42
+ }
43
+ };
44
+ function getToolSchemaStrictnessErrorMessage(options) {
45
+ const tools = options.toolNames.length === 0 ? "(none)" : options.toolNames.map((name) => `"${name}"`).join(", ");
46
+ switch (options.reason) {
47
+ case "limit-exceeded": {
48
+ const toolWord = options.maxStrictTools === 1 ? "tool" : "tools";
49
+ return `${options.providerId} model "${options.modelId}" supports at most ${options.maxStrictTools} strict ${toolWord}, but received ${options.toolNames.length}: ${tools}`;
50
+ }
51
+ case "invalid-schema": {
52
+ const violations = options.violations.map(
53
+ (violation) => `tool "${violation.toolName}"${violation.path === "" ? "" : ` at ${violation.path}`}: ${violation.message}`
54
+ ).join("; ");
55
+ return `${options.providerId} model "${options.modelId}" received strict tools whose schemas are outside the strict-capable schema contract: ${violations}`;
56
+ }
57
+ case "unsupported":
58
+ return `${options.providerId} model "${options.modelId}" does not support per-tool strict schemas. Requested by: ${tools}`;
59
+ }
60
+ }
18
61
  var UnsupportedInputModalityError = class extends ValidationError {
19
62
  requestedModalities;
20
63
  supportedModalities;
@@ -256,6 +299,348 @@ function zodSchemaToJsonSchema(schema) {
256
299
  io: "input"
257
300
  });
258
301
  }
302
+ var MAX_SAFE_INTEGER_BOUND = 9007199254740991;
303
+ function isPlainObject(value) {
304
+ return typeof value === "object" && value !== null && !Array.isArray(value);
305
+ }
306
+ function isObjectSchemaNode(node) {
307
+ return node.type === "object" || isPlainObject(node.properties);
308
+ }
309
+ function isImplicitSafeIntegerBound(node, key) {
310
+ if (node.type !== "integer") {
311
+ return false;
312
+ }
313
+ return key === "minimum" ? node.minimum === -MAX_SAFE_INTEGER_BOUND : node.maximum === MAX_SAFE_INTEGER_BOUND;
314
+ }
315
+ var NESTED_MAP_KEYWORDS = ["properties"];
316
+ var NESTED_LIST_KEYWORDS = ["anyOf", "oneOf", "allOf"];
317
+ var NESTED_SINGLE_KEYWORDS = ["items"];
318
+ var DEFINITION_KEYWORDS = ["$defs", "definitions"];
319
+ function getChildSchemaNodeEntries(node) {
320
+ const entries = getNamedChildEntries(node, NESTED_MAP_KEYWORDS);
321
+ for (const keyword of NESTED_SINGLE_KEYWORDS) {
322
+ if (node[keyword] !== void 0) {
323
+ entries.push({ segment: keyword, child: node[keyword] });
324
+ }
325
+ }
326
+ for (const keyword of NESTED_LIST_KEYWORDS) {
327
+ const value = node[keyword];
328
+ if (Array.isArray(value)) {
329
+ value.forEach((child, index) => {
330
+ entries.push({ segment: `${keyword}.${index}`, child });
331
+ });
332
+ }
333
+ }
334
+ return entries;
335
+ }
336
+ function getSchemaDefinitionEntries(node) {
337
+ return getNamedChildEntries(node, DEFINITION_KEYWORDS);
338
+ }
339
+ function getNamedChildEntries(node, keywords) {
340
+ const entries = [];
341
+ for (const keyword of keywords) {
342
+ const value = node[keyword];
343
+ if (isPlainObject(value)) {
344
+ for (const [key, child] of Object.entries(value)) {
345
+ entries.push({ segment: `${keyword}.${key}`, child });
346
+ }
347
+ }
348
+ }
349
+ return entries;
350
+ }
351
+ function mapChildSchemaNodes(node, map) {
352
+ const result = { ...node };
353
+ for (const keyword of [...NESTED_MAP_KEYWORDS, ...DEFINITION_KEYWORDS]) {
354
+ const value = result[keyword];
355
+ if (isPlainObject(value)) {
356
+ result[keyword] = Object.fromEntries(
357
+ Object.entries(value).map(([key, child]) => [key, map(child)])
358
+ );
359
+ }
360
+ }
361
+ for (const keyword of NESTED_SINGLE_KEYWORDS) {
362
+ if (result[keyword] !== void 0) {
363
+ result[keyword] = map(result[keyword]);
364
+ }
365
+ }
366
+ for (const keyword of NESTED_LIST_KEYWORDS) {
367
+ const value = result[keyword];
368
+ if (Array.isArray(value)) {
369
+ result[keyword] = value.map((child) => map(child));
370
+ }
371
+ }
372
+ return result;
373
+ }
374
+ function normalizeStrictJsonSchema(schema) {
375
+ return normalizeObjectNode(schema);
376
+ }
377
+ function normalizeNode(node) {
378
+ return isPlainObject(node) ? normalizeObjectNode(node) : node;
379
+ }
380
+ function normalizeObjectNode(node) {
381
+ const stripped = {};
382
+ for (const [key, value] of Object.entries(node)) {
383
+ if (key === "$schema") {
384
+ continue;
385
+ }
386
+ if ((key === "minimum" || key === "maximum") && isImplicitSafeIntegerBound(node, key)) {
387
+ continue;
388
+ }
389
+ stripped[key] = value;
390
+ }
391
+ const result = mapChildSchemaNodes(stripped, normalizeNode);
392
+ if (Array.isArray(result.oneOf) && result.anyOf === void 0) {
393
+ result.anyOf = result.oneOf;
394
+ delete result.oneOf;
395
+ }
396
+ if (isObjectSchemaNode(result) && result.additionalProperties === void 0) {
397
+ result.additionalProperties = false;
398
+ }
399
+ return result;
400
+ }
401
+
402
+ // src/strict-tool-schema-contract.ts
403
+ var MAX_SCHEMA_DEPTH = 64;
404
+ var ALLOWED_KEYWORDS = /* @__PURE__ */ new Set([
405
+ "$schema",
406
+ "$ref",
407
+ "$defs",
408
+ "definitions",
409
+ "type",
410
+ "properties",
411
+ "required",
412
+ "additionalProperties",
413
+ "items",
414
+ "enum",
415
+ "const",
416
+ "anyOf",
417
+ "oneOf",
418
+ "format",
419
+ "pattern",
420
+ "title",
421
+ "description",
422
+ "default",
423
+ "examples",
424
+ "minimum",
425
+ "maximum"
426
+ ]);
427
+ var ALLOWED_TYPES = /* @__PURE__ */ new Set([
428
+ "object",
429
+ "array",
430
+ "string",
431
+ "number",
432
+ "integer",
433
+ "boolean",
434
+ "null"
435
+ ]);
436
+ var ALLOWED_STRING_FORMATS = /* @__PURE__ */ new Set([
437
+ "date-time",
438
+ "time",
439
+ "date",
440
+ "duration",
441
+ "email",
442
+ "hostname",
443
+ "ipv4",
444
+ "ipv6",
445
+ "uuid"
446
+ ]);
447
+ var BOUND_HINTS = {
448
+ minimum: "remove numeric bounds (.min()/.gt()/.gte()) \u2014 numeric range constraints are outside the strict-capable subset; validate ranges after parsing instead",
449
+ maximum: "remove numeric bounds (.max()/.lt()/.lte()) \u2014 numeric range constraints are outside the strict-capable subset; validate ranges after parsing instead"
450
+ };
451
+ var KEYWORD_HINTS = {
452
+ ...BOUND_HINTS,
453
+ minLength: "remove .min()/.length() \u2014 string length constraints are outside the strict-capable subset; validate lengths after parsing instead",
454
+ maxLength: "remove .max()/.length() \u2014 string length constraints are outside the strict-capable subset; validate lengths after parsing instead",
455
+ exclusiveMinimum: "remove .gt() \u2014 numeric range constraints are outside the strict-capable subset; validate ranges after parsing instead",
456
+ exclusiveMaximum: "remove .lt() \u2014 numeric range constraints are outside the strict-capable subset; validate ranges after parsing instead",
457
+ multipleOf: "remove .multipleOf()/.step() \u2014 numeric constraints are outside the strict-capable subset; validate after parsing instead",
458
+ minItems: "remove .min()/.nonempty() on arrays \u2014 array length constraints are outside the strict-capable subset; validate after parsing instead",
459
+ maxItems: "remove .max() on arrays \u2014 array length constraints are outside the strict-capable subset; validate after parsing instead",
460
+ uniqueItems: "array uniqueness constraints are outside the strict-capable subset; validate after parsing instead",
461
+ prefixItems: "tuples (z.tuple) are not strict-capable; use a uniform z.array() or an object instead",
462
+ allOf: "intersections (z.intersection()/.and(), serialized as allOf) are not strict-capable; flatten the members into a single z.object() instead",
463
+ not: "negated schemas (not) are not strict-capable",
464
+ patternProperties: "pattern properties are not strict-capable; declare explicit keys with z.object()",
465
+ propertyNames: "property-name constraints are not strict-capable; declare explicit keys with z.object()"
466
+ };
467
+ function getStrictToolSchemaViolations(toolName, schema) {
468
+ const violations = [];
469
+ const refEdges = /* @__PURE__ */ new Map();
470
+ if (schema.type !== "object") {
471
+ violations.push({
472
+ toolName,
473
+ path: "type",
474
+ message: "tool parameters must be an object at the root; wrap the schema in z.object()"
475
+ });
476
+ }
477
+ walkNode(schema, "", "root", 0, toolName, violations, refEdges);
478
+ for (const { segment, child } of getSchemaDefinitionEntries(schema)) {
479
+ walkNode(child, segment, segment, 0, toolName, violations, refEdges);
480
+ }
481
+ for (const region of findCyclicRegions(refEdges)) {
482
+ violations.push({
483
+ toolName,
484
+ path: region === "root" ? "" : region,
485
+ message: "recursive schemas (z.lazy or self-referencing types) are not strict-capable"
486
+ });
487
+ }
488
+ return violations;
489
+ }
490
+ function walkNode(node, path, region, depth, toolName, violations, refEdges) {
491
+ if (!isPlainObject(node)) {
492
+ return;
493
+ }
494
+ if (depth > MAX_SCHEMA_DEPTH) {
495
+ violations.push({
496
+ toolName,
497
+ path,
498
+ message: `schema nesting exceeds the supported depth of ${MAX_SCHEMA_DEPTH}`
499
+ });
500
+ return;
501
+ }
502
+ for (const key of Object.keys(node)) {
503
+ if (ALLOWED_KEYWORDS.has(key)) {
504
+ continue;
505
+ }
506
+ const hint = KEYWORD_HINTS[key];
507
+ violations.push({
508
+ toolName,
509
+ path: joinPath(path, key),
510
+ message: hint ?? `keyword "${key}" is outside the strict-capable subset; remove it and validate after parsing instead`
511
+ });
512
+ }
513
+ validateBounds(node, path, toolName, violations);
514
+ validateType(node, path, toolName, violations);
515
+ validateFormat(node, path, toolName, violations);
516
+ validateObjectShape(node, path, toolName, violations);
517
+ validateRef(node, path, region, toolName, violations, refEdges);
518
+ for (const { segment, child } of getChildSchemaNodeEntries(node)) {
519
+ walkNode(
520
+ child,
521
+ joinPath(path, segment),
522
+ region,
523
+ depth + 1,
524
+ toolName,
525
+ violations,
526
+ refEdges
527
+ );
528
+ }
529
+ }
530
+ function validateBounds(node, path, toolName, violations) {
531
+ for (const key of ["minimum", "maximum"]) {
532
+ if (key in node && !isImplicitSafeIntegerBound(node, key)) {
533
+ violations.push({
534
+ toolName,
535
+ path: joinPath(path, key),
536
+ message: BOUND_HINTS[key]
537
+ });
538
+ }
539
+ }
540
+ }
541
+ function validateType(node, path, toolName, violations) {
542
+ if (!("type" in node)) {
543
+ return;
544
+ }
545
+ const types = Array.isArray(node.type) ? node.type : [node.type];
546
+ for (const type of types) {
547
+ if (typeof type !== "string" || !ALLOWED_TYPES.has(type)) {
548
+ violations.push({
549
+ toolName,
550
+ path: joinPath(path, "type"),
551
+ message: `type "${String(type)}" is outside the strict-capable subset (object, array, string, number, integer, boolean, null)`
552
+ });
553
+ }
554
+ }
555
+ }
556
+ function validateFormat(node, path, toolName, violations) {
557
+ if (typeof node.format !== "string") {
558
+ return;
559
+ }
560
+ if (!ALLOWED_STRING_FORMATS.has(node.format)) {
561
+ violations.push({
562
+ toolName,
563
+ path: joinPath(path, "format"),
564
+ message: `format "${node.format}" is outside the strict-capable subset (${[...ALLOWED_STRING_FORMATS].join(", ")}); remove the format refinement (for URLs, use z.string() instead of z.url()) and validate after parsing instead`
565
+ });
566
+ }
567
+ }
568
+ function validateObjectShape(node, path, toolName, violations) {
569
+ if (!isObjectSchemaNode(node)) {
570
+ return;
571
+ }
572
+ const properties = isPlainObject(node.properties) ? node.properties : {};
573
+ const required = new Set(
574
+ Array.isArray(node.required) ? node.required.filter((key) => typeof key === "string") : []
575
+ );
576
+ for (const key of Object.keys(properties)) {
577
+ if (!required.has(key)) {
578
+ violations.push({
579
+ toolName,
580
+ path: joinPath(path, `properties.${key}`),
581
+ message: `"${key}" is optional; strict schemas require every key \u2014 use .nullable() instead of .optional() (note that .default() also makes a key optional)`
582
+ });
583
+ }
584
+ }
585
+ if ("additionalProperties" in node && node.additionalProperties !== false) {
586
+ violations.push({
587
+ toolName,
588
+ path: joinPath(path, "additionalProperties"),
589
+ message: "open objects are not strict-capable; use z.object() with explicit keys instead of z.record(), .catchall(), .passthrough(), or z.looseObject()"
590
+ });
591
+ }
592
+ }
593
+ function validateRef(node, path, region, toolName, violations, refEdges) {
594
+ if (typeof node.$ref !== "string") {
595
+ return;
596
+ }
597
+ if (node.$ref === "#") {
598
+ addRefEdge(refEdges, region, "root");
599
+ return;
600
+ }
601
+ const match = /^#\/(\$defs|definitions)\/([^/]+)$/.exec(node.$ref);
602
+ if (!match) {
603
+ violations.push({
604
+ toolName,
605
+ path: joinPath(path, "$ref"),
606
+ message: `only local references into $defs are strict-capable; found "${node.$ref}"`
607
+ });
608
+ return;
609
+ }
610
+ addRefEdge(refEdges, region, `${match[1]}.${match[2]}`);
611
+ }
612
+ function addRefEdge(refEdges, from, to) {
613
+ const edges = refEdges.get(from) ?? /* @__PURE__ */ new Set();
614
+ edges.add(to);
615
+ refEdges.set(from, edges);
616
+ }
617
+ function findCyclicRegions(refEdges) {
618
+ const cyclic = /* @__PURE__ */ new Set();
619
+ const visiting = /* @__PURE__ */ new Set();
620
+ const done = /* @__PURE__ */ new Set();
621
+ function visit(region) {
622
+ if (done.has(region)) {
623
+ return;
624
+ }
625
+ if (visiting.has(region)) {
626
+ cyclic.add(region);
627
+ return;
628
+ }
629
+ visiting.add(region);
630
+ for (const target of refEdges.get(region) ?? []) {
631
+ visit(target);
632
+ }
633
+ visiting.delete(region);
634
+ done.add(region);
635
+ }
636
+ for (const region of refEdges.keys()) {
637
+ visit(region);
638
+ }
639
+ return [...cyclic];
640
+ }
641
+ function joinPath(path, segment) {
642
+ return path === "" ? segment : `${path}.${segment}`;
643
+ }
259
644
 
260
645
  // src/model-id.ts
261
646
  var MODEL_DATE_SUFFIX_PATTERN = /[-@](?:\d{8}|\d{4}-\d{2}-\d{2})$/;
@@ -271,6 +656,12 @@ var EFFORT_RANK = {
271
656
  high: 3,
272
657
  max: 4
273
658
  };
659
+ var UNSUPPORTED_TOOL_SCHEMA_STRICTNESS = Object.freeze({
660
+ supported: false
661
+ });
662
+ var SUPPORTED_TOOL_SCHEMA_STRICTNESS = Object.freeze({
663
+ supported: true
664
+ });
274
665
  function clampReasoningEffort(effort, supportedEfforts) {
275
666
  if (supportedEfforts.includes(effort)) {
276
667
  return effort;
@@ -349,6 +740,63 @@ function collectRequestedInputModalities(messages) {
349
740
  return [...requested];
350
741
  }
351
742
 
743
+ // src/validate-tool-schema-strictness.ts
744
+ var contractViolationCache = /* @__PURE__ */ new WeakMap();
745
+ function getContractViolations(tool) {
746
+ let cached = contractViolationCache.get(tool.parameters);
747
+ if (cached === void 0) {
748
+ cached = getStrictToolSchemaViolations(
749
+ tool.name,
750
+ zodSchemaToJsonSchema(tool.parameters)
751
+ ).map(({ path, message }) => ({ path, message }));
752
+ contractViolationCache.set(tool.parameters, cached);
753
+ }
754
+ return cached.map((violation) => ({ toolName: tool.name, ...violation }));
755
+ }
756
+ function validateToolSchemaStrictness({
757
+ tools,
758
+ capabilities,
759
+ providerId,
760
+ modelId
761
+ }) {
762
+ const strictTools = Object.values(tools).filter(
763
+ (tool) => tool.strict === true
764
+ );
765
+ if (strictTools.length === 0) {
766
+ return;
767
+ }
768
+ const strictToolNames = strictTools.map((tool) => tool.name);
769
+ const strictCapabilities = capabilities.tools.strictSchemas;
770
+ if (!strictCapabilities.supported) {
771
+ throw new ToolSchemaStrictnessError({
772
+ providerId,
773
+ modelId,
774
+ toolNames: strictToolNames,
775
+ reason: "unsupported"
776
+ });
777
+ }
778
+ const maxStrictTools = strictCapabilities.maxStrictTools;
779
+ if (maxStrictTools !== void 0 && strictTools.length > maxStrictTools) {
780
+ throw new ToolSchemaStrictnessError({
781
+ providerId,
782
+ modelId,
783
+ toolNames: strictToolNames,
784
+ reason: "limit-exceeded",
785
+ maxStrictTools
786
+ });
787
+ }
788
+ const violations = strictTools.flatMap(getContractViolations);
789
+ if (violations.length > 0) {
790
+ throw new ToolSchemaStrictnessError({
791
+ providerId,
792
+ modelId,
793
+ toolNames: [...new Set(violations.map((v) => v.toolName))],
794
+ reason: "invalid-schema",
795
+ violations
796
+ });
797
+ }
798
+ }
799
+
352
800
  // src/model-capabilities-registry.ts
353
801
  var UNKNOWN_MODEL = /* @__PURE__ */ Symbol("unknown-model");
354
802
  function getRegisteredModelCapabilities(registry, modelId) {
@@ -1000,6 +1448,7 @@ export {
1000
1448
  ProviderQuotaExceededError,
1001
1449
  RateLimitError,
1002
1450
  RetryableProviderError,
1451
+ SUPPORTED_TOOL_SCHEMA_STRICTNESS,
1003
1452
  ServiceUnavailableError,
1004
1453
  StreamAbortedError,
1005
1454
  StructuredOutputError,
@@ -1007,7 +1456,9 @@ export {
1007
1456
  StructuredOutputParseError,
1008
1457
  StructuredOutputValidationError,
1009
1458
  TEXT_ONLY_MODALITIES,
1459
+ ToolSchemaStrictnessError,
1010
1460
  UNKNOWN_MODEL,
1461
+ UNSUPPORTED_TOOL_SCHEMA_STRICTNESS,
1011
1462
  UnsupportedInputModalityError,
1012
1463
  ValidationError,
1013
1464
  asObject,
@@ -1026,10 +1477,12 @@ export {
1026
1477
  getProviderMetadata,
1027
1478
  getRegisteredModelCapabilities,
1028
1479
  getRetryAfterSecondsFromError,
1480
+ getStrictToolSchemaViolations,
1029
1481
  getString,
1030
1482
  isAbortErrorByName,
1031
1483
  isRateLimitStatus,
1032
1484
  isTransientUnavailableStatus,
1485
+ normalizeStrictJsonSchema,
1033
1486
  parseRetryAfterSeconds,
1034
1487
  resultToMessage,
1035
1488
  safeParseJsonObject,
@@ -1039,6 +1492,7 @@ export {
1039
1492
  supportsInputModality,
1040
1493
  supportsOutputModality,
1041
1494
  validateInputModalities,
1495
+ validateToolSchemaStrictness,
1042
1496
  wrapChatModel,
1043
1497
  wrapEmbeddingModel,
1044
1498
  wrapImageModel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@core-ai/core-ai",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Type-safe LLM abstraction layer over native provider SDKs",
5
5
  "license": "MIT",
6
6
  "author": "Omnifact (https://omnifact.ai)",