@@ -44,6 +44,7 @@ import { logger } from 'genkit/logging';
4444
4545import { KNOWN_CLAUDE_MODELS , extractVersion } from '../models.js' ;
4646import { AnthropicConfigSchema , type ClaudeRunnerParams } from '../types.js' ;
47+ import { removeUndefinedProperties } from '../utils.js' ;
4748import { BaseRunner } from './base.js' ;
4849import { RunnerTypes } from './types.js' ;
4950
@@ -66,6 +67,57 @@ const BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES = new Set<string>([
6667 'container_upload' ,
6768] ) ;
6869
70+ const BETA_APIS = [
71+ // 'message-batches-2024-09-24',
72+ // 'prompt-caching-2024-07-31',
73+ // 'computer-use-2025-01-24',
74+ // 'pdfs-2024-09-25',
75+ // 'token-counting-2024-11-01',
76+ // 'token-efficient-tools-2025-02-19',
77+ // 'output-128k-2025-02-19',
78+ 'files-api-2025-04-14' ,
79+ // 'mcp-client-2025-04-04',
80+ // 'dev-full-thinking-2025-05-14',
81+ // 'interleaved-thinking-2025-05-14',
82+ // 'code-execution-2025-05-22',
83+ // 'extended-cache-ttl-2025-04-11',
84+ // 'context-1m-2025-08-07',
85+ // 'context-management-2025-06-27',
86+ // 'model-context-window-exceeded-2025-08-26',
87+ // 'skills-2025-10-02',
88+ 'effort-2025-11-24' ,
89+ // 'advanced-tool-use-2025-11-20',
90+ 'structured-outputs-2025-11-13' ,
91+ ] ;
92+
93+ /**
94+ * Transforms a JSON schema to be compatible with Anthropic's structured output requirements.
95+ * Anthropic requires `additionalProperties: false` on all object types.
96+ * @see https://docs.anthropic.com/en/docs/build-with-claude/structured-outputs#json-schema-limitations
97+ */
98+ function toAnthropicSchema (
99+ schema : Record < string , unknown >
100+ ) : Record < string , unknown > {
101+ const out = structuredClone ( schema ) ;
102+
103+ // Remove $schema if present
104+ delete out . $schema ;
105+
106+ // Add additionalProperties: false to objects
107+ if ( out . type === 'object' ) {
108+ out . additionalProperties = false ;
109+ }
110+
111+ // Recursively process nested objects
112+ for ( const key in out ) {
113+ if ( typeof out [ key ] === 'object' && out [ key ] !== null ) {
114+ out [ key ] = toAnthropicSchema ( out [ key ] as Record < string , unknown > ) ;
115+ }
116+ }
117+
118+ return out ;
119+ }
120+
69121const unsupportedServerToolError = ( blockType : string ) : string =>
70122 `Anthropic beta runner does not yet support server-managed tool block '${ blockType } '. Please retry against the stable API or wait for dedicated support.` ;
71123
@@ -140,6 +192,26 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
140192
141193 // Media
142194 if ( part . media ) {
195+ if ( part . media . contentType === 'anthropic/file' ) {
196+ return {
197+ type : 'document' ,
198+ source : {
199+ type : 'file' ,
200+ file_id : part . media . url ,
201+ } ,
202+ } ;
203+ }
204+
205+ if ( part . media . contentType === 'anthropic/image' ) {
206+ return {
207+ type : 'image' ,
208+ source : {
209+ type : 'file' ,
210+ file_id : part . media . url ,
211+ } ,
212+ } ;
213+ }
214+
143215 if ( part . media . contentType === 'application/pdf' ) {
144216 return {
145217 type : 'document' ,
@@ -249,45 +321,49 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
249321 : system ;
250322 }
251323
252- const body : BetaMessageCreateParamsNonStreaming = {
324+ const thinkingConfig = this . toAnthropicThinkingConfig (
325+ request . config ?. thinking
326+ ) as BetaMessageCreateParams [ 'thinking' ] | undefined ;
327+
328+ // Need to extract topP and topK from request.config to avoid duplicate properties being added to the body
329+ // This happens because topP and topK have different property names (top_p and top_k) in the Anthropic API.
330+ // Thinking is extracted separately to avoid type issues.
331+ // ApiVersion is extracted separately as it's not a valid property for the Anthropic API.
332+ const {
333+ topP,
334+ topK,
335+ apiVersion : _1 ,
336+ thinking : _2 ,
337+ ...restConfig
338+ } = request . config ?? { } ;
339+
340+ const body = {
253341 model : mappedModelName ,
254342 max_tokens :
255343 request . config ?. maxOutputTokens ?? this . DEFAULT_MAX_OUTPUT_TOKENS ,
256344 messages,
257- } ;
258-
259- if ( betaSystem !== undefined ) body . system = betaSystem ;
260- if ( request . config ?. stopSequences !== undefined )
261- body . stop_sequences = request . config . stopSequences ;
262- if ( request . config ?. temperature !== undefined )
263- body . temperature = request . config . temperature ;
264- if ( request . config ?. topK !== undefined ) body . top_k = request . config . topK ;
265- if ( request . config ?. topP !== undefined ) body . top_p = request . config . topP ;
266- if ( request . config ?. tool_choice !== undefined ) {
267- body . tool_choice = request . config
268- . tool_choice as BetaMessageCreateParams [ 'tool_choice' ] ;
269- }
270- if ( request . config ?. metadata !== undefined ) {
271- body . metadata = request . config
272- . metadata as BetaMessageCreateParams [ 'metadata' ] ;
273- }
274- if ( request . tools ) {
275- body . tools = request . tools . map ( ( tool ) => this . toAnthropicTool ( tool ) ) ;
276- }
277- const thinkingConfig = this . toAnthropicThinkingConfig (
278- request . config ?. thinking
279- ) ;
280- if ( thinkingConfig ) {
281- body . thinking = thinkingConfig as BetaMessageCreateParams [ 'thinking' ] ;
282- }
283-
284- if ( request . output ?. format && request . output . format !== 'text' ) {
285- throw new Error (
286- `Only text output format is supported for Claude models currently`
287- ) ;
288- }
289-
290- return body ;
345+ system : betaSystem ,
346+ stop_sequences : request . config ?. stopSequences ,
347+ temperature : request . config ?. temperature ,
348+ top_k : topK ,
349+ top_p : topP ,
350+ tool_choice : request . config ?. tool_choice ,
351+ metadata : request . config ?. metadata ,
352+ tools : request . tools ?. map ( ( tool ) => this . toAnthropicTool ( tool ) ) ,
353+ thinking : thinkingConfig ,
354+ output_format : this . isStructuredOutputEnabled ( request )
355+ ? {
356+ type : 'json_schema' ,
357+ schema : toAnthropicSchema ( request . output ! . schema ! ) ,
358+ }
359+ : undefined ,
360+ betas : Array . isArray ( request . config ?. betas )
361+ ? [ ...( request . config ?. betas ?? [ ] ) ]
362+ : [ ...BETA_APIS ] ,
363+ ...restConfig ,
364+ } as BetaMessageCreateParamsNonStreaming ;
365+
366+ return removeUndefinedProperties ( body ) ;
291367 }
292368
293369 /**
@@ -316,46 +392,50 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
316392 ]
317393 : system ;
318394
319- const body : BetaMessageCreateParamsStreaming = {
395+ const thinkingConfig = this . toAnthropicThinkingConfig (
396+ request . config ?. thinking
397+ ) as BetaMessageCreateParams [ 'thinking' ] | undefined ;
398+
399+ // Need to extract topP and topK from request.config to avoid duplicate properties being added to the body
400+ // This happens because topP and topK have different property names (top_p and top_k) in the Anthropic API.
401+ // Thinking is extracted separately to avoid type issues.
402+ // ApiVersion is extracted separately as it's not a valid property for the Anthropic API.
403+ const {
404+ topP,
405+ topK,
406+ apiVersion : _1 ,
407+ thinking : _2 ,
408+ ...restConfig
409+ } = request . config ?? { } ;
410+
411+ const body = {
320412 model : mappedModelName ,
321413 max_tokens :
322414 request . config ?. maxOutputTokens ?? this . DEFAULT_MAX_OUTPUT_TOKENS ,
323415 messages,
324416 stream : true ,
325- } ;
326-
327- if ( betaSystem !== undefined ) body . system = betaSystem ;
328- if ( request . config ?. stopSequences !== undefined )
329- body . stop_sequences = request . config . stopSequences ;
330- if ( request . config ?. temperature !== undefined )
331- body . temperature = request . config . temperature ;
332- if ( request . config ?. topK !== undefined ) body . top_k = request . config . topK ;
333- if ( request . config ?. topP !== undefined ) body . top_p = request . config . topP ;
334- if ( request . config ?. tool_choice !== undefined ) {
335- body . tool_choice = request . config
336- . tool_choice as BetaMessageCreateParams [ 'tool_choice' ] ;
337- }
338- if ( request . config ?. metadata !== undefined ) {
339- body . metadata = request . config
340- . metadata as BetaMessageCreateParams [ 'metadata' ] ;
341- }
342- if ( request . tools ) {
343- body . tools = request . tools . map ( ( tool ) => this . toAnthropicTool ( tool ) ) ;
344- }
345- const thinkingConfig = this . toAnthropicThinkingConfig (
346- request . config ?. thinking
347- ) ;
348- if ( thinkingConfig ) {
349- body . thinking = thinkingConfig as BetaMessageCreateParams [ 'thinking' ] ;
350- }
351-
352- if ( request . output ?. format && request . output . format !== 'text' ) {
353- throw new Error (
354- `Only text output format is supported for Claude models currently`
355- ) ;
356- }
357-
358- return body ;
417+ system : betaSystem ,
418+ stop_sequences : request . config ?. stopSequences ,
419+ temperature : request . config ?. temperature ,
420+ top_k : topK ,
421+ top_p : topP ,
422+ tool_choice : request . config ?. tool_choice ,
423+ metadata : request . config ?. metadata ,
424+ tools : request . tools ?. map ( ( tool ) => this . toAnthropicTool ( tool ) ) ,
425+ thinking : thinkingConfig ,
426+ output_format : this . isStructuredOutputEnabled ( request )
427+ ? {
428+ type : 'json_schema' ,
429+ schema : toAnthropicSchema ( request . output ! . schema ! ) ,
430+ }
431+ : undefined ,
432+ betas : Array . isArray ( request . config ?. betas )
433+ ? [ ...( request . config ?. betas ?? [ ] ) ]
434+ : [ ...BETA_APIS ] ,
435+ ...restConfig ,
436+ } as BetaMessageCreateParamsStreaming ;
437+
438+ return removeUndefinedProperties ( body ) ;
359439 }
360440
361441 protected toGenkitResponse ( message : BetaMessage ) : GenerateResponseData {
@@ -491,4 +571,14 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
491571 return 'other' ;
492572 }
493573 }
574+
575+ private isStructuredOutputEnabled (
576+ request : GenerateRequest < typeof AnthropicConfigSchema >
577+ ) : boolean {
578+ return ! ! (
579+ request . output ?. schema &&
580+ request . output . constrained &&
581+ request . output . format === 'json'
582+ ) ;
583+ }
494584}
0 commit comments