Skip to content

Conversation

@Leftwitch
Copy link

@Leftwitch Leftwitch commented Feb 10, 2026

🎯 Changes

The changes are mostly made because we have structured outputs offered by anthropic, so we don't have to deal with malformed json thus zod having validation errors.

The intention if this PR is to get rid off the tool-call-as-response hack and use real structured output since it's also not a beta feature anymore.

For it to be out of beta, we also need to bump the SDK version and the things affected (mostly types)

✅ Checklist

  • I have followed the steps in the Contributing guide.
    (The file seems missing, but I did everything I could think of)

  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Dependencies

    • Updated Anthropic SDK to v0.74.0.
  • Improvements

    • Switched from beta messaging/streaming APIs to GA messaging/streaming APIs.
    • Added native structured-output via JSON Schema for Claude 4+ models, with tool-based fallback for older models.
    • Aligned public tool/type surface to GA message resource types.
  • Note

    • Type names and import paths for some tool-choice and streaming types changed; update TypeScript imports accordingly.

@Leftwitch Leftwitch requested a review from a team February 10, 2026 00:00
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Migrates Anthropic adapter to Anthropic’s GA structured output via output_config.format.json_schema, switches from client.beta.messages to client.messages, updates SDK types/imports to GA equivalents, marks Claude 4+ models as structured-output capable, bumps @anthropic-ai/sdk to ^0.74.0, and updates tests for native and fallback flows.

Changes

Cohort / File(s) Summary
Release & Dependency
\.changeset/real-structured-output.md, packages/typescript/ai-anthropic/package.json
Adds changeset and upgrades @anthropic-ai/sdk to ^0.74.0.
Adapter & Streaming
packages/typescript/ai-anthropic/src/adapters/text.ts
Replaces client.beta.messages with client.messages; adds branching: nativeStructuredOutput using output_config.format.json_schema for Claude 4+ models and toolBasedStructuredOutput fallback for older models; updates streaming event typing to RawMessageStreamEvent.
Model Metadata
packages/typescript/ai-anthropic/src/model-meta.ts
Adds structured_output?: boolean to ModelMeta.supports, annotates models, and exports ANTHROPIC_STRUCTURED_OUTPUT_MODELS set to select native vs fallback flows.
Types & Tools Surface
packages/typescript/ai-anthropic/src/text/text-provider-options.ts, packages/typescript/ai-anthropic/src/tools/..., packages/typescript/ai-anthropic/src/tools/tool-converter.ts
Replaces Beta* types/imports with GA ToolChoice*, ToolUnion, and message types from @anthropic-ai/sdk/resources/messages; updates public type signatures/unions (e.g., AnthropicTool, BashTool, convertToolsToProviderFormat return type).
Tests
packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts
Removes beta-specific mocks, switches tests to messagesCreate path, relaxes adapter factory typing, and adds structured output tests (valid JSON parsing, invalid JSON error, tool-fallback behavior).

Sequence Diagram(s)

sequenceDiagram
  participant Adapter as Adapter (text.ts)
  participant SDK as Anthropic SDK (client.messages)
  participant Model as Claude Model
  participant Tool as Tool-based Fallback

  Adapter->>SDK: messages.create(..., output_config.format.json_schema?, stream?)
  SDK->>Model: forward request
  alt Model supports structured_output (Claude 4+)
    Model-->>SDK: structured JSON content blocks
    SDK-->>Adapter: response.content (JSON)
    Adapter->>Adapter: parse JSON -> data + rawText
  else Older model / no structured_output
    Adapter->>SDK: messages.create(..., tools + force tool_choice)
    Model->>Tool: invoke tool
    Tool-->>SDK: tool_use content
    SDK-->>Adapter: tool_use or text fallback
    Adapter->>Adapter: extract & JSON.parse -> data + rawText
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • AlemTuzlak
  • jherr

Poem

🐇 From beta burrows to GA's sunny glen,
Schemas now hop where tools led back then.
Streams hum JSON, neat and bright,
Fallbacks wait if Claude takes flight.
Hop—parse—ship: a rabbit's cheer for new code!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: updating @anthropic-ai/sdk to 0.74.0 and switching to native structured output instead of a tool-based workaround.
Description check ✅ Passed The description covers the main changes (structured output migration, SDK bump) and motivation (removing JSON handling hacks). Both checklist items are marked appropriately, and a changeset was generated.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @.changeset/real-structured-output.md:
- Around line 9-11: The changeset claim "No breaking changes to the public API"
contradicts the earlier note that TypeScript tool choice types moved from the
beta namespace to `@anthropic-ai/sdk/resources/messages`; update the changeset to
either (a) mark this release as a major bump or (b) explicitly state that
runtime behavior is compatible but TypeScript type imports have changed (i.e.,
consumers must update imports from BetaToolChoice* / client.beta.messages to the
GA equivalents under `@anthropic-ai/sdk/resources/messages`); reference the
phrasing in the changeset text that mentions "tool choice types" and the
statement "No breaking changes to the public API" so the maintainer edits the
wording to reflect the type-level breaking change or increments the version
level accordingly.

In `@packages/typescript/ai-anthropic/src/text/text-provider-options.ts`:
- Around line 132-137: The JSDoc block for the system field is malformed—fix the
comment above the declaration system?: string | Array<TextBlockParam> by
restoring the missing leading '*' on the description line and aligning
indentation so every line in the JSDoc starts with " *" (e.g., " * System
prompt." and " * A system prompt is..."), keeping the text content intact and
leaving the system property signature unchanged.
🧹 Nitpick comments (3)
packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts (2)

7-23: betaMessagesCreate mock is now dead code.

After migrating to client.messages.create, the betaMessagesCreate mock (and client.beta branch) is never used or asserted. Consider removing it to keep tests focused and avoid confusion about which API surface is exercised.

♻️ Suggested cleanup
 const mocks = vi.hoisted(() => {
-  const betaMessagesCreate = vi.fn()
   const messagesCreate = vi.fn()
 
   const client = {
-    beta: {
-      messages: {
-        create: betaMessagesCreate,
-      },
-    },
     messages: {
       create: messagesCreate,
     },
   }
 
-  return { betaMessagesCreate, messagesCreate, client }
+  return { messagesCreate, client }
 })

57-185: No test coverage for structuredOutput path.

The new native structured output flow (output_config.format with json_schema) introduced in text.ts is a key feature of this PR but has no test. Consider adding a test that mocks a non-streaming client.messages.create call with output_config and verifies the JSON parsing logic, including the error path.

packages/typescript/ai-anthropic/src/tools/tool-converter.ts (1)

42-64: The as Array<ToolUnion> cast silently bypasses type safety for beta-only tools.

Several branches (lines 49–58: code_execution, computer, memory, str_replace_editor, web_fetch) return types that are not part of ToolUnion per the JSDoc on lines 37–38. The cast on line 64 hides this mismatch. If the GA client.messages.create endpoint rejects these tool types at runtime, there will be no compile-time warning.

This is acknowledged in the doc comment, so no immediate action needed, but consider adding a runtime guard or at least a console.warn when a beta-only tool is passed through the GA endpoint, to surface issues early.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/typescript/ai-anthropic/src/adapters/text.ts`:
- Around line 178-220: The structured output request builds
createParams.output_config.format without a required "name" property and also
wraps JSON.parse errors inside the outer catch, causing double-prefixed error
messages; update the createParams construction in the method containing
createParams so output_config.format includes a "name" string alongside
type:'json_schema' and schema:outputSchema (matching Anthropic's expected
shape), and change error handling around client.messages.create and parsing so
JSON.parse(rawText) is either performed outside the outer try/catch or its
parsing errors are re-thrown directly (e.g., detect and re-throw the parse
Error) instead of being wrapped by the outer catch that throws `Structured
output generation failed`; reference createParams, output_config.format,
client.messages.create, rawText and the JSON.parse block when applying the fix.
🧹 Nitpick comments (2)
packages/typescript/ai-anthropic/src/text/text-provider-options.ts (1)

71-91: Inconsistent JSDoc indentation in AnthropicThinkingOptions.

The thinking property (Line 73) uses 5 spaces of indentation before *, and the budget_tokens description (Line 80) starts at column 0. While these appear to be pre-existing, they're in the same block as the reformatted JSDoc lines (74–75, 81–82), so this would be a good time to fix them.

Suggested fix
 export interface AnthropicThinkingOptions {
   /**
-     * Configuration for enabling Claude's extended thinking.
-     *
-     * When enabled, responses include thinking content blocks showing Claude's thinking process before the final answer. Requires a minimum budget of 1,024 tokens and counts towards your max_tokens limit.
-     */
+   * Configuration for enabling Claude's extended thinking.
+   *
+   * When enabled, responses include thinking content blocks showing Claude's thinking process before the final answer. Requires a minimum budget of 1,024 tokens and counts towards your max_tokens limit.
+   */
   thinking?:
     | {
         /**
-* Determines how many tokens Claude can use for its internal reasoning process. Larger budgets can enable more thorough analysis for complex problems, improving response quality.
-*
-* Must be ≥1024 and less than max_tokens
-*/
+         * Determines how many tokens Claude can use for its internal reasoning process. Larger budgets can enable more thorough analysis for complex problems, improving response quality.
+         *
+         * Must be ≥1024 and less than max_tokens
+         */
         budget_tokens: number
packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts (1)

31-32: as any cast trades type safety for test flexibility.

This allows testing with arbitrary model strings. It's fine for test code, but consider using actual model IDs from ANTHROPIC_MODELS (e.g., 'claude-sonnet-4') where possible to catch type regressions. The structured output tests already do this correctly.

@Leftwitch Leftwitch force-pushed the main branch 2 times, most recently from 23214a0 to cff44cd Compare February 10, 2026 00:43
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/typescript/ai-anthropic/src/adapters/text.ts (1)

247-297: ⚠️ Potential issue | 🟡 Minor

toolBasedStructuredOutput has the same double-wrapped error that was fixed in nativeStructuredOutput.

The throw at Line 284 (JSON parse failure) is inside the outer try block, so it gets caught at Line 291 and re-wrapped as "Structured output generation failed: Failed to extract structured output from response...". Apply the same separation pattern used in nativeStructuredOutput (Lines 193-222).

Proposed fix
+  private async toolBasedStructuredOutput(
+    requestParams: InternalTextProviderOptions,
+    chatOptions: StructuredOutputOptions<AnthropicTextProviderOptions>['chatOptions'],
+    outputSchema: StructuredOutputOptions<AnthropicTextProviderOptions>['outputSchema'],
+  ): Promise<StructuredOutputResult<unknown>> {
     const structuredOutputTool = {
       name: 'structured_output',
       description:
         'Use this tool to provide your response in the required structured format.',
       input_schema: {
         type: 'object' as const,
         properties: outputSchema.properties ?? {},
         required: outputSchema.required ?? [],
       },
     }

+    let response: Awaited<ReturnType<typeof this.client.messages.create>>
     try {
-      const response = await this.client.messages.create(
+      response = await this.client.messages.create(
         {
           ...requestParams,
           stream: false,
           tools: [structuredOutputTool],
           tool_choice: { type: 'tool', name: 'structured_output' },
         },
         {
           signal: chatOptions.request?.signal,
           headers: chatOptions.request?.headers,
         },
       )
+    } catch (error: unknown) {
+      const err = error as Error
+      throw new Error(
+        `Structured output generation failed: ${err.message || 'Unknown error occurred'}`,
+      )
+    }

-      let parsed: unknown = null
-      let rawText = ''
+    let parsed: unknown = null
+    let rawText = ''

-      for (const block of response.content) {
-        if (block.type === 'tool_use' && block.name === 'structured_output') {
-          parsed = block.input
-          rawText = JSON.stringify(block.input)
-          break
-        }
+    for (const block of response.content) {
+      if (block.type === 'tool_use' && block.name === 'structured_output') {
+        parsed = block.input
+        rawText = JSON.stringify(block.input)
+        break
       }
+    }

-      if (parsed === null) {
-        rawText = response.content
-          .map((b) => {
-            if (b.type === 'text') {
-              return b.text
-            }
-            return ''
-          })
-          .join('')
-        try {
-          parsed = JSON.parse(rawText)
-        } catch {
-          throw new Error(
-            `Failed to extract structured output from response. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,
-          )
-        }
+    if (parsed === null) {
+      rawText = response.content
+        .map((b) => {
+          if (b.type === 'text') {
+            return b.text
+          }
+          return ''
+        })
+        .join('')
+      try {
+        parsed = JSON.parse(rawText)
+      } catch {
+        throw new Error(
+          `Failed to extract structured output from response. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,
+        )
       }
-
-      return { data: parsed, rawText }
-    } catch (error: unknown) {
-      const err = error as Error
-      throw new Error(
-        `Structured output generation failed: ${err.message || 'Unknown error occurred'}`,
-      )
     }
+
+    return { data: parsed, rawText }
   }
🧹 Nitpick comments (3)
packages/typescript/ai-anthropic/src/model-meta.ts (1)

69-69: Dual source of truth for structured output capability.

Each model declares structured_output: true/false in its metadata and the ANTHROPIC_STRUCTURED_OUTPUT_MODELS set duplicates this information. Currently they're consistent, but future model additions could easily drift. Consider deriving the set from the model metadata (or vice versa) to keep a single source of truth.

Example: derive the set from model metadata
// Collect all model consts into an array, then filter
const ALL_MODELS = [
  CLAUDE_OPUS_4_6, CLAUDE_OPUS_4_5, CLAUDE_SONNET_4_5,
  CLAUDE_HAIKU_4_5, CLAUDE_OPUS_4_1, CLAUDE_SONNET_4,
  CLAUDE_SONNET_3_7, CLAUDE_OPUS_4, CLAUDE_HAIKU_3_5, CLAUDE_HAIKU_3,
] as const

export const ANTHROPIC_STRUCTURED_OUTPUT_MODELS: ReadonlySet<string> = new Set(
  ALL_MODELS.filter(m => m.supports.structured_output).map(m => m.id),
)

Also applies to: 100-100, 131-131, 162-162, 193-193, 224-224, 254-254, 285-285, 316-316, 347-347, 418-430

packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts (1)

31-32: as any cast disables all type checking on model parameter.

This hides invalid model strings at test time. A narrower cast like as (typeof ANTHROPIC_MODELS)[number] would still allow flexibility while preserving some safety:

-const createAdapter = <TModel extends string>(model: TModel) =>
-  new AnthropicTextAdapter({ apiKey: 'test-key' }, model as any)
+import type { ANTHROPIC_MODELS } from '../src/model-meta'
+type AnyAnthropicModel = (typeof ANTHROPIC_MODELS)[number]
+const createAdapter = (model: string) =>
+  new AnthropicTextAdapter({ apiKey: 'test-key' }, model as AnyAnthropicModel)
packages/typescript/ai-anthropic/src/tools/tool-converter.ts (1)

9-9: Type cast for beta-only tools is documented and necessary.

The as Array<ToolUnion> cast on Line 64 is necessary because the tool converters for ComputerUse, CodeExecution, Memory, and WebFetch return beta-only types from @anthropic-ai/sdk/resources/beta that are not included in the GA ToolUnion discriminated union. The JSDoc at Lines 36-38 correctly documents this structural compatibility at runtime. The cast is appropriate here given the SDK constraints.

Consider adding a runtime assertion or type guard in a future iteration to catch shape mismatches for beta tools, rather than relying solely on the cast. This would provide better protection if Anthropic's SDK evolves in ways that change the runtime shape of these beta tool types.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/typescript/ai-anthropic/src/adapters/text.ts (1)

236-245: ⚠️ Potential issue | 🟡 Minor

toolBasedStructuredOutput hardcodes type: 'object' regardless of outputSchema.type.

The tool's input_schema always sets type: 'object' (line 241), discarding whatever outputSchema.type actually is. If a caller passes a non-object schema (e.g., an array root), this silently changes the schema semantics. Consider propagating outputSchema.type instead.

Suggested fix
     const structuredOutputTool = {
       name: 'structured_output',
       description:
         'Use this tool to provide your response in the required structured format.',
       input_schema: {
-        type: 'object' as const,
-        properties: outputSchema.properties ?? {},
-        required: outputSchema.required ?? [],
+        ...outputSchema,
       },
     }
🧹 Nitpick comments (3)
packages/typescript/ai-anthropic/src/tools/index.ts (1)

16-25: Potential overlap between ToolUnion and individual GA tool types in this union.

ToolUnion from the SDK likely already includes GA equivalents of BashTool, ComputerUseTool, TextEditorTool, etc. The overlap is harmless (TypeScript collapses duplicate union members), but as beta tools graduate to GA, consider pruning the individual types that become redundant with ToolUnion to keep the union tidy.

packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts (1)

31-32: The as any cast bypasses model type safety.

Using model as any allows testing with arbitrary model strings, which is practical. However, consider using a type assertion to (typeof ANTHROPIC_MODELS)[number] or adding a comment explaining why the cast is needed, so future contributors don't accidentally misuse this pattern in non-test code.

packages/typescript/ai-anthropic/src/model-meta.ts (1)

418-430: ANTHROPIC_STRUCTURED_OUTPUT_MODELS duplicates per-model structured_output flags.

The set is consistent with the individual model metadata, but the capability is now expressed in two places. Consider deriving the set from the model metadata to avoid them drifting apart, e.g.:

export const ANTHROPIC_STRUCTURED_OUTPUT_MODELS: ReadonlySet<string> = new Set(
  [CLAUDE_OPUS_4_6, CLAUDE_OPUS_4_5, CLAUDE_SONNET_4_5, CLAUDE_HAIKU_4_5, CLAUDE_OPUS_4_1, CLAUDE_SONNET_4, CLAUDE_OPUS_4]
    .filter(m => m.supports.structured_output)
    .map(m => m.id)
)

This is optional — the current approach is clear and unlikely to drift with only ~10 models.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant