Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
72f1271
feat(anthropic): add structured output
CorieW Dec 2, 2025
2c0fa8a
test(js/plugins/anthropic): add live test for structured output
cabljac Dec 2, 2025
f76acf2
fix(js/plugins/anthropic): filter by model and dynamically enhance ou…
cabljac Dec 2, 2025
e79bea5
fix(js/plugins/anthropic): pass through constrained output options co…
cabljac Dec 3, 2025
30f65ae
fix(anthropic): fix and add tests
CorieW Dec 3, 2025
5366cc4
refactor(anthropic): beta api addition moved
CorieW Dec 4, 2025
ccde30a
chore(anthropic): remove some comments
CorieW Dec 4, 2025
ba9b86a
refactor(anthropic): clean code a bit
CorieW Dec 4, 2025
17abcd7
feat(testapps/anthropic): add testapp for structured outputs
CorieW Dec 8, 2025
a221d21
chore: format
CorieW Dec 8, 2025
a40ea16
Merge branch 'add-structured-output-2' into add-structured-output-tes…
CorieW Dec 8, 2025
a0f3a6d
refactor(anthropic): body request simplified and allowed additional p…
CorieW Dec 8, 2025
b85f675
feat(testapps/anthropic): add test app for additional params
CorieW Dec 9, 2025
806bb09
fix(anthropic): sending wrong properties to Anthropic
CorieW Dec 9, 2025
f849352
feat(anthropic): support structured outputs for Claude Haiku 4.5
CorieW Dec 9, 2025
14bfcde
fix(anthropic): handling of thinking was incorrect. now removes undef…
CorieW Dec 10, 2025
5a613e4
fix(plugins/vertexai): VertexAI plugin's anthropic sdk version confli…
CorieW Dec 10, 2025
794490a
Merge branch 'add-structured-output-2' of https://github.com/invertas…
CorieW Dec 10, 2025
077d86d
fix(js/plugins/anthropic): cast in test mock
cabljac Dec 16, 2025
0830f70
Merge remote-tracking branch 'public/main' into add-structured-output-2
cabljac Dec 16, 2025
6d6af62
fix(js/plugins/anthropic): fix merge conflict in lockfile
cabljac Dec 16, 2025
ad234ef
feat(anthropic): add files api
CorieW Dec 4, 2025
888a4c2
refactor(js/testapps/anthropic): remove node-fetch and form-data dev …
cabljac Dec 16, 2025
e452ef7
refactor(js/plugins/anthropic): change file api discriminators slightly
cabljac Dec 16, 2025
7cd800a
fix(anthropic): small correction
CorieW Dec 16, 2025
5f5c6c2
chore: format
CorieW Dec 16, 2025
ad283eb
feat(js/plugins/anthropic): add support for opus 4.5
cabljac Dec 16, 2025
038b20a
Merge remote-tracking branch 'public/main' into add-structured-output-2
cabljac Dec 16, 2025
0167bbd
feat(js/plugins/anthropic): add structured response schema to opus 4.5
cabljac Dec 16, 2025
3058aca
feat(anthropic): effort param and testapp
CorieW Dec 18, 2025
667028f
feat(anthropic): support additional params
dackers86 Dec 29, 2025
22dfb34
chore(anthropic): added effort unit tests
dackers86 Dec 29, 2025
36241d8
feat(anthropic): effort param and testapp
dackers86 Dec 29, 2025
94dd878
chore(*): merged latest
dackers86 Dec 30, 2025
6635e6a
chore(*): added fixes to execution tests
dackers86 Dec 30, 2025
75e7cf4
feat(anthropic): add support for opus 4.5
dackers86 Dec 30, 2025
5e6dd22
chore(*): resolved conflicts
dackers86 Dec 30, 2025
8b9bec4
Merge branch 'add-structured-output-2' into add-structured-output-tes…
dackers86 Dec 30, 2025
1069d88
feat(anthropic): add support for the files API
dackers86 Dec 30, 2025
6ea4cf8
Merge branch 'add-structured-output-2' into add-structured-output-tes…
dackers86 Dec 30, 2025
da916c1
feat(anthropic): add testapp for structured outputs
dackers86 Dec 30, 2025
293c2c7
Update js/testapps/anthropic/src/beta/structured_output.ts
cabljac Jan 5, 2026
03486e7
Update js/testapps/anthropic/src/beta/structured_output.ts
cabljac Jan 5, 2026
f675575
Merge remote-tracking branch 'upstream/main' into add-structured-outp…
cabljac Jan 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion js/plugins/anthropic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"genkit": "workspace:^"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.68.0"
"@anthropic-ai/sdk": "^0.71.2"
},
"devDependencies": {
"@types/node": "^20.11.16",
Expand Down Expand Up @@ -64,6 +64,7 @@
"build": "npm-run-all build:clean check compile",
"build:watch": "tsup-node --watch",
"test": "tsx --test tests/*_test.ts",
"test:live": "tsx --test tests/live_test.ts",
"test:file": "tsx --test",
"test:live": "tsx --test tests/live_test.ts",
"test:coverage": "check-node-version --node '>=22' && tsx --test --experimental-test-coverage --test-coverage-include='src/**/*.ts' ./tests/**/*_test.ts"
Expand Down
69 changes: 59 additions & 10 deletions js/plugins/anthropic/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,66 @@ export const KNOWN_CLAUDE_MODELS: Record<
'claude-opus-4': commonRef('claude-opus-4', AnthropicThinkingConfigSchema),
'claude-sonnet-4-5': commonRef(
'claude-sonnet-4-5',
AnthropicThinkingConfigSchema
AnthropicThinkingConfigSchema,
{
supports: {
multiturn: true,
tools: true,
media: true,
systemRole: true,
output: ['text', 'json'],
constrained: 'all',
},
}
),
'claude-haiku-4-5': commonRef(
'claude-haiku-4-5',
AnthropicThinkingConfigSchema
),
'claude-opus-4-5': commonRef(
'claude-opus-4-5',
AnthropicThinkingConfigSchema
AnthropicThinkingConfigSchema,
{
supports: {
multiturn: true,
tools: true,
media: true,
systemRole: true,
output: ['text', 'json'],
constrained: 'all',
},
}
),
'claude-opus-4-1': commonRef(
'claude-opus-4-1',
AnthropicThinkingConfigSchema
AnthropicThinkingConfigSchema,
{
supports: {
multiturn: true,
tools: true,
media: true,
systemRole: true,
output: ['text', 'json'],
constrained: 'all',
},
}
),
'claude-opus-4-5': commonRef(
'claude-opus-4-5',
AnthropicThinkingConfigSchema.extend({
output_config: z
.object({
effort: z.enum(['low', 'medium', 'high']).optional(),
})
.passthrough()
.optional(),
}),
{
supports: {
multiturn: true,
tools: true,
media: true,
systemRole: true,
output: ['text', 'json'],
constrained: 'all',
},
}
),
};

Expand Down Expand Up @@ -232,9 +279,11 @@ export function claudeModel(
defaultApiVersion: apiVersion,
} = params;
// Use supported model ref if available, otherwise create generic model ref
const modelRef = KNOWN_CLAUDE_MODELS[name];
const modelInfo = modelRef ? modelRef.info : GENERIC_CLAUDE_MODEL_INFO;
const configSchema = modelRef?.configSchema ?? AnthropicConfigSchema;
const knownModelRef = KNOWN_CLAUDE_MODELS[name];
let modelInfo = knownModelRef
? knownModelRef.info
: GENERIC_CLAUDE_MODEL_INFO;
const configSchema = knownModelRef?.configSchema ?? AnthropicConfigSchema;

return model<
AnthropicBaseConfigSchemaType | AnthropicThinkingConfigSchemaType
Expand Down
230 changes: 160 additions & 70 deletions js/plugins/anthropic/src/runner/beta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { logger } from 'genkit/logging';

import { KNOWN_CLAUDE_MODELS, extractVersion } from '../models.js';
import { AnthropicConfigSchema, type ClaudeRunnerParams } from '../types.js';
import { removeUndefinedProperties } from '../utils.js';
import { BaseRunner } from './base.js';
import { RunnerTypes } from './types.js';

Expand All @@ -66,6 +67,57 @@ const BETA_UNSUPPORTED_SERVER_TOOL_BLOCK_TYPES = new Set<string>([
'container_upload',
]);

const BETA_APIS = [
// 'message-batches-2024-09-24',
// 'prompt-caching-2024-07-31',
// 'computer-use-2025-01-24',
// 'pdfs-2024-09-25',
// 'token-counting-2024-11-01',
// 'token-efficient-tools-2025-02-19',
// 'output-128k-2025-02-19',
'files-api-2025-04-14',
// 'mcp-client-2025-04-04',
// 'dev-full-thinking-2025-05-14',
// 'interleaved-thinking-2025-05-14',
// 'code-execution-2025-05-22',
// 'extended-cache-ttl-2025-04-11',
// 'context-1m-2025-08-07',
// 'context-management-2025-06-27',
// 'model-context-window-exceeded-2025-08-26',
// 'skills-2025-10-02',
'effort-2025-11-24',
// 'advanced-tool-use-2025-11-20',
'structured-outputs-2025-11-13',
];

/**
* Transforms a JSON schema to be compatible with Anthropic's structured output requirements.
* Anthropic requires `additionalProperties: false` on all object types.
* @see https://docs.anthropic.com/en/docs/build-with-claude/structured-outputs#json-schema-limitations
*/
function toAnthropicSchema(
schema: Record<string, unknown>
): Record<string, unknown> {
const out = structuredClone(schema);

// Remove $schema if present
delete out.$schema;

// Add additionalProperties: false to objects
if (out.type === 'object') {
out.additionalProperties = false;
}

// Recursively process nested objects
for (const key in out) {
if (typeof out[key] === 'object' && out[key] !== null) {
out[key] = toAnthropicSchema(out[key] as Record<string, unknown>);
}
}

return out;
}

const unsupportedServerToolError = (blockType: string): string =>
`Anthropic beta runner does not yet support server-managed tool block '${blockType}'. Please retry against the stable API or wait for dedicated support.`;

Expand Down Expand Up @@ -140,6 +192,26 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {

// Media
if (part.media) {
if (part.media.contentType === 'anthropic/file') {
return {
type: 'document',
source: {
type: 'file',
file_id: part.media.url,
},
};
}

if (part.media.contentType === 'anthropic/image') {
return {
type: 'image',
source: {
type: 'file',
file_id: part.media.url,
},
};
}

if (part.media.contentType === 'application/pdf') {
return {
type: 'document',
Expand Down Expand Up @@ -249,45 +321,49 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
: system;
}

const body: BetaMessageCreateParamsNonStreaming = {
const thinkingConfig = this.toAnthropicThinkingConfig(
request.config?.thinking
) as BetaMessageCreateParams['thinking'] | undefined;

// Need to extract topP and topK from request.config to avoid duplicate properties being added to the body
// This happens because topP and topK have different property names (top_p and top_k) in the Anthropic API.
// Thinking is extracted separately to avoid type issues.
// ApiVersion is extracted separately as it's not a valid property for the Anthropic API.
const {
topP,
topK,
apiVersion: _1,
thinking: _2,
...restConfig
} = request.config ?? {};

const body = {
model: mappedModelName,
max_tokens:
request.config?.maxOutputTokens ?? this.DEFAULT_MAX_OUTPUT_TOKENS,
messages,
};

if (betaSystem !== undefined) body.system = betaSystem;
if (request.config?.stopSequences !== undefined)
body.stop_sequences = request.config.stopSequences;
if (request.config?.temperature !== undefined)
body.temperature = request.config.temperature;
if (request.config?.topK !== undefined) body.top_k = request.config.topK;
if (request.config?.topP !== undefined) body.top_p = request.config.topP;
if (request.config?.tool_choice !== undefined) {
body.tool_choice = request.config
.tool_choice as BetaMessageCreateParams['tool_choice'];
}
if (request.config?.metadata !== undefined) {
body.metadata = request.config
.metadata as BetaMessageCreateParams['metadata'];
}
if (request.tools) {
body.tools = request.tools.map((tool) => this.toAnthropicTool(tool));
}
const thinkingConfig = this.toAnthropicThinkingConfig(
request.config?.thinking
);
if (thinkingConfig) {
body.thinking = thinkingConfig as BetaMessageCreateParams['thinking'];
}

if (request.output?.format && request.output.format !== 'text') {
throw new Error(
`Only text output format is supported for Claude models currently`
);
}

return body;
system: betaSystem,
stop_sequences: request.config?.stopSequences,
temperature: request.config?.temperature,
top_k: topK,
top_p: topP,
tool_choice: request.config?.tool_choice,
metadata: request.config?.metadata,
tools: request.tools?.map((tool) => this.toAnthropicTool(tool)),
thinking: thinkingConfig,
output_format: this.isStructuredOutputEnabled(request)
? {
type: 'json_schema',
schema: toAnthropicSchema(request.output!.schema!),
}
: undefined,
betas: Array.isArray(request.config?.betas)
? [...(request.config?.betas ?? [])]
: [...BETA_APIS],
...restConfig,
} as BetaMessageCreateParamsNonStreaming;

return removeUndefinedProperties(body);
}

/**
Expand Down Expand Up @@ -316,46 +392,50 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
]
: system;

const body: BetaMessageCreateParamsStreaming = {
const thinkingConfig = this.toAnthropicThinkingConfig(
request.config?.thinking
) as BetaMessageCreateParams['thinking'] | undefined;

// Need to extract topP and topK from request.config to avoid duplicate properties being added to the body
// This happens because topP and topK have different property names (top_p and top_k) in the Anthropic API.
// Thinking is extracted separately to avoid type issues.
// ApiVersion is extracted separately as it's not a valid property for the Anthropic API.
const {
topP,
topK,
apiVersion: _1,
thinking: _2,
...restConfig
} = request.config ?? {};

const body = {
model: mappedModelName,
max_tokens:
request.config?.maxOutputTokens ?? this.DEFAULT_MAX_OUTPUT_TOKENS,
messages,
stream: true,
};

if (betaSystem !== undefined) body.system = betaSystem;
if (request.config?.stopSequences !== undefined)
body.stop_sequences = request.config.stopSequences;
if (request.config?.temperature !== undefined)
body.temperature = request.config.temperature;
if (request.config?.topK !== undefined) body.top_k = request.config.topK;
if (request.config?.topP !== undefined) body.top_p = request.config.topP;
if (request.config?.tool_choice !== undefined) {
body.tool_choice = request.config
.tool_choice as BetaMessageCreateParams['tool_choice'];
}
if (request.config?.metadata !== undefined) {
body.metadata = request.config
.metadata as BetaMessageCreateParams['metadata'];
}
if (request.tools) {
body.tools = request.tools.map((tool) => this.toAnthropicTool(tool));
}
const thinkingConfig = this.toAnthropicThinkingConfig(
request.config?.thinking
);
if (thinkingConfig) {
body.thinking = thinkingConfig as BetaMessageCreateParams['thinking'];
}

if (request.output?.format && request.output.format !== 'text') {
throw new Error(
`Only text output format is supported for Claude models currently`
);
}

return body;
system: betaSystem,
stop_sequences: request.config?.stopSequences,
temperature: request.config?.temperature,
top_k: topK,
top_p: topP,
tool_choice: request.config?.tool_choice,
metadata: request.config?.metadata,
tools: request.tools?.map((tool) => this.toAnthropicTool(tool)),
thinking: thinkingConfig,
output_format: this.isStructuredOutputEnabled(request)
? {
type: 'json_schema',
schema: toAnthropicSchema(request.output!.schema!),
}
: undefined,
betas: Array.isArray(request.config?.betas)
? [...(request.config?.betas ?? [])]
: [...BETA_APIS],
...restConfig,
} as BetaMessageCreateParamsStreaming;

return removeUndefinedProperties(body);
}

protected toGenkitResponse(message: BetaMessage): GenerateResponseData {
Expand Down Expand Up @@ -491,4 +571,14 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
return 'other';
}
}

private isStructuredOutputEnabled(
request: GenerateRequest<typeof AnthropicConfigSchema>
): boolean {
return !!(
request.output?.schema &&
request.output.constrained &&
request.output.format === 'json'
);
}
}
Loading