@ai-sdk/anthropic 4.0.93 → 4.0.94

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @ai-sdk/anthropic
2
2
 
3
+ ## 4.0.94
4
+
5
+ ### Patch Changes
6
+
7
+ - f4c5f1e: fix(anthropic): use native structured outputs for Claude models that reject forced tool use (Sonnet 5.5, Opus 5.5, Fable 5.1) even when the provider disables them by default, as Amazon Bedrock and Google Vertex do. Self-referencing schemas keep the JSON response tool. Also keep `definitions` and `$defs` next to a top-level `$ref` when sanitizing structured output schemas, so the reference still resolves.
8
+ - e10aebb: fix(anthropic): when the JSON response tool is in use but the model answers in text without calling it (for example with manual thinking, where the forced tool choice is downgraded to `auto`), return the final answer text instead of dropping it. Text before a tool call, result, or message continuation is still dropped.
9
+ - b0d51fe: fix(anthropic): keep the JSON response tool for schemas that native structured outputs cannot represent when upgrading models that reject forced tool use. Schemas with open objects (maps, free-form objects) would otherwise come back as `{}`, and schemas with more than 10 optional properties can be rejected as too complex.
10
+
3
11
  ## 4.0.93
4
12
 
5
13
  ### Patch Changes
package/dist/index.js CHANGED
@@ -3700,8 +3700,10 @@ function sanitizeDefinition(definition) {
3700
3700
  }
3701
3701
  function sanitizeSchema(schema) {
3702
3702
  const result = {};
3703
- const schemaWithDefs = schema;
3704
- if (schema.$ref != null) return { $ref: schema.$ref };
3703
+ if (schema.$ref != null) return {
3704
+ $ref: schema.$ref,
3705
+ ...sanitizeSchemaDefinitions(schema)
3706
+ };
3705
3707
  if (schema.$schema != null) result.$schema = schema.$schema;
3706
3708
  if (schema.$id != null) result.$id = schema.$id;
3707
3709
  if (schema.title != null) result.title = schema.title;
@@ -3713,11 +3715,7 @@ function sanitizeSchema(schema) {
3713
3715
  if (schema.anyOf != null) result.anyOf = schema.anyOf.map(sanitizeDefinition);
3714
3716
  else if (schema.oneOf != null) result.anyOf = schema.oneOf.map(sanitizeDefinition);
3715
3717
  if (schema.allOf != null) result.allOf = schema.allOf.map(sanitizeDefinition);
3716
- if (schema.definitions != null) result.definitions = Object.fromEntries(Object.entries(schema.definitions).map(([name, definition]) => [name, sanitizeDefinition(definition)]));
3717
- if (schemaWithDefs.$defs != null) {
3718
- const resultWithDefs = result;
3719
- resultWithDefs.$defs = Object.fromEntries(Object.entries(schemaWithDefs.$defs).map(([name, definition]) => [name, sanitizeDefinition(definition)]));
3720
- }
3718
+ Object.assign(result, sanitizeSchemaDefinitions(schema));
3721
3719
  if (schema.type === "object" || schema.properties != null) {
3722
3720
  if (schema.properties != null) result.properties = Object.fromEntries(Object.entries(schema.properties).map(([name, definition]) => [name, sanitizeDefinition(definition)]));
3723
3721
  result.additionalProperties = false;
@@ -3729,6 +3727,14 @@ function sanitizeSchema(schema) {
3729
3727
  if (constraintDescription != null) result.description = result.description == null ? constraintDescription : `${result.description}\n${constraintDescription}`;
3730
3728
  return result;
3731
3729
  }
3730
+ function sanitizeSchemaDefinitions(schema) {
3731
+ const { $defs } = schema;
3732
+ const sanitizeAll = (definitions) => Object.fromEntries(Object.entries(definitions).map(([name, definition]) => [name, sanitizeDefinition(definition)]));
3733
+ return {
3734
+ ...schema.definitions != null && { definitions: sanitizeAll(schema.definitions) },
3735
+ ...$defs != null && { $defs: sanitizeAll($defs) }
3736
+ };
3737
+ }
3732
3738
  function getConstraintDescription(schema) {
3733
3739
  const descriptions = DESCRIPTION_CONSTRAINT_KEYS.flatMap((key) => {
3734
3740
  const value = schema[key];
@@ -3746,6 +3752,155 @@ function formatConstraintValue(value) {
3746
3752
  return JSON.stringify(value);
3747
3753
  }
3748
3754
  //#endregion
3755
+ //#region src/has-circular-schema-reference.ts
3756
+ const SCHEMA_MAP_KEYWORDS = /* @__PURE__ */ new Set([
3757
+ "dependentSchemas",
3758
+ "patternProperties",
3759
+ "properties"
3760
+ ]);
3761
+ const DATA_KEYWORDS = /* @__PURE__ */ new Set([
3762
+ "const",
3763
+ "default",
3764
+ "enum",
3765
+ "examples"
3766
+ ]);
3767
+ /**
3768
+ * Whether a JSON schema contains a cycle of local `$ref`s, e.g. a tree node
3769
+ * whose children reference the node itself.
3770
+ *
3771
+ * Anthropic's native structured outputs reject such schemas, while the JSON
3772
+ * response tool accepts them.
3773
+ */
3774
+ function hasCircularSchemaReference(schema) {
3775
+ const visiting = /* @__PURE__ */ new Set();
3776
+ const visited = /* @__PURE__ */ new Set();
3777
+ const visit = (pointer) => {
3778
+ if (visiting.has(pointer)) return true;
3779
+ if (visited.has(pointer)) return false;
3780
+ const target = resolveSchemaPointer(schema, pointer);
3781
+ if (target === void 0) return false;
3782
+ visiting.add(pointer);
3783
+ const isCircular = collectLocalRefs(target).some(visit);
3784
+ visiting.delete(pointer);
3785
+ visited.add(pointer);
3786
+ return isCircular;
3787
+ };
3788
+ return visit("#");
3789
+ }
3790
+ function collectLocalRefs(node, refs = []) {
3791
+ if (Array.isArray(node)) {
3792
+ for (const item of node) collectLocalRefs(item, refs);
3793
+ return refs;
3794
+ }
3795
+ if (!isRecord(node)) return refs;
3796
+ for (const [key, value] of Object.entries(node)) if (key === "$ref") {
3797
+ if (typeof value === "string" && value.startsWith("#")) refs.push(value);
3798
+ } else if (SCHEMA_MAP_KEYWORDS.has(key) && isRecord(value)) for (const subschema of Object.values(value)) collectLocalRefs(subschema, refs);
3799
+ else if (key !== "$defs" && key !== "definitions" && !DATA_KEYWORDS.has(key)) collectLocalRefs(value, refs);
3800
+ return refs;
3801
+ }
3802
+ function resolveSchemaPointer(schema, pointer) {
3803
+ if (pointer === "#") return schema;
3804
+ if (!pointer.startsWith("#/")) return;
3805
+ let decodedPointer;
3806
+ try {
3807
+ decodedPointer = decodeURIComponent(pointer.slice(2));
3808
+ } catch {
3809
+ return;
3810
+ }
3811
+ let current = schema;
3812
+ for (const rawSegment of decodedPointer.split("/")) {
3813
+ const segment = rawSegment.replace(/~1/g, "/").replace(/~0/g, "~");
3814
+ if (Array.isArray(current)) current = current[Number(segment)];
3815
+ else if (isRecord(current)) current = current[segment];
3816
+ else return;
3817
+ }
3818
+ return current;
3819
+ }
3820
+ //#endregion
3821
+ //#region src/supports-native-structured-output-schema.ts
3822
+ const MAX_OPTIONAL_PROPERTIES = 10;
3823
+ const SUBSCHEMA_KEYWORDS = [
3824
+ "additionalProperties",
3825
+ "contains",
3826
+ "else",
3827
+ "if",
3828
+ "items",
3829
+ "not",
3830
+ "then"
3831
+ ];
3832
+ const SUBSCHEMA_LIST_KEYWORDS = [
3833
+ "allOf",
3834
+ "anyOf",
3835
+ "oneOf",
3836
+ "prefixItems"
3837
+ ];
3838
+ const SUBSCHEMA_MAP_KEYWORDS = [
3839
+ "dependentSchemas",
3840
+ "patternProperties",
3841
+ "properties"
3842
+ ];
3843
+ /**
3844
+ * Whether Anthropic's native structured outputs (`output_config.format`) can
3845
+ * represent a schema without rejecting it or losing data.
3846
+ *
3847
+ * Rejected: self-referencing schemas, and schemas with too many optional
3848
+ * properties. Lossy: open objects (maps, free-form objects), which native
3849
+ * structured outputs close with `additionalProperties: false`, so the model
3850
+ * can only return `{}` for them.
3851
+ */
3852
+ function supportsNativeStructuredOutputSchema(schema) {
3853
+ if (hasCircularSchemaReference(schema)) return false;
3854
+ const { optionalProperties, hasOpenObject } = getSchemaStats(schema, schema, /* @__PURE__ */ new Map());
3855
+ return !hasOpenObject && optionalProperties <= MAX_OPTIONAL_PROPERTIES;
3856
+ }
3857
+ function getSchemaStats(node, root, refStats) {
3858
+ const stats = {
3859
+ optionalProperties: 0,
3860
+ hasOpenObject: false
3861
+ };
3862
+ if (!isRecord(node)) return stats;
3863
+ const add = (child) => {
3864
+ const childStats = getSchemaStats(child, root, refStats);
3865
+ stats.optionalProperties += childStats.optionalProperties;
3866
+ stats.hasOpenObject ||= childStats.hasOpenObject;
3867
+ };
3868
+ if (typeof node.$ref === "string" && node.$ref.startsWith("#")) {
3869
+ let resolved = refStats.get(node.$ref);
3870
+ if (resolved == null) {
3871
+ resolved = getSchemaStats(resolveSchemaPointer(root, node.$ref), root, refStats);
3872
+ refStats.set(node.$ref, resolved);
3873
+ }
3874
+ stats.optionalProperties += resolved.optionalProperties;
3875
+ stats.hasOpenObject ||= resolved.hasOpenObject;
3876
+ }
3877
+ if (isOpenObject(node)) stats.hasOpenObject = true;
3878
+ if (isRecord(node.properties)) {
3879
+ const required = new Set(Array.isArray(node.required) ? node.required : []);
3880
+ stats.optionalProperties += Object.keys(node.properties).filter((name) => !required.has(name)).length;
3881
+ }
3882
+ for (const keyword of SUBSCHEMA_KEYWORDS) {
3883
+ const value = node[keyword];
3884
+ if (Array.isArray(value)) value.forEach(add);
3885
+ else add(value);
3886
+ }
3887
+ for (const keyword of SUBSCHEMA_LIST_KEYWORDS) {
3888
+ const value = node[keyword];
3889
+ if (Array.isArray(value)) value.forEach(add);
3890
+ }
3891
+ for (const keyword of SUBSCHEMA_MAP_KEYWORDS) {
3892
+ const value = node[keyword];
3893
+ if (isRecord(value)) Object.values(value).forEach(add);
3894
+ }
3895
+ return stats;
3896
+ }
3897
+ function isOpenObject(node) {
3898
+ const { additionalProperties, patternProperties, properties, type } = node;
3899
+ if (additionalProperties === true || isRecord(additionalProperties)) return true;
3900
+ if (isRecord(patternProperties) && Object.keys(patternProperties).length > 0) return true;
3901
+ return (type === "object" || Array.isArray(type) && type.includes("object")) && properties == null && additionalProperties !== false;
3902
+ }
3903
+ //#endregion
3749
3904
  //#region src/anthropic-language-model.ts
3750
3905
  function createAnthropicStreamError(error) {
3751
3906
  const inferredMetadata = getAnthropicStreamErrorMetadata(error.type);
@@ -3864,6 +4019,28 @@ function getProviderOptionsName(provider) {
3864
4019
  const dotIndex = provider.indexOf(".");
3865
4020
  return dotIndex === -1 ? provider : provider.substring(0, dotIndex);
3866
4021
  }
4022
+ const TEXT_OR_THINKING_BLOCK_TYPES = /* @__PURE__ */ new Set([
4023
+ "redacted_thinking",
4024
+ "text",
4025
+ "thinking"
4026
+ ]);
4027
+ function isContinuationStopReason(stopReason) {
4028
+ return stopReason === "pause_turn" || stopReason === "tool_use";
4029
+ }
4030
+ /**
4031
+ * Text parts to return when the JSON response tool is in use: the text after
4032
+ * the last tool call or result, and only if the tool was never called.
4033
+ */
4034
+ function getJsonResponseToolFallbackText(content, stopReason) {
4035
+ const text = /* @__PURE__ */ new Set();
4036
+ if (isContinuationStopReason(stopReason)) return text;
4037
+ for (const part of content) {
4038
+ if (part.type === "tool_use" && part.name === "json") return /* @__PURE__ */ new Set();
4039
+ if (part.type === "text") text.add(part);
4040
+ else if (!TEXT_OR_THINKING_BLOCK_TYPES.has(part.type)) text.clear();
4041
+ }
4042
+ return text;
4043
+ }
3867
4044
  var AnthropicLanguageModel = class AnthropicLanguageModel {
3868
4045
  static [WORKFLOW_SERIALIZE](model) {
3869
4046
  return serializeModelOptions({
@@ -3977,7 +4154,7 @@ var AnthropicLanguageModel = class AnthropicLanguageModel {
3977
4154
  const supportsStrictTools = (config.supportsStrictTools ?? true) && modelSupportsStructuredOutput;
3978
4155
  const structureOutputMode = anthropicOptions?.structuredOutputMode ?? "auto";
3979
4156
  let useStructuredOutput = structureOutputMode === "outputFormat" || structureOutputMode === "auto" && supportsStructuredOutput;
3980
- if (!useStructuredOutput && rejectsForcedToolUse && supportsStructuredOutput && responseFormat?.type === "json" && responseFormat.schema != null) {
4157
+ if (!useStructuredOutput && rejectsForcedToolUse && modelSupportsStructuredOutput && responseFormat?.type === "json" && responseFormat.schema != null && supportsNativeStructuredOutputSchema(responseFormat.schema)) {
3981
4158
  warnings.push({
3982
4159
  type: "unsupported",
3983
4160
  feature: "providerOptions.anthropic.structuredOutputMode",
@@ -4384,9 +4561,10 @@ var AnthropicLanguageModel = class AnthropicLanguageModel {
4384
4561
  const serverToolCalls = {};
4385
4562
  let isJsonResponseFromTool = false;
4386
4563
  const hasWebSearchResults = response.content.some((part) => part.type === "web_search_tool_result" && Array.isArray(part.content) && part.content.length > 0);
4564
+ const jsonResponseToolFallbackText = usesJsonResponseTool ? getJsonResponseToolFallbackText(response.content, response.stop_reason) : void 0;
4387
4565
  for (const part of response.content) switch (part.type) {
4388
4566
  case "text":
4389
- if (!usesJsonResponseTool) {
4567
+ if (jsonResponseToolFallbackText == null || jsonResponseToolFallbackText.has(part)) {
4390
4568
  const citations = part.citations ?? [];
4391
4569
  content.push({
4392
4570
  type: "text",
@@ -4824,6 +5002,16 @@ var AnthropicLanguageModel = class AnthropicLanguageModel {
4824
5002
  let safeguardResults;
4825
5003
  let container = null;
4826
5004
  let isJsonResponseFromTool = false;
5005
+ const heldBackText = [];
5006
+ const heldBackTextByIndex = /* @__PURE__ */ new Map();
5007
+ const createTextEnd = (id, citations) => ({
5008
+ type: "text-end",
5009
+ id,
5010
+ ...citations.length > 0 && {
5011
+ citations: citations.map((citation) => mapAnthropicCitation(citation, citationDocuments)),
5012
+ providerMetadata: { anthropic: { citations } }
5013
+ }
5014
+ });
4827
5015
  let hasWebSearchResults = false;
4828
5016
  const fallbackWebCitations = [];
4829
5017
  let isMessageOpen = false;
@@ -4870,9 +5058,19 @@ var AnthropicLanguageModel = class AnthropicLanguageModel {
4870
5058
  return;
4871
5059
  }
4872
5060
  blockType = contentBlockType;
5061
+ if (!TEXT_OR_THINKING_BLOCK_TYPES.has(contentBlockType)) heldBackText.length = 0;
4873
5062
  switch (contentBlockType) {
4874
5063
  case "text":
4875
- if (usesJsonResponseTool) return;
5064
+ if (usesJsonResponseTool) {
5065
+ const entry = {
5066
+ id: String(value.index),
5067
+ text: "",
5068
+ citations: []
5069
+ };
5070
+ heldBackText.push(entry);
5071
+ heldBackTextByIndex.set(value.index, entry);
5072
+ return;
5073
+ }
4876
5074
  contentBlocks[value.index] = {
4877
5075
  type: "text",
4878
5076
  citations: []
@@ -5258,14 +5456,7 @@ var AnthropicLanguageModel = class AnthropicLanguageModel {
5258
5456
  const contentBlock = contentBlocks[value.index];
5259
5457
  switch (contentBlock.type) {
5260
5458
  case "text":
5261
- controller.enqueue({
5262
- type: "text-end",
5263
- id: String(value.index),
5264
- ...contentBlock.citations.length > 0 && {
5265
- citations: contentBlock.citations.map((citation) => mapAnthropicCitation(citation, citationDocuments)),
5266
- providerMetadata: { anthropic: { citations: contentBlock.citations } }
5267
- }
5268
- });
5459
+ controller.enqueue(createTextEnd(String(value.index), contentBlock.citations));
5269
5460
  break;
5270
5461
  case "reasoning":
5271
5462
  controller.enqueue({
@@ -5325,7 +5516,11 @@ var AnthropicLanguageModel = class AnthropicLanguageModel {
5325
5516
  const deltaType = value.delta.type;
5326
5517
  switch (deltaType) {
5327
5518
  case "text_delta":
5328
- if (usesJsonResponseTool) return;
5519
+ if (usesJsonResponseTool) {
5520
+ const entry = heldBackTextByIndex.get(value.index);
5521
+ if (entry != null) entry.text += value.delta.text;
5522
+ return;
5523
+ }
5329
5524
  controller.enqueue({
5330
5525
  type: "text-delta",
5331
5526
  id: String(value.index),
@@ -5386,6 +5581,7 @@ var AnthropicLanguageModel = class AnthropicLanguageModel {
5386
5581
  const citation = value.delta.citation;
5387
5582
  const contentBlock = contentBlocks[value.index];
5388
5583
  if (contentBlock?.type === "text") contentBlock.citations.push(citation);
5584
+ heldBackTextByIndex.get(value.index)?.citations.push(citation);
5389
5585
  if (citation.type === "web_search_result_location") fallbackWebCitations.push(citation);
5390
5586
  else {
5391
5587
  const source = createCitationSource(citation, citationDocuments, generateId);
@@ -5411,6 +5607,7 @@ var AnthropicLanguageModel = class AnthropicLanguageModel {
5411
5607
  }
5412
5608
  isMessageOpen = true;
5413
5609
  activeMessageId = value.message.id;
5610
+ heldBackTextByIndex.clear();
5414
5611
  usage.input_tokens = value.message.usage.input_tokens;
5415
5612
  usage.cache_read_input_tokens = value.message.usage.cache_read_input_tokens ?? 0;
5416
5613
  usage.cache_creation_input_tokens = value.message.usage.cache_creation_input_tokens ?? 0;
@@ -5531,6 +5728,20 @@ var AnthropicLanguageModel = class AnthropicLanguageModel {
5531
5728
  if (source) controller.enqueue(source);
5532
5729
  }
5533
5730
  fallbackWebCitations.length = 0;
5731
+ if (!isJsonResponseFromTool && !isContinuationStopReason(finishReason.raw)) for (const { id, text, citations } of heldBackText) {
5732
+ if (text.length === 0) continue;
5733
+ controller.enqueue({
5734
+ type: "text-start",
5735
+ id
5736
+ });
5737
+ controller.enqueue({
5738
+ type: "text-delta",
5739
+ id,
5740
+ delta: text
5741
+ });
5742
+ controller.enqueue(createTextEnd(id, citations));
5743
+ }
5744
+ heldBackText.length = 0;
5534
5745
  controller.enqueue({
5535
5746
  type: "finish",
5536
5747
  finishReason,
@@ -7133,7 +7344,7 @@ var AnthropicSkills = class {
7133
7344
  };
7134
7345
  //#endregion
7135
7346
  //#region src/version.ts
7136
- const VERSION = "4.0.93";
7347
+ const VERSION = "4.0.94";
7137
7348
  //#endregion
7138
7349
  //#region src/anthropic-provider.ts
7139
7350
  const ANTHROPIC_API_URL = "https://api.anthropic.com";