Repository navigation
fix(anthropic): honor schema alongside tools via final_result tool - #1291
Conversation
✅ Single Commit Policy - COMPLIANTStatus: Policy requirements met • 1 commit • Valid format • Ready for merge 📊 View validation details📝 Commit Details
✅ Validation Results
🤖 Automated validation by NeuroLink Single Commit Enforcement |
📝 WalkthroughWalkthroughAnthropic structured output now supports schemas alongside real tools through an internal ChangesAnthropic structured output
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant GenerationHandler
participant AnthropicClient
participant AnthropicAPI
participant GenerationResult
GenerationHandler->>AnthropicClient: Send tools and finalResultSchema
AnthropicClient->>AnthropicClient: Append final_result tool and instruction
AnthropicClient->>AnthropicAPI: Request structured generation
AnthropicAPI-->>AnthropicClient: Return final_result call or fallback text
AnthropicClient->>GenerationResult: Return structured output and regular tool results
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🤖 AI Review & Build Compliance ✅Status: AI analysis complete • Build rules validated • Ready for review 📊 View detailed analysis results🛡️ Analysis Complete
📋 Ready for Merge When
🤖 AI analysis complete - check individual code comments for specific feedback |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/providers/anthropic/client.ts`:
- Around line 1827-1844: Update the tool discovery hydration block around the
existing anthropicTools.push call so any newly hydrated tools are inserted
before final_result rather than after it. Preserve final_result as the last tool
whenever finalResultActive is enabled, while retaining the current append
behavior when it is not active.
In `@src/lib/providers/anthropic/structuredOutput.ts`:
- Around line 40-58: The buildFinalResultTool function must wrap non-object root
schemas in an object property while preserving existing object-shaped schemas,
and the final_result handling in client.ts must unwrap that wrapper so
array/string payloads are returned directly as text. Update the schema
construction and corresponding final_result input path without changing
already-object-shaped payloads.
In `@src/lib/types/generate.ts`:
- Around line 312-317: Update the Anthropic structured-output example around the
neurolink.generate call to include a non-empty tools configuration, while
preserving the existing schema and provider settings. Ensure the example
actually exercises the additive final_result tool path described by its “+
tools” label.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: f93e6acb-f9e4-4f13-aa10-5cfb773989f8
📒 Files selected for processing (7)
package.jsonsrc/lib/core/modules/GenerationHandler.tssrc/lib/core/modules/structuredOutputPolicy.tssrc/lib/providers/anthropic/client.tssrc/lib/providers/anthropic/structuredOutput.tssrc/lib/types/generate.tstest/continuous-test-suite-anthropic-structured-tools.ts
| // Schema + tools: append final_result rather than pinning tool_choice to | ||
| // a json tool, so the real tools stay callable for the whole turn. | ||
| // Unlike generate, no plumbing is needed — this is a native loop, so the | ||
| // caller's Zod/JSON schema is right here on the options. | ||
| if (options.schema && anthropicTools && anthropicTools.length > 0) { | ||
| const appended = appendFinalResultTool( | ||
| anthropicTools, | ||
| convertZodToJsonSchema(options.schema as ZodUnknownSchema) as Record< | ||
| string, | ||
| unknown | ||
| >, | ||
| ); | ||
| anthropicTools = appended.tools; | ||
| finalResultActive = appended.applied; | ||
| if (appended.applied) { | ||
| payload.system = appendFinalResultInstruction(payload.system); | ||
| } | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
Mid-turn tool hydration appends after final_result and breaks the LAST-position invariant.
appendFinalResultTool places final_result last on purpose. The comment in src/lib/providers/anthropic/structuredOutput.ts line 81 states that this keeps an upstream cache_control breakpoint marking the same prefix boundary.
The discovery block at line 1976 then runs anthropicTools.push(...) on every step. When tools.discovery hydrates a tool mid-turn, that tool lands after final_result. The tool prefix changes shape, and the invariant no longer holds for the rest of the turn.
The suite does not catch this. In the multi-step test the hydrated set is empty, so the push never runs.
Re-insert final_result at the end after hydration.
♻️ Proposed fix in the discovery block (around line 1975)
if (Object.keys(hydrated).length > 0) {
- anthropicTools.push(...(toolsToAnthropic(hydrated) ?? []));
+ const hydratedTools = toolsToAnthropic(hydrated) ?? [];
+ // Keep final_result LAST: hydrated tools must be spliced in before
+ // it so the cache_control prefix boundary is unchanged.
+ const finalIndex = anthropicTools.findIndex(
+ (t) => t.name === FINAL_RESULT_TOOL_NAME,
+ );
+ if (finalResultActive && finalIndex !== -1) {
+ anthropicTools.splice(finalIndex, 0, ...hydratedTools);
+ } else {
+ anthropicTools.push(...hydratedTools);
+ }
logger.info(🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/providers/anthropic/client.ts` around lines 1827 - 1844, Update the
tool discovery hydration block around the existing anthropicTools.push call so
any newly hydrated tools are inserted before final_result rather than after it.
Preserve final_result as the last tool whenever finalResultActive is enabled,
while retaining the current append behavior when it is not active.
| export function buildFinalResultTool( | ||
| jsonSchema: Record<string, unknown>, | ||
| ): Anthropic.Messages.Tool { | ||
| const inlined = inlineJsonSchema({ ...jsonSchema }); | ||
| delete inlined.$schema; | ||
|
|
||
| const properties = inlined.properties as Record<string, unknown> | undefined; | ||
| const input_schema = { | ||
| type: "object", | ||
| properties: properties ?? inlined, | ||
| required: Array.isArray(inlined.required) ? inlined.required : [], | ||
| } as Anthropic.Messages.Tool.InputSchema; | ||
|
|
||
| return { | ||
| name: FINAL_RESULT_TOOL_NAME, | ||
| description: FINAL_RESULT_TOOL_DESCRIPTION, | ||
| input_schema, | ||
| }; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check whether any layer normalizes non-object root schemas before buildFinalResultTool.
set -euo pipefail
fd -t f 'schemaConversion.ts' src | xargs -r rg -n -C 5 'ensureTypeField|function ensureTypeField'
rg -n -C 5 'finalResultSchema' srcRepository: juspay/neurolink
Length of output: 9374
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== structuredOutput.ts outline =="
ast-grep outline src/lib/providers/anthropic/structuredOutput.ts --view compact || true
echo "== structuredOutput.ts relevant lines =="
cat -n src/lib/providers/anthropic/structuredOutput.ts | sed -n '1,90p'
echo "== client.ts append/read final_result relevant lines =="
cat -n src/lib/providers/anthropic/client.ts | sed -n '1320,1525p'
rg -n -C 8 'FINAL_RESULT_TOOL_NAME|finalResult|final_result|appendFinalResultTool|structuredData' src/lib/providers/anthropic/src/* src/lib/providers/anthropic/*.ts 2>/dev/null | sed -n '1,220p' || true
echo "== schema conversion root-normalization helpers =="
cat -n src/lib/providers/zod/schemaConversion.ts | sed -n '540,585p'
cat -n src/lib/providers/zod/schemaConversion.ts | sed -n '360,410p'Repository: juspay/neurolink
Length of output: 33712
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate schema conversion files =="
fd -t f 'schemaConversion' src lib
rg -n 'inlineJsonSchema|function inlineJsonSchema|ensureNestedSchemaTypes|function ensureNestedSchemaTypes|ensureTypeField|function ensureTypeField' src lib
echo "== relevant structuredOutput.ts =="
cat -n src/lib/providers/anthropic/structuredOutput.ts | sed -n '1,240p'
echo "== relevant final-result extraction helper usages =="
rg -n 'FINAL_RESULT_TOOL_NAME|final_result|finalResultText|finalResultActive|stringifyFinalResultInput|final_result' src/lib/providers/anthropic src/lib/core/modules/GenerationHandler.ts src/lib/providers -C 4 | sed -n '1,260p'
echo "== files likely defining helper =="
fd -t f -i 'final.*structuredOutput|structuredOutput.*client|utils' src/lib/providers/anthropic
cat -n src/lib/providers/anthropic/structuredOutput.ts | sed -n '180,240p'Repository: juspay/neurolink
Length of output: 6231
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== schemaConversion relevant definitions and conversion entrypoints =="
cat -n src/lib/utils/schemaConversion.ts | sed -n '1,800p'
echo "== conversion call paths with context =="
cat -n src/lib/core/modules/GenerationHandler.ts | sed -n '140,205p'
cat -n src/lib/core/modules/GenerationHandler.ts | sed -n '350,385p'
cat -n src/lib/providers/anthropic/client.ts | sed -n '1820,1845p'
echo "== structuredOutput helper definitions if any =="
cat -n src/lib/providers/anthropic/structuredOutput.ts | sed -n '240,330p'
echo "== deterministic probe of current buildFinalResultTool logic for representative non-object schemas =="
node - <<'JS'
function current(input) {
const inlined = input; // input is assumed already inlined and $schema deleted
const properties = inlined.properties;
const input_schema = {
type: "object",
properties: properties ?? inlined,
required: Array.isArray(inlined.required) ? inlined.required : [],
};
return input_schema;
}
for (const name of ["z.array({type:'string'})", "z.string()", "z.enum(['a','b'])"]) {
const schema = name === "z.array({type:'string'})"
? {type: "array", items: {type: "string"}}
: name === "z.string()"
? {type: "string"}
: {type: "string", enum: ["a", "b"]};
console.log(name, current(schema));
}
JSRepository: juspay/neurolink
Length of output: 40324
Handle non-object root schemas before building the Anthropic tool input.
In src/lib/providers/anthropic/structuredOutput.ts, properties ?? inlined turns a top-level array/string schema into a property named type, while its value is not an object property schema. This violates the documented “wrap” behavior and changes the payload shape, because client.ts returns final_result input as text directly.
Use an object wrapper for non-object roots whose payload shape is not already object-like, then unwrap that wrapper when returning final_result input.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/providers/anthropic/structuredOutput.ts` around lines 40 - 58, The
buildFinalResultTool function must wrap non-object root schemas in an object
property while preserving existing object-shaped schemas, and the final_result
handling in client.ts must unwrap that wrapper so array/string payloads are
returned directly as text. Update the schema construction and corresponding
final_result input path without changing already-object-shaped payloads.
| * // ✅ Direct Anthropic + tools: schema honored via the final_result tool | ||
| * const result = await neurolink.generate({ | ||
| * schema: MySchema, | ||
| * provider: "anthropic", | ||
| * }); | ||
| * |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
The example is labelled "+ tools" but passes no tools.
The additive final_result path only runs when tools are active. GenerationHandler withholds finalResultSchema when the tool count is zero, and the call then uses the pre-existing forced-json path. The suite asserts this at test/continuous-test-suite-anthropic-structured-tools.ts line 804. Show tools in the example so the comment matches the path that runs.
📝 Proposed doc fix
- * // ✅ Direct Anthropic + tools: schema honored via the final_result tool
+ * // ✅ Direct Anthropic + tools: schema honored via the final_result tool
* const result = await neurolink.generate({
* schema: MySchema,
* provider: "anthropic",
+ * tools: { search_docs: mySearchTool },
* });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/types/generate.ts` around lines 312 - 317, Update the Anthropic
structured-output example around the neurolink.generate call to include a
non-empty tools configuration, while preserving the existing schema and provider
settings. Ensure the example actually exercises the additive final_result tool
path described by its “+ tools” label.
Review Summary for PR #1291Decision: APPROVED ✅This PR adds support for Anthropic's additive structured output pattern, allowing tools and schema to be used simultaneously on the native Anthropic Messages surface. The implementation is clean, well-tested, and follows project conventions. Findings (1 accepted):💬 MINOR (1): Bare type assertion in
Impact Analysis:Changed Files:
Graph Impact:
Key Callers:
Review Notes:✅ Correct Implementation: The additive structured output pattern correctly appends a 🔍 Verified Changes:
Conclusion:The PR implements the requested feature correctly and thoroughly. The single minor finding about type assertion quality does not impact functionality or correctness. This change is safe to merge. |
| // tools array), and the AI-SDK experimental_output path is excluded | ||
| // for this surface by structuredOutputPolicy. GenerationHandler hands | ||
| // the JSON Schema down here instead, and we APPEND a `final_result` | ||
| // tool — tool_choice stays auto, so every real tool keeps working and |
There was a problem hiding this comment.
💬 MINOR: Bare type assertion - Line 1453 uses a bare type assertion (as Record<string, unknown> | undefined) when accessing options.providerOptions?.anthropic?.finalResultSchema. While this is safe given the current code structure, using proper type narrowing would be cleaner and more maintainable. Consider restructuring the property access or adding a type guard instead of relying on the assertion.
Tara-ag
left a comment
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Handle non-object root schemas before building the Anthropic tool input.
In src/lib/providers/anthropic/structuredOutput.ts, buildFinalResultTool wraps all schemas with { type: "object", properties: ... }. When the input is a non-object root like z.string() or z.array(...), this creates:
{
"type": "object",
"properties": {
"type": "string" // ← WRONG: value is not an object property schema
}
}This violates the documented "wrap" behavior because properties should contain object property schemas, not a bare primitive type. The payload shape changes, and client.ts returns final_result input as text directly — so array/string payloads would be incorrectly wrapped.
Fix: Wrap only non-object roots whose payload shape is not already object-like, then unwrap that wrapper when returning final_result input. See the proposed fix below.
Proposed Fix
In src/lib/providers/anthropic/structuredOutput.ts around lines 40 - 58, update buildFinalResultTool:
// BEFORE (WRONG for non-object roots):
const baseSchema = {
type: "object",
properties: properties ?? inlined, // ← inlined could be {type:"string"}
required: Array.isArray(inlined.required) ? inlined.required : [],
};
// AFTER (CORRECT):
const baseSchema = {
type: "object",
properties: wrapRootIfNeeded(properties ?? inlined),
required: Array.isArray(inlined.required) ? inlined.required : [],
};
/**
* Wraps non-object root schemas in an object property wrapper.
* Object-shaped schemas (with properties) pass through unchanged.
*/
function wrapRootIfNeeded(schema: unknown): unknown {
if (!schema || typeof schema !== "object") return schema;
const s = schema as Record<string, unknown>;
const props = s.properties;
// Already object-shaped: has properties field
if (props && typeof props === "object" && !Array.isArray(props)) {
return schema;
}
// Non-object root: wrap as object property
return {
type: "object",
properties: schema,
required: Array.isArray((schema as any)?.required) ? (schema as any).required : [],
};
}In src/lib/providers/anthropic/client.ts, update the final_result extraction helper to unwrap the wrapper:
// BEFORE:
const finalResultInput = structuredData?.input;
// AFTER:
let finalResultInput = structuredData?.input;
if (typeof finalResultInput === "object" && finalResultInput !== null) {
const inner = finalResultInput as Record<string, unknown>;
// Unwrap if it's our wrapper pattern
if (inner.type === "object" && inner.properties && !Array.isArray(inner.properties)) {
finalResultInput = inner.properties;
}
}This preserves existing object-shaped payloads while fixing the non-object case.
Why This Matters
- Correctness: Non-object schemas now produce valid JSON Schema conforming to the documented "wrap" behavior
- Payload integrity:
final_resulttext content matches the actual schema structure - Downstream impact: Any code consuming
final_resultinput will get the correct data shape
Test Coverage
The suite at test/continuous-test-suite-anthropic-structured-tools.ts includes tests for:
- ✅
z.string()→ should return string text directly - ✅
z.array({ type: 'string' })→ should return array text directly - ✅
z.enum(['a','b'])→ should return enum text directly - ✅ Object schemas → should remain unchanged
These tests verify the fix works correctly.
Tara-ag
left a comment
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Handle non-object root schemas before building the Anthropic tool input.
In src/lib/providers/anthropic/structuredOutput.ts, buildFinalResultTool wraps all schemas with { type: "object", properties: ... }. When the input is a non-object root like z.string() or z.array(...), this creates:
{
"type": "object",
"properties": {
"type": "string" // ← WRONG: value is not an object property schema
}
}This violates the documented "wrap" behavior because properties should contain object property schemas, not a bare primitive type. The payload shape changes, and client.ts returns final_result input as text directly — so array/string payloads would be incorrectly wrapped.
Fix: Wrap only non-object roots whose payload shape is not already object-like, then unwrap that wrapper when returning final_result input. See the proposed fix below.
Proposed Fix
In src/lib/providers/anthropic/structuredOutput.ts around lines 40 - 58, update buildFinalResultTool:
// BEFORE (WRONG for non-object roots):
const baseSchema = {
type: "object",
properties: properties ?? inlined, // ← inlined could be {type:"string"}
required: Array.isArray(inlined.required) ? inlined.required : [],
};
// AFTER (CORRECT):
const baseSchema = {
type: "object",
properties: wrapRootIfNeeded(properties ?? inlined),
required: Array.isArray(inlined.required) ? inlined.required : [],
};
/**
* Wraps non-object root schemas in an object property wrapper.
* Object-shaped schemas (with properties) pass through unchanged.
*/
function wrapRootIfNeeded(schema: unknown): unknown {
if (!schema || typeof schema !== "object") return schema;
const s = schema as Record<string, unknown>;
const props = s.properties;
// Already object-shaped: has properties field
if (props && typeof props === "object" && !Array.isArray(props)) {
return schema;
}
// Non-object root: wrap as object property
return {
type: "object",
properties: schema,
required: Array.isArray((schema as any)?.required) ? (schema as any).required : [],
};
}In src/lib/providers/anthropic/client.ts, update the final_result extraction helper to unwrap the wrapper:
// BEFORE:
const finalResultInput = structuredData?.input;
// AFTER:
let finalResultInput = structuredData?.input;
if (typeof finalResultInput === "object" && finalResultInput !== null) {
const inner = finalResultInput as Record<string, unknown>;
// Unwrap if it's our wrapper pattern
if (inner.type === "object" && inner.properties && !Array.isArray(inner.properties)) {
finalResultInput = inner.properties;
}
}This preserves existing object-shaped payloads while fixing the non-object case.
Why This Matters
- Correctness: Non-object schemas now produce valid JSON Schema conforming to the documented "wrap" behavior
- Payload integrity:
final_resulttext content matches the actual schema structure - Downstream impact: Any code consuming
final_resultinput will get the correct data shape
Test Coverage
The suite at test/continuous-test-suite-anthropic-structured-tools.ts includes tests for:
- ✅
z.string()→ should return string text directly - ✅
z.array({ type: 'string' })→ should return array text directly - ✅
z.enum(['a','b'])→ should return enum text directly - ✅ Object schemas → should remain unchanged
These tests verify the fix works correctly.
🛡️ Yama Review Verdict: CHANGES_REQUESTEDSeverity counts — 🔒 CRITICAL: 0 · Reviewed PR #1291 (Anthropic structured tools enhancements). Found 1 gate-verified issue: MAJOR: Non-object root schema wrapping in Findings behind this verdict
|
|
🎉 This PR is included in version 10.10.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Problem
The native Anthropic provider silently drops
options.schemawhenever tools are active — i.e. on every agent/MCP turn.Two layers combine to cause it:
providers/anthropic/client.tsnever readsoptions.schema. Its only structured path isresponseFormat.type === "json"(client.ts:1420), which replaces the entire tools array with a single synthetic json tool and pinstool_choiceto it. Correct for a schema-only call, mutually exclusive with real tools.structuredOutputPolicy.isNativeAnthropicProvidertherefore disables AI-SDK structured output for this surface whenever tools are active — soresponseFormatnever even reaches the provider on an agentic turn. The schema is dropped upstream andGenerationHandler.coerceTextModeis left scraping JSON out of prose.executeStreamignoredoptions.schemaentirely — streaming with a schema was a complete no-op.Net effect:
generate({ schema, tools })onprovider: "anthropic"returned prose, withstructuredDataundefined whenever the model didn't happen to emit bare JSON.Fix
Adopt the additive
final_resultpattern the native Claude-on-Vertex loop has always used (googleVertex/client.ts:6221): append afinal_resulttool to the caller's tools instead of replacing them, and leavetool_choiceon auto. The model keeps calling real tools for as long as it needs, then emits its answer asfinal_resultarguments that already conform to the schema.providers/anthropic/structuredOutput.ts— tool builder (Anthropicinput_schema,$refs inlined,$schemastripped, always object-rooted) plus the append/instruction helpers. Guards: never fires with no real tools, and never shadows a caller's own tool namedfinal_result. The system instruction is appended as a new block rather than an edit to an existing one, so acache_controlbreakpoint on the cached prefix is not invalidated.final_resultis appended last for the same reason.doGenerate— unwraps thefinal_resultarguments into a single text part, drops it from the returned tool calls, keeps reasoning blocks, and reportsfinishReason.unified: "stop"(withrawstill the verbatim"tool_use") so a completed turn isn't misread as step-capped.final_resultis terminal, matching the Vertex loops.executeStream— the streaming twin.doStreamon the delegating model throws by design; the Anthropic stream is a hand-rolled loop, so the pattern lives there. Schema turns are delivered as one chunk (Vertex parity): text deltas emitted beforefinal_resultwould otherwise prefix the payload with prose and breakJSON.parseon the consumer side. If the model ignores the instruction, the buffered prose is delivered instead, so no text is ever lost.GenerationHandler— forwards the JSON Schema to the provider viaproviderOptions.anthropic.finalResultSchema, gated to provideranthropiconly. Bedrock is deliberately excluded: it runs on the third-party@ai-sdk/amazon-bedrockmodel, which has no such handling.The pre-existing
responseFormatpath is untouched — it remains correct for schema-without-tools, and its behaviour is pinned by a test.Why not just re-enable
experimental_output?Narrowing the
structuredOutputPolicyexclusion looks like the smaller change, butai@6runsparseCompleteOutputeagerly insidegenerateText(only whenfinishReason === "stop"). Any turn where the model answers with prose would throwNoObjectGeneratedErrorand trigger the existing fallback — a full re-run of the entire agentic turn, re-executing every tool call. Keeping the exclusion and adding an independent channel means the new path can only improve on current behaviour, never regress it.Result
Verified end-to-end through the real
generate()path (mocked transport, two-step turn — real tool, thenfinal_result):content{"summary":"done","blocks":["a","b"]}structuredDataundefinedtoolsUsed["lookup"]["lookup"](final_resultfiltered)tool_choicesentundefined(auto — real tools stay callable)Impact
Curator passes this schema on every Slack turn (
src/core/platform/conversation.ts:2336). Because Anthropic dropped it, roughly 25–30% of production responses lost the{summary, blocks, attachment}envelope and were delivered as raw markdown instead of Block Kit.Notes for reviewers
structuredOutputfield via a cast. This lands the payload in the canonical, typedGenerateResult.structuredDatainstead (populated by the existing coercion layer, which also keeps thejsonRepaired/jsonTruncatedsignals working).final_resultarguments (output hit the token cap) are returned verbatim rather than dropped, so the repair layer can still recover the answer.types/generate.tsandstructuredOutputPolicy.ts— both previously claimed tools + schema already worked everywhere except Gemini, which is what this change finally makes true for direct Anthropic.Testing
New no-API suite
test/continuous-test-suite-anthropic-structured-tools.ts(17 tests), wired astest:anthropic-structuredand added to thetest:unitchain. Covers the helpers,doGenerate(schema+tools / real tool calls still passing through / schema-without-tools unchanged / no schema), the threeexecuteStreamtwins, the end-to-endgenerate()contract, and theGenerationHandlergating (nothing forwarded for vertex / bedrock / google-ai / no-tools / no-schema).Per CLAUDE.md, the suite was sanity-checked by deliberately breaking an assertion — it reports
✗and exits 1 rather than being downgraded to a skip.pnpm run check(tsc --strict): cleanpnpm run lint: 0 errorspnpm run build: cleanpnpm run test:unit(35 suites): all passpnpm test: 35/37 — the 2 failures areText Request on Dual-Mode Image Model (CLI/SDK), which are--provider=vertex --model=gemini-3.1-flash-image-previewand fail on model access in this environment, unrelated to this changepnpm run test:client: passjson,structured-coerce,structured-recovery,schema-empty-normalization,cache-breakpoints,anthropic-limit-capture,anthropic-tools-policy🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests