genai-lite 0.13.0 → 0.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.
@@ -53,14 +53,6 @@ export declare class AnthropicClientAdapter implements ILLMClientAdapter {
53
53
  private mapAnthropicUsage;
54
54
  private mergeAnthropicUsage;
55
55
  private createSyntheticMessage;
56
- /**
57
- * Recursively adds additionalProperties: false to all object schemas
58
- * Required by Anthropic's strict mode for structured outputs
59
- *
60
- * @param schema - The JSON schema to process
61
- * @returns A new schema with additionalProperties: false on all objects
62
- */
63
- private addAdditionalPropertiesFalse;
64
56
  /**
65
57
  * Formats messages for Anthropic API with proper system message handling
66
58
  *
@@ -9,6 +9,7 @@ exports.AnthropicClientAdapter = void 0;
9
9
  const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
10
10
  const types_1 = require("./types");
11
11
  const errorUtils_1 = require("../../shared/adapters/errorUtils");
12
+ const schemaUtils_1 = require("../../shared/adapters/schemaUtils");
12
13
  const systemMessageUtils_1 = require("../../shared/adapters/systemMessageUtils");
13
14
  const defaultLogger_1 = require("../../logging/defaultLogger");
14
15
  /**
@@ -54,26 +55,15 @@ class AnthropicClientAdapter {
54
55
  hasStopSequences: !!messageParams.stop_sequences,
55
56
  useStructuredOutput,
56
57
  });
57
- // Make the API call - use beta endpoint for structured output
58
- let completion;
59
- if (useStructuredOutput) {
60
- // For structured output, we need to use the beta messages endpoint with proper headers
61
- completion = await anthropic.messages.create(messageParams, {
62
- headers: {
63
- 'anthropic-beta': 'structured-outputs-2025-11-13',
64
- },
65
- ...requestTransportOptions,
66
- });
67
- }
68
- else {
69
- completion =
70
- Object.keys(requestTransportOptions).length > 0
71
- ? await anthropic.messages.create(messageParams, requestTransportOptions)
72
- : await anthropic.messages.create(messageParams);
73
- }
58
+ // Structured and unstructured requests take the same endpoint - structured
59
+ // output is GA, so there is no beta header or beta endpoint involved.
60
+ const completion = Object.keys(requestTransportOptions).length > 0
61
+ ? await anthropic.messages.create(messageParams, requestTransportOptions)
62
+ : await anthropic.messages.create(messageParams);
74
63
  this.logger.info(`Anthropic API call successful, response ID: ${completion.id}`);
75
- // Convert to standardized response format
76
- // Cast to any to handle beta response type differences
64
+ // Convert to standardized response format. `create` is typed against the
65
+ // streaming/non-streaming param union; we never set `stream`, so this is
66
+ // always a Message.
77
67
  return this.createSuccessResponse(completion, request);
78
68
  }
79
69
  catch (error) {
@@ -94,17 +84,12 @@ class AnthropicClientAdapter {
94
84
  let started = false;
95
85
  try {
96
86
  const { anthropic, messageParams, requestTransportOptions, useStructuredOutput, } = this.prepareMessageRequest(request, apiKey, options);
97
- this.logger.info(`Making Anthropic streaming API call for model: ${request.modelId}`);
98
- const streamOptions = useStructuredOutput
99
- ? {
100
- headers: {
101
- "anthropic-beta": "structured-outputs-2025-11-13",
102
- },
103
- ...requestTransportOptions,
104
- }
105
- : requestTransportOptions;
106
- const stream = Object.keys(streamOptions).length > 0
107
- ? anthropic.messages.stream(messageParams, streamOptions)
87
+ this.logger.info(`Making Anthropic streaming API call for model: ${request.modelId}`, {
88
+ useStructuredOutput,
89
+ });
90
+ // No beta header for structured output - output_config.format is GA.
91
+ const stream = Object.keys(requestTransportOptions).length > 0
92
+ ? anthropic.messages.stream(messageParams, requestTransportOptions)
108
93
  : anthropic.messages.stream(messageParams);
109
94
  for await (const event of stream) {
110
95
  sawEvent = true;
@@ -254,20 +239,22 @@ class AnthropicClientAdapter {
254
239
  stop_sequences: request.settings.stopSequences,
255
240
  }),
256
241
  };
257
- // Handle structured output configuration for Anthropic
258
- // Note: Structured output requires the beta API endpoint
242
+ // Handle structured output configuration for Anthropic.
243
+ // Structured outputs are generally available: the stable request field is
244
+ // output_config.format, with no beta header. The format object carries only
245
+ // `type` and `schema` - the generic StructuredOutputSettings `name` and
246
+ // `strict` fields exist for other providers and are not serialized here.
259
247
  if (useStructuredOutput) {
260
248
  const so = request.settings.structuredOutput;
261
249
  // Anthropic requires additionalProperties: false on all object schemas
262
250
  const processedSchema = so.strict !== false
263
- ? this.addAdditionalPropertiesFalse(so.schema)
264
- : so.schema;
265
- // Anthropic's format: output_format.schema is the schema directly
266
- messageParams.output_format = {
267
- type: 'json_schema',
268
- name: so.name,
269
- schema: processedSchema,
270
- strict: so.strict !== false,
251
+ ? (0, schemaUtils_1.applyStrictSchemaConstraints)({ ...so.schema })
252
+ : { ...so.schema };
253
+ messageParams.output_config = {
254
+ format: {
255
+ type: "json_schema",
256
+ schema: processedSchema,
257
+ },
271
258
  };
272
259
  }
273
260
  // Handle reasoning/thinking configuration for Claude models
@@ -358,35 +345,6 @@ class AnthropicClientAdapter {
358
345
  usage: accumulator.usage,
359
346
  };
360
347
  }
361
- /**
362
- * Recursively adds additionalProperties: false to all object schemas
363
- * Required by Anthropic's strict mode for structured outputs
364
- *
365
- * @param schema - The JSON schema to process
366
- * @returns A new schema with additionalProperties: false on all objects
367
- */
368
- addAdditionalPropertiesFalse(schema) {
369
- if (!schema || typeof schema !== 'object') {
370
- return schema;
371
- }
372
- const processed = { ...schema };
373
- // If this is an object type, add additionalProperties: false
374
- if (processed.type === 'object') {
375
- processed.additionalProperties = false;
376
- // Process nested properties
377
- if (processed.properties) {
378
- processed.properties = {};
379
- for (const [key, value] of Object.entries(schema.properties)) {
380
- processed.properties[key] = this.addAdditionalPropertiesFalse(value);
381
- }
382
- }
383
- }
384
- // Process array items
385
- if (processed.items) {
386
- processed.items = this.addAdditionalPropertiesFalse(schema.items);
387
- }
388
- return processed;
389
- }
390
348
  /**
391
349
  * Formats messages for Anthropic API with proper system message handling
392
350
  *
@@ -504,24 +462,15 @@ class AnthropicClientAdapter {
504
462
  if (textBlocks.length === 0) {
505
463
  throw new Error("Invalid completion structure from Anthropic API");
506
464
  }
507
- // Extract thinking/reasoning content if available
508
- let reasoning;
509
- let reasoning_details;
510
- // Check for thinking content in the response
511
- if (completion.thinking_content) {
512
- reasoning = completion.thinking_content;
513
- }
465
+ // Extract reasoning from thinking content blocks. This is the only place
466
+ // Anthropic exposes it: `Message` has no `thinking_content` or
467
+ // `reasoning_details` field (the latter is an OpenRouter concept, handled in
468
+ // OpenRouterClientAdapter), so branches reading those could never fire.
514
469
  const thinkingContent = completion.content
515
470
  .filter((block) => block.type === "thinking" && typeof block.thinking === "string")
516
471
  .map((block) => block.thinking)
517
472
  .join("");
518
- if (!reasoning && thinkingContent) {
519
- reasoning = thinkingContent;
520
- }
521
- // Check for reasoning details that need to be preserved
522
- if (completion.reasoning_details) {
523
- reasoning_details = completion.reasoning_details;
524
- }
473
+ const reasoning = thinkingContent || undefined;
525
474
  // Map Anthropic's stop reason to our standard format
526
475
  const finishReason = this.mapAnthropicStopReason(completion.stop_reason);
527
476
  const choice = {
@@ -536,10 +485,6 @@ class AnthropicClientAdapter {
536
485
  if (reasoning && request.settings.reasoning && !request.settings.reasoning.exclude) {
537
486
  choice.reasoning = reasoning;
538
487
  }
539
- // Always include reasoning_details if present (for tool use continuation)
540
- if (reasoning_details) {
541
- choice.reasoning_details = reasoning_details;
542
- }
543
488
  return {
544
489
  id: completion.id,
545
490
  provider: request.providerId,
@@ -565,12 +510,22 @@ class AnthropicClientAdapter {
565
510
  mapAnthropicStopReason(anthropicReason) {
566
511
  if (!anthropicReason)
567
512
  return null;
513
+ // Anthropic's StopReason union is exactly:
514
+ // end_turn | max_tokens | stop_sequence | tool_use | pause_turn | refusal
515
+ // Note there is no `content_filter` - that is OpenAI's vocabulary. A model
516
+ // that declines on policy grounds reports `refusal`, which we normalize to
517
+ // `content_filter` so callers can detect a blocked completion with the same
518
+ // check they already use for OpenAI and Gemini.
568
519
  const reasonMap = {
569
520
  end_turn: "stop",
570
521
  max_tokens: "length",
571
522
  stop_sequence: "stop",
572
- content_filter: "content_filter",
573
523
  tool_use: "tool_calls",
524
+ refusal: "content_filter",
525
+ // Anthropic-specific: a long-running turn was paused and the caller is
526
+ // expected to continue it. No equivalent in the normalized vocabulary, so
527
+ // it maps to "other" deliberately rather than by falling through.
528
+ pause_turn: "other",
574
529
  };
575
530
  return reasonMap[anthropicReason] || "other";
576
531
  }
@@ -52,14 +52,6 @@ export declare class OpenAIClientAdapter implements ILLMClientAdapter {
52
52
  private createTransportOptions;
53
53
  private getStreamChoiceState;
54
54
  private createSyntheticCompletion;
55
- /**
56
- * Recursively adds additionalProperties: false to all object schemas
57
- * Required by OpenAI's strict mode for structured outputs
58
- *
59
- * @param schema - The JSON schema to process
60
- * @returns A new schema with additionalProperties: false on all objects
61
- */
62
- private addAdditionalPropertiesFalse;
63
55
  /**
64
56
  * Formats messages for OpenAI API
65
57
  *
@@ -10,6 +10,7 @@ const openai_1 = __importDefault(require("openai"));
10
10
  const types_1 = require("./types");
11
11
  const errorUtils_1 = require("../../shared/adapters/errorUtils");
12
12
  const logprobsUtils_1 = require("../../shared/adapters/logprobsUtils");
13
+ const schemaUtils_1 = require("../../shared/adapters/schemaUtils");
13
14
  const systemMessageUtils_1 = require("../../shared/adapters/systemMessageUtils");
14
15
  const defaultLogger_1 = require("../../logging/defaultLogger");
15
16
  /**
@@ -231,8 +232,10 @@ class OpenAIClientAdapter {
231
232
  }
232
233
  if (request.settings.structuredOutput?.schema && request.settings.structuredOutput.enabled !== false) {
233
234
  const so = request.settings.structuredOutput;
235
+ // OpenAI strict mode additionally requires `required` to list every
236
+ // property of each object schema.
234
237
  const processedSchema = so.strict !== false
235
- ? this.addAdditionalPropertiesFalse(so.schema)
238
+ ? (0, schemaUtils_1.applyStrictSchemaConstraints)({ ...so.schema }, { requireAllProperties: true })
236
239
  : so.schema;
237
240
  completionParams.response_format = {
238
241
  type: 'json_schema',
@@ -288,38 +291,6 @@ class OpenAIClientAdapter {
288
291
  ...(usage && { usage }),
289
292
  };
290
293
  }
291
- /**
292
- * Recursively adds additionalProperties: false to all object schemas
293
- * Required by OpenAI's strict mode for structured outputs
294
- *
295
- * @param schema - The JSON schema to process
296
- * @returns A new schema with additionalProperties: false on all objects
297
- */
298
- addAdditionalPropertiesFalse(schema) {
299
- if (!schema || typeof schema !== 'object') {
300
- return schema;
301
- }
302
- const processed = { ...schema };
303
- // If this is an object type, add additionalProperties: false and ensure required includes all properties
304
- if (processed.type === 'object') {
305
- processed.additionalProperties = false;
306
- // Process nested properties
307
- if (processed.properties) {
308
- const propertyKeys = Object.keys(schema.properties);
309
- processed.properties = {};
310
- for (const [key, value] of Object.entries(schema.properties)) {
311
- processed.properties[key] = this.addAdditionalPropertiesFalse(value);
312
- }
313
- // OpenAI strict mode requires 'required' to include ALL properties
314
- processed.required = propertyKeys;
315
- }
316
- }
317
- // Process array items
318
- if (processed.items) {
319
- processed.items = this.addAdditionalPropertiesFalse(schema.items);
320
- }
321
- return processed;
322
- }
323
294
  /**
324
295
  * Formats messages for OpenAI API
325
296
  *
@@ -873,6 +873,10 @@ exports.SUPPORTED_MODELS = [
873
873
  outputType: 'summary',
874
874
  requiresStreamingAbove: 21333,
875
875
  },
876
+ structuredOutput: {
877
+ supported: true,
878
+ strictMode: true,
879
+ },
876
880
  },
877
881
  {
878
882
  id: "claude-sonnet-4-5-20250929",
@@ -897,6 +901,10 @@ exports.SUPPORTED_MODELS = [
897
901
  outputType: 'summary',
898
902
  requiresStreamingAbove: 21333,
899
903
  },
904
+ structuredOutput: {
905
+ supported: true,
906
+ strictMode: true,
907
+ },
900
908
  },
901
909
  {
902
910
  id: "claude-haiku-4-5-20251001",
@@ -921,8 +929,14 @@ exports.SUPPORTED_MODELS = [
921
929
  outputType: 'summary',
922
930
  requiresStreamingAbove: 21333,
923
931
  },
932
+ structuredOutput: {
933
+ supported: true,
934
+ strictMode: true,
935
+ },
924
936
  },
925
937
  // Anthropic Models - Claude 4 Series
938
+ // Note: Anthropic's structured outputs are generally available for Claude 4.5
939
+ // and later models only, so the entries below declare it unsupported.
926
940
  {
927
941
  id: "claude-sonnet-4-20250514",
928
942
  name: "Claude Sonnet 4",
@@ -946,6 +960,10 @@ exports.SUPPORTED_MODELS = [
946
960
  outputType: 'summary',
947
961
  requiresStreamingAbove: 21333,
948
962
  },
963
+ structuredOutput: {
964
+ supported: false,
965
+ notes: "Anthropic structured outputs require Claude 4.5 or later",
966
+ },
949
967
  },
950
968
  {
951
969
  id: "claude-opus-4-20250514",
@@ -970,6 +988,10 @@ exports.SUPPORTED_MODELS = [
970
988
  outputType: 'summary',
971
989
  requiresStreamingAbove: 21333,
972
990
  },
991
+ structuredOutput: {
992
+ supported: false,
993
+ notes: "Anthropic structured outputs require Claude 4.5 or later",
994
+ },
973
995
  },
974
996
  {
975
997
  id: "claude-3-7-sonnet-20250219",
@@ -994,6 +1016,10 @@ exports.SUPPORTED_MODELS = [
994
1016
  outputType: 'full',
995
1017
  requiresStreamingAbove: 21333,
996
1018
  },
1019
+ structuredOutput: {
1020
+ supported: false,
1021
+ notes: "Anthropic structured outputs require Claude 4.5 or later",
1022
+ },
997
1023
  },
998
1024
  {
999
1025
  id: "claude-3-5-sonnet-20241022",
@@ -1008,6 +1034,10 @@ exports.SUPPORTED_MODELS = [
1008
1034
  supportsPromptCache: true,
1009
1035
  cacheWritesPrice: 3.75,
1010
1036
  cacheReadsPrice: 0.3,
1037
+ structuredOutput: {
1038
+ supported: false,
1039
+ notes: "Anthropic structured outputs require Claude 4.5 or later",
1040
+ },
1011
1041
  },
1012
1042
  {
1013
1043
  id: "claude-3-5-haiku-20241022",
@@ -1022,6 +1052,10 @@ exports.SUPPORTED_MODELS = [
1022
1052
  supportsPromptCache: true,
1023
1053
  cacheWritesPrice: 1.0,
1024
1054
  cacheReadsPrice: 0.08,
1055
+ structuredOutput: {
1056
+ supported: false,
1057
+ notes: "Anthropic structured outputs require Claude 4.5 or later",
1058
+ },
1025
1059
  },
1026
1060
  // Google Gemini Models - Gemini 3 Series (Preview)
1027
1061
  {
@@ -0,0 +1,32 @@
1
+ export interface StrictSchemaOptions {
2
+ /**
3
+ * When true, every object schema's `required` is rewritten to list all of its
4
+ * properties. OpenAI's strict mode demands this; Anthropic's does not.
5
+ *
6
+ * @default false
7
+ */
8
+ requireAllProperties?: boolean;
9
+ }
10
+ /**
11
+ * Returns a deep copy of `schema` with `additionalProperties: false` set on every
12
+ * object schema, as required by OpenAI's and Anthropic's strict structured-output
13
+ * modes.
14
+ *
15
+ * The input is never mutated.
16
+ *
17
+ * Traversal covers `properties`, `items` (single or tuple form), `prefixItems`,
18
+ * `$defs`, `definitions`, `anyOf`, `oneOf`, `allOf`, and `not`. Note that `$ref`
19
+ * pointers are **not** resolved — they are left as-is, and the schemas they point
20
+ * at are constrained where they are *defined* (typically under `$defs`), which is
21
+ * what the providers validate. Both providers reject genuinely recursive schemas
22
+ * anyway, so resolving refs would buy nothing.
23
+ *
24
+ * Cyclic *JavaScript* object graphs (a schema object reachable from itself) are
25
+ * handled: each input node is copied at most once and revisits reuse that copy,
26
+ * so the walk terminates and shared subschemas stay shared.
27
+ *
28
+ * @param schema - The JSON schema to process
29
+ * @param options - Provider-specific strictness tweaks
30
+ * @returns A new schema with strict-mode constraints applied
31
+ */
32
+ export declare function applyStrictSchemaConstraints<T>(schema: T, options?: StrictSchemaOptions): T;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ // AI Summary: Shared JSON Schema preparation for providers whose strict
3
+ // structured-output mode requires additionalProperties: false on every object
4
+ // schema. Traverses properties, items (incl. tuple form), $defs/definitions,
5
+ // anyOf/oneOf/allOf, prefixItems and not, and is safe against cyclic input.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.applyStrictSchemaConstraints = applyStrictSchemaConstraints;
8
+ /** Keywords whose value is a map of name -> subschema. */
9
+ const SUBSCHEMA_MAPS = ["properties", "$defs", "definitions"];
10
+ /** Keywords whose value is an array of subschemas. */
11
+ const SUBSCHEMA_ARRAYS = ["anyOf", "oneOf", "allOf", "prefixItems"];
12
+ /**
13
+ * Returns a deep copy of `schema` with `additionalProperties: false` set on every
14
+ * object schema, as required by OpenAI's and Anthropic's strict structured-output
15
+ * modes.
16
+ *
17
+ * The input is never mutated.
18
+ *
19
+ * Traversal covers `properties`, `items` (single or tuple form), `prefixItems`,
20
+ * `$defs`, `definitions`, `anyOf`, `oneOf`, `allOf`, and `not`. Note that `$ref`
21
+ * pointers are **not** resolved — they are left as-is, and the schemas they point
22
+ * at are constrained where they are *defined* (typically under `$defs`), which is
23
+ * what the providers validate. Both providers reject genuinely recursive schemas
24
+ * anyway, so resolving refs would buy nothing.
25
+ *
26
+ * Cyclic *JavaScript* object graphs (a schema object reachable from itself) are
27
+ * handled: each input node is copied at most once and revisits reuse that copy,
28
+ * so the walk terminates and shared subschemas stay shared.
29
+ *
30
+ * @param schema - The JSON schema to process
31
+ * @param options - Provider-specific strictness tweaks
32
+ * @returns A new schema with strict-mode constraints applied
33
+ */
34
+ function applyStrictSchemaConstraints(schema, options = {}) {
35
+ return walk(schema, options, new Map());
36
+ }
37
+ function walk(node, options, seen) {
38
+ if (!node || typeof node !== "object") {
39
+ return node;
40
+ }
41
+ const cached = seen.get(node);
42
+ if (cached !== undefined) {
43
+ return cached;
44
+ }
45
+ if (Array.isArray(node)) {
46
+ const copy = [];
47
+ seen.set(node, copy);
48
+ for (const entry of node) {
49
+ copy.push(walk(entry, options, seen));
50
+ }
51
+ return copy;
52
+ }
53
+ const source = node;
54
+ const processed = { ...source };
55
+ // Registered before recursing so a cycle resolves to this same object.
56
+ seen.set(node, processed);
57
+ if (source.type === "object") {
58
+ processed.additionalProperties = false;
59
+ const properties = source.properties;
60
+ if (options.requireAllProperties && properties && typeof properties === "object") {
61
+ processed.required = Object.keys(properties);
62
+ }
63
+ }
64
+ for (const key of SUBSCHEMA_MAPS) {
65
+ const value = source[key];
66
+ if (value && typeof value === "object" && !Array.isArray(value)) {
67
+ const out = {};
68
+ for (const [name, subschema] of Object.entries(value)) {
69
+ out[name] = walk(subschema, options, seen);
70
+ }
71
+ processed[key] = out;
72
+ }
73
+ }
74
+ for (const key of SUBSCHEMA_ARRAYS) {
75
+ const value = source[key];
76
+ if (Array.isArray(value)) {
77
+ processed[key] = value.map((subschema) => walk(subschema, options, seen));
78
+ }
79
+ }
80
+ if (source.items !== undefined) {
81
+ processed.items = walk(source.items, options, seen);
82
+ }
83
+ if (source.not !== undefined) {
84
+ processed.not = walk(source.not, options, seen);
85
+ }
86
+ return processed;
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genai-lite",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "A lightweight, portable toolkit for interacting with various Generative AI APIs.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",