@ai-sdk/anthropic 3.0.136 → 3.0.138
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 +19 -0
- package/dist/index.js +304 -50
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +305 -50
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.d.mts +12 -1
- package/dist/internal/index.d.ts +12 -1
- package/dist/internal/index.js +307 -51
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/index.mjs +306 -50
- package/dist/internal/index.mjs.map +1 -1
- package/docs/05-anthropic.mdx +19 -2
- package/package.json +2 -2
- package/src/anthropic-messages-language-model.ts +136 -24
- package/src/convert-to-anthropic-messages-prompt.ts +21 -2
- package/src/has-circular-schema-reference.ts +118 -0
- package/src/internal/index.ts +1 -0
- package/src/sanitize-json-schema.ts +27 -24
- package/src/supports-native-structured-output-schema.ts +138 -0
package/docs/05-anthropic.mdx
CHANGED
|
@@ -152,7 +152,7 @@ The following optional provider options are available for Anthropic models:
|
|
|
152
152
|
|
|
153
153
|
- `"outputFormat"`: Use the `output_format` parameter to specify the structured output format.
|
|
154
154
|
- `"jsonTool"`: Use a special `"json"` tool to specify the structured output format.
|
|
155
|
-
- `"auto"`: Use `"outputFormat"` when
|
|
155
|
+
- `"auto"`: Use `"outputFormat"` when the model and schema support it, otherwise fall back to `"jsonTool"` (default). Schemas with recursive references, open objects, or more than ten optional properties use the JSON-tool fallback to avoid rejected requests or lost data.
|
|
156
156
|
|
|
157
157
|
- `metadata` _object_
|
|
158
158
|
|
|
@@ -194,10 +194,27 @@ default `"auto"` mode uses native structured outputs through
|
|
|
194
194
|
`output_config.format`. These models reject forced tool use, so the
|
|
195
195
|
`"jsonTool"` mode cannot be used with them. If you
|
|
196
196
|
request `"jsonTool"` anyway, the provider falls back to `"outputFormat"` and
|
|
197
|
-
emits a warning.
|
|
197
|
+
emits a warning. The same fallback applies when these models are served through
|
|
198
|
+
providers that otherwise default to the JSON tool, such as the Amazon Bedrock
|
|
199
|
+
Anthropic and Google Vertex Anthropic providers. On those providers, schemas
|
|
200
|
+
that native structured outputs cannot represent keep the JSON tool:
|
|
201
|
+
self-referencing schemas, schemas with more than 10 optional properties, and
|
|
202
|
+
schemas with open objects such as maps (`additionalProperties` set to a schema
|
|
203
|
+
or `true`), which native structured outputs would return as `{}`.
|
|
204
|
+
Likewise, a `required` or named `toolChoice` is downgraded to
|
|
198
205
|
`auto` with a warning; instruct the model to use the tool in your prompt and
|
|
199
206
|
verify that a tool call was made.
|
|
200
207
|
|
|
208
|
+
The JSON-tool fallback limits which application tools are available:
|
|
209
|
+
`toolChoice: 'none'` sends only the internal JSON response tool, and a named tool
|
|
210
|
+
choice sends only that application tool alongside the JSON response tool. The
|
|
211
|
+
fallback can produce a JSON response without calling the named application tool;
|
|
212
|
+
verify the selected call if your application requires it.
|
|
213
|
+
|
|
214
|
+
When the JSON tool cannot be forced (for example with manual thinking) and the
|
|
215
|
+
model answers in text without calling it, the text of its final answer is
|
|
216
|
+
returned. Text before a tool call is still dropped.
|
|
217
|
+
|
|
201
218
|
### Effort
|
|
202
219
|
|
|
203
220
|
Anthropic introduced an `effort` option with `claude-opus-4-5` that affects thinking, text responses, and function calls. Effort defaults to `high` and you can set it to `medium` or `low` to save tokens and to lower time-to-last-token latency (TTLT). `claude-opus-4-7`, `claude-opus-4-8`, `claude-opus-5`, `claude-opus-5-5`, `claude-fable-5`, `claude-fable-5-1`, `claude-sonnet-5`, `claude-sonnet-5-5`, and `claude-haiku-5-5` additionally support `xhigh` for maximum reasoning effort.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/anthropic",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.138",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@ai-sdk/provider": "3.0.19",
|
|
40
|
-
"@ai-sdk/provider-utils": "4.0.
|
|
40
|
+
"@ai-sdk/provider-utils": "4.0.64"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "20.17.24",
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
import { CacheControlValidator } from './get-cache-control';
|
|
64
64
|
import { mapAnthropicStopReason } from './map-anthropic-stop-reason';
|
|
65
65
|
import { sanitizeJsonSchema } from './sanitize-json-schema';
|
|
66
|
+
import { supportsNativeStructuredOutputSchema } from './supports-native-structured-output-schema';
|
|
66
67
|
|
|
67
68
|
function createCitationSource(
|
|
68
69
|
citation: Citation,
|
|
@@ -216,6 +217,47 @@ type AnthropicMessagesConfig = {
|
|
|
216
217
|
supportsStrictTools?: boolean;
|
|
217
218
|
};
|
|
218
219
|
|
|
220
|
+
const TEXT_OR_THINKING_BLOCK_TYPES = new Set<string>([
|
|
221
|
+
'redacted_thinking',
|
|
222
|
+
'text',
|
|
223
|
+
'thinking',
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
function isContinuationStopReason(
|
|
227
|
+
stopReason: string | null | undefined,
|
|
228
|
+
): boolean {
|
|
229
|
+
return stopReason === 'pause_turn' || stopReason === 'tool_use';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Text parts to return when the JSON response tool is in use: the text after
|
|
234
|
+
* the last tool call or result, and only if the tool was never called.
|
|
235
|
+
*/
|
|
236
|
+
function getJsonResponseToolFallbackText<
|
|
237
|
+
PART extends { type: string; name?: unknown },
|
|
238
|
+
>(
|
|
239
|
+
content: ReadonlyArray<PART>,
|
|
240
|
+
stopReason: string | null | undefined,
|
|
241
|
+
): Set<PART> {
|
|
242
|
+
const text = new Set<PART>();
|
|
243
|
+
if (isContinuationStopReason(stopReason)) {
|
|
244
|
+
return text;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const part of content) {
|
|
248
|
+
if (part.type === 'tool_use' && part.name === 'json') {
|
|
249
|
+
return new Set();
|
|
250
|
+
}
|
|
251
|
+
if (part.type === 'text') {
|
|
252
|
+
text.add(part);
|
|
253
|
+
} else if (!TEXT_OR_THINKING_BLOCK_TYPES.has(part.type)) {
|
|
254
|
+
text.clear();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return text;
|
|
259
|
+
}
|
|
260
|
+
|
|
219
261
|
export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
|
|
220
262
|
readonly specificationVersion = 'v3';
|
|
221
263
|
|
|
@@ -407,6 +449,11 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
|
|
|
407
449
|
(this.config.supportsNativeStructuredOutput ?? true) &&
|
|
408
450
|
modelSupportsStructuredOutput;
|
|
409
451
|
|
|
452
|
+
const supportsResponseSchema =
|
|
453
|
+
responseFormat?.type !== 'json' ||
|
|
454
|
+
responseFormat.schema == null ||
|
|
455
|
+
supportsNativeStructuredOutputSchema(responseFormat.schema);
|
|
456
|
+
|
|
410
457
|
const supportsStrictTools =
|
|
411
458
|
(this.config.supportsStrictTools ?? true) &&
|
|
412
459
|
modelSupportsStructuredOutput;
|
|
@@ -415,17 +462,21 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
|
|
|
415
462
|
anthropicOptions?.structuredOutputMode ?? 'auto';
|
|
416
463
|
let useStructuredOutput =
|
|
417
464
|
structureOutputMode === 'outputFormat' ||
|
|
418
|
-
(structureOutputMode === 'auto' &&
|
|
465
|
+
(structureOutputMode === 'auto' &&
|
|
466
|
+
supportsStructuredOutput &&
|
|
467
|
+
supportsResponseSchema);
|
|
419
468
|
|
|
420
469
|
// The JSON response tool relies on forced tool use, which some models
|
|
421
470
|
// reject (e.g. Opus 5.5, Fable 5.1). Fall back to native structured
|
|
422
|
-
// outputs when the
|
|
471
|
+
// outputs even when the provider disables them by default (Bedrock,
|
|
472
|
+
// Vertex), unless they would reject the schema or lose data from it.
|
|
423
473
|
if (
|
|
424
474
|
!useStructuredOutput &&
|
|
425
475
|
rejectsForcedToolUse &&
|
|
426
|
-
|
|
476
|
+
modelSupportsStructuredOutput &&
|
|
427
477
|
responseFormat?.type === 'json' &&
|
|
428
|
-
responseFormat.schema != null
|
|
478
|
+
responseFormat.schema != null &&
|
|
479
|
+
supportsNativeStructuredOutputSchema(responseFormat.schema)
|
|
429
480
|
) {
|
|
430
481
|
warnings.push({
|
|
431
482
|
type: 'unsupported',
|
|
@@ -963,7 +1014,15 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
|
|
|
963
1014
|
} = await prepareTools(
|
|
964
1015
|
jsonResponseTool != null
|
|
965
1016
|
? {
|
|
966
|
-
tools: [
|
|
1017
|
+
tools: [
|
|
1018
|
+
...(tools ?? []).filter(
|
|
1019
|
+
tool =>
|
|
1020
|
+
toolChoice?.type !== 'none' &&
|
|
1021
|
+
(toolChoice?.type !== 'tool' ||
|
|
1022
|
+
tool.name === toolChoice.toolName),
|
|
1023
|
+
),
|
|
1024
|
+
jsonResponseTool,
|
|
1025
|
+
],
|
|
967
1026
|
toolChoice: { type: 'required' },
|
|
968
1027
|
disableParallelToolUse: true,
|
|
969
1028
|
cacheControlValidator,
|
|
@@ -1146,11 +1205,20 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
|
|
|
1146
1205
|
const serverToolCalls: Record<string, string> = {}; // tool_use_id -> provider tool name
|
|
1147
1206
|
let isJsonResponseFromTool = false;
|
|
1148
1207
|
|
|
1208
|
+
// The JSON response tool call becomes the text, so other text is dropped,
|
|
1209
|
+
// unless the model never called the tool and answered in text instead.
|
|
1210
|
+
const jsonResponseToolFallbackText = usesJsonResponseTool
|
|
1211
|
+
? getJsonResponseToolFallbackText(response.content, response.stop_reason)
|
|
1212
|
+
: undefined;
|
|
1213
|
+
|
|
1149
1214
|
// map response content to content array
|
|
1150
1215
|
for (const part of response.content) {
|
|
1151
1216
|
switch (part.type) {
|
|
1152
1217
|
case 'text': {
|
|
1153
|
-
if (
|
|
1218
|
+
if (
|
|
1219
|
+
jsonResponseToolFallbackText == null ||
|
|
1220
|
+
jsonResponseToolFallbackText.has(part)
|
|
1221
|
+
) {
|
|
1154
1222
|
const webSearchCitations = part.citations?.filter(
|
|
1155
1223
|
citation => citation.type === 'web_search_result_location',
|
|
1156
1224
|
);
|
|
@@ -1793,6 +1861,21 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
|
|
|
1793
1861
|
let safeguardResults: AnthropicMessageMetadata['safeguardResults'];
|
|
1794
1862
|
let container: AnthropicMessageMetadata['container'] | null = null;
|
|
1795
1863
|
let isJsonResponseFromTool = false;
|
|
1864
|
+
// Text held back while the JSON response tool is in use: it is only the
|
|
1865
|
+
// answer if no tool block follows it and the model never calls the tool.
|
|
1866
|
+
type HeldBackText = { id: string; text: string; citations: Citation[] };
|
|
1867
|
+
const heldBackText: HeldBackText[] = [];
|
|
1868
|
+
const heldBackTextByIndex = new Map<number, HeldBackText>();
|
|
1869
|
+
const createTextEnd = (
|
|
1870
|
+
id: string,
|
|
1871
|
+
citations: Citation[],
|
|
1872
|
+
): LanguageModelV3StreamPart => ({
|
|
1873
|
+
type: 'text-end',
|
|
1874
|
+
id,
|
|
1875
|
+
...(citations.length > 0 && {
|
|
1876
|
+
providerMetadata: { anthropic: { citations } },
|
|
1877
|
+
}),
|
|
1878
|
+
});
|
|
1796
1879
|
let isMessageOpen = false;
|
|
1797
1880
|
let activeMessageId: string | null | undefined;
|
|
1798
1881
|
let hasInvalidMessageSequence = false;
|
|
@@ -1860,11 +1943,23 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
|
|
|
1860
1943
|
|
|
1861
1944
|
blockType = contentBlockType;
|
|
1862
1945
|
|
|
1946
|
+
// text before a tool call or result is preamble, not the answer:
|
|
1947
|
+
if (!TEXT_OR_THINKING_BLOCK_TYPES.has(contentBlockType)) {
|
|
1948
|
+
heldBackText.length = 0;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1863
1951
|
switch (contentBlockType) {
|
|
1864
1952
|
case 'text': {
|
|
1865
1953
|
// when a json response tool is used, the tool call is returned as text,
|
|
1866
|
-
// so we
|
|
1954
|
+
// so the text content is held back until we know whether it was called:
|
|
1867
1955
|
if (usesJsonResponseTool) {
|
|
1956
|
+
const entry = {
|
|
1957
|
+
id: String(value.index),
|
|
1958
|
+
text: '',
|
|
1959
|
+
citations: [],
|
|
1960
|
+
};
|
|
1961
|
+
heldBackText.push(entry);
|
|
1962
|
+
heldBackTextByIndex.set(value.index, entry);
|
|
1868
1963
|
return;
|
|
1869
1964
|
}
|
|
1870
1965
|
|
|
@@ -2414,17 +2509,12 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
|
|
|
2414
2509
|
|
|
2415
2510
|
switch (contentBlock.type) {
|
|
2416
2511
|
case 'text': {
|
|
2417
|
-
controller.enqueue(
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
citations: contentBlock.citations,
|
|
2424
|
-
},
|
|
2425
|
-
},
|
|
2426
|
-
}),
|
|
2427
|
-
});
|
|
2512
|
+
controller.enqueue(
|
|
2513
|
+
createTextEnd(
|
|
2514
|
+
String(value.index),
|
|
2515
|
+
contentBlock.citations,
|
|
2516
|
+
),
|
|
2517
|
+
);
|
|
2428
2518
|
break;
|
|
2429
2519
|
}
|
|
2430
2520
|
|
|
@@ -2547,8 +2637,12 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
|
|
|
2547
2637
|
switch (deltaType) {
|
|
2548
2638
|
case 'text_delta': {
|
|
2549
2639
|
// when a json response tool is used, the tool call is returned as text,
|
|
2550
|
-
// so we
|
|
2640
|
+
// so the text content is held back until we know whether it was called:
|
|
2551
2641
|
if (usesJsonResponseTool) {
|
|
2642
|
+
const entry = heldBackTextByIndex.get(value.index);
|
|
2643
|
+
if (entry != null) {
|
|
2644
|
+
entry.text += value.delta.text;
|
|
2645
|
+
}
|
|
2552
2646
|
return; // excluding the text-start will also exclude the text-end
|
|
2553
2647
|
}
|
|
2554
2648
|
|
|
@@ -2659,11 +2753,13 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
|
|
|
2659
2753
|
const citation = value.delta.citation;
|
|
2660
2754
|
const contentBlock = contentBlocks[value.index];
|
|
2661
2755
|
|
|
2662
|
-
if (
|
|
2663
|
-
contentBlock?.type === 'text'
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2756
|
+
if (citation.type === 'web_search_result_location') {
|
|
2757
|
+
if (contentBlock?.type === 'text') {
|
|
2758
|
+
contentBlock.citations.push(citation);
|
|
2759
|
+
}
|
|
2760
|
+
heldBackTextByIndex
|
|
2761
|
+
.get(value.index)
|
|
2762
|
+
?.citations.push(citation);
|
|
2667
2763
|
}
|
|
2668
2764
|
|
|
2669
2765
|
const source = createCitationSource(
|
|
@@ -2709,6 +2805,7 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
|
|
|
2709
2805
|
|
|
2710
2806
|
isMessageOpen = true;
|
|
2711
2807
|
activeMessageId = value.message.id;
|
|
2808
|
+
heldBackTextByIndex.clear();
|
|
2712
2809
|
|
|
2713
2810
|
usage.input_tokens = value.message.usage.input_tokens;
|
|
2714
2811
|
usage.cache_read_input_tokens =
|
|
@@ -2926,6 +3023,21 @@ export class AnthropicMessagesLanguageModel implements LanguageModelV3 {
|
|
|
2926
3023
|
providerMetadata[providerOptionsName] = anthropicMetadata;
|
|
2927
3024
|
}
|
|
2928
3025
|
|
|
3026
|
+
// a message that hands off to a tool or continuation is not the
|
|
3027
|
+
// final answer, so its held-back text is discarded:
|
|
3028
|
+
if (
|
|
3029
|
+
!isJsonResponseFromTool &&
|
|
3030
|
+
!isContinuationStopReason(finishReason.raw)
|
|
3031
|
+
) {
|
|
3032
|
+
for (const { id, text, citations } of heldBackText) {
|
|
3033
|
+
if (text.length === 0) continue;
|
|
3034
|
+
controller.enqueue({ type: 'text-start', id });
|
|
3035
|
+
controller.enqueue({ type: 'text-delta', id, delta: text });
|
|
3036
|
+
controller.enqueue(createTextEnd(id, citations));
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
heldBackText.length = 0;
|
|
3040
|
+
|
|
2929
3041
|
controller.enqueue({
|
|
2930
3042
|
type: 'finish',
|
|
2931
3043
|
finishReason,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type SharedV3ProviderMetadata,
|
|
9
9
|
} from '@ai-sdk/provider';
|
|
10
10
|
import {
|
|
11
|
+
stringifyWithBigIntAndCycles,
|
|
11
12
|
convertBase64ToUint8Array,
|
|
12
13
|
convertToBase64,
|
|
13
14
|
parseProviderOptions,
|
|
@@ -585,7 +586,7 @@ export async function convertToAnthropicMessagesPrompt({
|
|
|
585
586
|
case 'json':
|
|
586
587
|
case 'error-json':
|
|
587
588
|
default:
|
|
588
|
-
contentValue =
|
|
589
|
+
contentValue = stringifyWithBigIntAndCycles(output.value);
|
|
589
590
|
break;
|
|
590
591
|
}
|
|
591
592
|
|
|
@@ -899,7 +900,7 @@ export async function convertToAnthropicMessagesPrompt({
|
|
|
899
900
|
type: 'tool_use',
|
|
900
901
|
id: part.toolCallId,
|
|
901
902
|
name: part.toolName,
|
|
902
|
-
input: part.input,
|
|
903
|
+
input: parseToolCallInput(part.input),
|
|
903
904
|
...(caller && { caller }),
|
|
904
905
|
cache_control: cacheControl,
|
|
905
906
|
});
|
|
@@ -1564,3 +1565,21 @@ function getAnthropicCaller(
|
|
|
1564
1565
|
|
|
1565
1566
|
return caller?.type === 'direct' ? { type: 'direct' } : undefined;
|
|
1566
1567
|
}
|
|
1568
|
+
|
|
1569
|
+
function parseToolCallInput(input: unknown): unknown {
|
|
1570
|
+
if (typeof input === 'string') {
|
|
1571
|
+
try {
|
|
1572
|
+
const parsed = secureJsonParse(input);
|
|
1573
|
+
if (
|
|
1574
|
+
typeof parsed === 'object' &&
|
|
1575
|
+
parsed !== null &&
|
|
1576
|
+
!Array.isArray(parsed)
|
|
1577
|
+
) {
|
|
1578
|
+
return parsed;
|
|
1579
|
+
}
|
|
1580
|
+
} catch {
|
|
1581
|
+
// Preserve malformed input for replay.
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return input;
|
|
1585
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { JSONSchema7 } from '@ai-sdk/provider';
|
|
2
|
+
|
|
3
|
+
// Keywords whose values are maps of name -> schema rather than a schema.
|
|
4
|
+
const SCHEMA_MAP_KEYWORDS = new Set([
|
|
5
|
+
'dependentSchemas',
|
|
6
|
+
'patternProperties',
|
|
7
|
+
'properties',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
// Keywords holding instance data, which can contain a literal `$ref` key.
|
|
11
|
+
const DATA_KEYWORDS = new Set(['const', 'default', 'enum', 'examples']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether a JSON schema contains a cycle of local `$ref`s, e.g. a tree node
|
|
15
|
+
* whose children reference the node itself.
|
|
16
|
+
*
|
|
17
|
+
* Anthropic's native structured outputs reject such schemas, while the JSON
|
|
18
|
+
* response tool accepts them.
|
|
19
|
+
*/
|
|
20
|
+
export function hasCircularSchemaReference(schema: JSONSchema7): boolean {
|
|
21
|
+
const visiting = new Set<string>();
|
|
22
|
+
const visited = new Set<string>();
|
|
23
|
+
|
|
24
|
+
const visit = (pointer: string): boolean => {
|
|
25
|
+
if (visiting.has(pointer)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (visited.has(pointer)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const target = resolveSchemaPointer(schema, pointer);
|
|
33
|
+
if (target === undefined) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
visiting.add(pointer);
|
|
38
|
+
const isCircular = collectLocalRefs(target).some(visit);
|
|
39
|
+
visiting.delete(pointer);
|
|
40
|
+
visited.add(pointer);
|
|
41
|
+
|
|
42
|
+
return isCircular;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return visit('#');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function collectLocalRefs(node: unknown, refs: string[] = []): string[] {
|
|
49
|
+
if (Array.isArray(node)) {
|
|
50
|
+
for (const item of node) {
|
|
51
|
+
collectLocalRefs(item, refs);
|
|
52
|
+
}
|
|
53
|
+
return refs;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!isRecord(node)) {
|
|
57
|
+
return refs;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [key, value] of Object.entries(node)) {
|
|
61
|
+
if (key === '$ref') {
|
|
62
|
+
if (typeof value === 'string' && value.startsWith('#')) {
|
|
63
|
+
refs.push(value);
|
|
64
|
+
}
|
|
65
|
+
} else if (SCHEMA_MAP_KEYWORDS.has(key) && isRecord(value)) {
|
|
66
|
+
for (const subschema of Object.values(value)) {
|
|
67
|
+
collectLocalRefs(subschema, refs);
|
|
68
|
+
}
|
|
69
|
+
} else if (
|
|
70
|
+
// definitions are only reached through a `$ref`:
|
|
71
|
+
key !== '$defs' &&
|
|
72
|
+
key !== 'definitions' &&
|
|
73
|
+
!DATA_KEYWORDS.has(key)
|
|
74
|
+
) {
|
|
75
|
+
collectLocalRefs(value, refs);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return refs;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveSchemaPointer(
|
|
83
|
+
schema: JSONSchema7,
|
|
84
|
+
pointer: string,
|
|
85
|
+
): unknown {
|
|
86
|
+
if (pointer === '#') {
|
|
87
|
+
return schema;
|
|
88
|
+
}
|
|
89
|
+
if (!pointer.startsWith('#/')) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let decodedPointer: string;
|
|
94
|
+
try {
|
|
95
|
+
decodedPointer = decodeURIComponent(pointer.slice(2));
|
|
96
|
+
} catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let current: unknown = schema;
|
|
101
|
+
for (const rawSegment of decodedPointer.split('/')) {
|
|
102
|
+
const segment = rawSegment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
103
|
+
|
|
104
|
+
if (Array.isArray(current)) {
|
|
105
|
+
current = current[Number(segment)];
|
|
106
|
+
} else if (isRecord(current)) {
|
|
107
|
+
current = current[segment];
|
|
108
|
+
} else {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
117
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
118
|
+
}
|
package/src/internal/index.ts
CHANGED
|
@@ -6,3 +6,4 @@ export { anthropicTools } from '../anthropic-tools';
|
|
|
6
6
|
export type { AnthropicMessagesModelId } from '../anthropic-messages-options';
|
|
7
7
|
export { prepareTools } from '../anthropic-prepare-tools';
|
|
8
8
|
export { sanitizeJsonSchema } from '../sanitize-json-schema';
|
|
9
|
+
export { supportsNativeStructuredOutputSchema } from '../supports-native-structured-output-schema';
|
|
@@ -53,12 +53,11 @@ function sanitizeDefinition(
|
|
|
53
53
|
|
|
54
54
|
function sanitizeSchema(schema: JSONSchema7): JSONSchema7 {
|
|
55
55
|
const result: JSONSchema7 = {};
|
|
56
|
-
const schemaWithDefs = schema as JSONSchema7 & {
|
|
57
|
-
$defs?: Record<string, JSONSchema7Definition>;
|
|
58
|
-
};
|
|
59
56
|
|
|
57
|
+
// Keep the definitions next to a `$ref` so a top-level reference still
|
|
58
|
+
// resolves (e.g. `{ $ref: '#/definitions/Answer', definitions: {...} }`).
|
|
60
59
|
if (schema.$ref != null) {
|
|
61
|
-
return { $ref: schema.$ref };
|
|
60
|
+
return { $ref: schema.$ref, ...sanitizeSchemaDefinitions(schema) };
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
if (schema.$schema != null) {
|
|
@@ -103,26 +102,7 @@ function sanitizeSchema(schema: JSONSchema7): JSONSchema7 {
|
|
|
103
102
|
result.allOf = schema.allOf.map(sanitizeDefinition);
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
result.definitions = Object.fromEntries(
|
|
108
|
-
Object.entries(schema.definitions).map(([name, definition]) => [
|
|
109
|
-
name,
|
|
110
|
-
sanitizeDefinition(definition),
|
|
111
|
-
]),
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (schemaWithDefs.$defs != null) {
|
|
116
|
-
const resultWithDefs = result as JSONSchema7 & {
|
|
117
|
-
$defs?: Record<string, JSONSchema7Definition>;
|
|
118
|
-
};
|
|
119
|
-
resultWithDefs.$defs = Object.fromEntries(
|
|
120
|
-
Object.entries(schemaWithDefs.$defs).map(([name, definition]) => [
|
|
121
|
-
name,
|
|
122
|
-
sanitizeDefinition(definition),
|
|
123
|
-
]),
|
|
124
|
-
);
|
|
125
|
-
}
|
|
105
|
+
Object.assign(result, sanitizeSchemaDefinitions(schema));
|
|
126
106
|
|
|
127
107
|
if (schema.type === 'object' || schema.properties != null) {
|
|
128
108
|
if (schema.properties != null) {
|
|
@@ -165,6 +145,29 @@ function sanitizeSchema(schema: JSONSchema7): JSONSchema7 {
|
|
|
165
145
|
return result;
|
|
166
146
|
}
|
|
167
147
|
|
|
148
|
+
function sanitizeSchemaDefinitions(schema: JSONSchema7): Pick<
|
|
149
|
+
JSONSchema7,
|
|
150
|
+
'definitions'
|
|
151
|
+
> & {
|
|
152
|
+
$defs?: Record<string, JSONSchema7Definition>;
|
|
153
|
+
} {
|
|
154
|
+
const { $defs } = schema as { $defs?: Record<string, JSONSchema7Definition> };
|
|
155
|
+
const sanitizeAll = (definitions: Record<string, JSONSchema7Definition>) =>
|
|
156
|
+
Object.fromEntries(
|
|
157
|
+
Object.entries(definitions).map(([name, definition]) => [
|
|
158
|
+
name,
|
|
159
|
+
sanitizeDefinition(definition),
|
|
160
|
+
]),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...(schema.definitions != null && {
|
|
165
|
+
definitions: sanitizeAll(schema.definitions),
|
|
166
|
+
}),
|
|
167
|
+
...($defs != null && { $defs: sanitizeAll($defs) }),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
168
171
|
function getConstraintDescription(schema: JSONSchema7): string | undefined {
|
|
169
172
|
const descriptions = DESCRIPTION_CONSTRAINT_KEYS.flatMap(key => {
|
|
170
173
|
const value = schema[key];
|