Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
5 changes: 4 additions & 1 deletion js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
},
"pnpm": {
"overrides": {
"cross-spawn": "^7.0.5"
"cross-spawn": "^7.0.5",
"@genkit-ai/vertexai>@anthropic-ai/sdk": "^0.24.3",
"@anthropic-ai/vertex-sdk>@anthropic-ai/sdk": "^0.24.3",
"@genkit-ai/anthropic>@anthropic-ai/sdk": "^0.71.2"
}
},
"packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b"
Expand Down
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:coverage": "check-node-version --node '>=22' && tsx --test --experimental-test-coverage --test-coverage-include='src/**/*.ts' ./tests/**/*_test.ts"
}
Expand Down
44 changes: 38 additions & 6 deletions js/plugins/anthropic/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,45 @@ 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
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',
},
}
),
};

Expand Down Expand Up @@ -216,9 +246,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
83 changes: 74 additions & 9 deletions js/plugins/anthropic/src/runner/beta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,56 @@ 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-param-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.
*/
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 @@ -254,6 +304,7 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
max_tokens:
request.config?.maxOutputTokens ?? this.DEFAULT_MAX_OUTPUT_TOKENS,
messages,
betas: BETA_APIS,
};

if (betaSystem !== undefined) body.system = betaSystem;
Expand Down Expand Up @@ -281,10 +332,12 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
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`
);
// Apply structured output when model supports it and constrained output is requested
if (this.isStructuredOutputEnabled(request)) {
body.output_format = {
type: 'json_schema',
schema: toAnthropicSchema(request.output!.schema!),
};
}

return body;
Expand Down Expand Up @@ -322,6 +375,7 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
request.config?.maxOutputTokens ?? this.DEFAULT_MAX_OUTPUT_TOKENS,
messages,
stream: true,
betas: BETA_APIS,
};

if (betaSystem !== undefined) body.system = betaSystem;
Expand Down Expand Up @@ -349,12 +403,13 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
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`
);
// Apply structured output when model supports it and constrained output is requested
if (this.isStructuredOutputEnabled(request)) {
body.output_format = {
type: 'json_schema',
schema: toAnthropicSchema(request.output!.schema!),
};
}

return body;
}

Expand Down Expand Up @@ -491,4 +546,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'
);
}
}
70 changes: 70 additions & 0 deletions js/plugins/anthropic/tests/live_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as assert from 'assert';
import { genkit, z } from 'genkit';
import { describe, it } from 'node:test';
import { anthropic } from '../src/index.js';

const SKIP_LIVE_TESTS = !process.env.ANTHROPIC_API_KEY;

describe('Anthropic Live Tests', { skip: SKIP_LIVE_TESTS }, () => {
it('should return structured output matching the schema', async () => {
const ai = genkit({
plugins: [anthropic({ apiVersion: 'beta' })],
});

const schema = z.object({
name: z.string(),
age: z.number(),
city: z.string(),
isStudent: z.boolean(),
isEmployee: z.boolean(),
isRetired: z.boolean(),
isUnemployed: z.boolean(),
isDisabled: z.boolean(),
});

const result = await ai.generate({
model: 'anthropic/claude-sonnet-4-5',
prompt:
'Generate a fictional person with name "Alice", age 30, and city "New York". Return only the JSON.',
output: { schema, format: 'json', constrained: true },
});

const parsed = result.output;
assert.ok(parsed, 'Should have parsed output');
assert.deepStrictEqual(
{ name: parsed.name, age: parsed.age, city: parsed.city },
{ name: 'Alice', age: 30, city: 'New York' }
);

// Check that boolean fields are present and are actually booleans
for (const key of [
'isStudent',
'isEmployee',
'isRetired',
'isUnemployed',
'isDisabled',
]) {
assert.strictEqual(
typeof parsed[key],
'boolean',
`Field ${key} should be a boolean but got: ${typeof parsed[key]}`
);
}
});
});
1 change: 1 addition & 0 deletions js/plugins/anthropic/tests/mocks/anthropic-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ export function mockMessageWithContent(
}

function toBetaMessage(message: Message): BetaMessage {
// @ts-ignore
return {
...message,
container: null,
Expand Down
Loading