Problem/Motivation
To support native Anthropic API features (effort, thinking, caching, compaction), we need the official PHP SDK installed and a Drupal service wrapping it. This is the foundation that all subsequent
feature issues in the [META] Native Anthropic SDK integration depend on.
The SDK provides two approaches for making API calls:
- Typed methods (
$client->messages->create()): For features with full SDK support (prompt caching, structured outputs, tool cache_control) - Raw requests (
$client->request()): The SDK's recommended escape hatch for features not yet typed (adaptive thinking, effort). Same auth, retries, and error handling.
Both paths use the same SDK infrastructure. As the SDK adds types for more features, raw request calls migrate to typed methods with no API changes.
Steps to reproduce
N/A — new functionality.
Proposed resolution
- Add
anthropic-ai/sdk: ^0.5andphp-http/guzzle7-adaptertocomposer.json - Create
AnthropicNativeClientservice (src/Service/AnthropicNativeClient.php) wrapping the SDK with Drupal's HTTP client - Register the service in
ai_provider_anthropic.services.yml - Override
chat()inAnthropicProviderwith hybrid routing: detect when native features are configured → route tonativeChat(), otherwise fall through to
parent::chat() - Basic
nativeChat()implementation using$client->request()
Key constraint: SDK v0.5.0 has a known issue where extraBodyParams is defined in RequestOptions but never merged into requests. The
$client->request() method is the reliable alternative.
No new features in this issue — just the plumbing. Effort, thinking, streaming, and token usage are handled in follow-up issues.
Remaining tasks
- Add composer dependencies
- Create AnthropicNativeClient service
- Create services.yml
- Override chat() with hybrid routing
- Verify basic chat still works via compat layer (no regression)
User interface changes
None.
API changes
None. Internal routing only.
Data model changes
None.
Issue fork ai_provider_anthropic-3572139
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
camoa commentedFoundation: Native SDK client and hybrid chat() routing
Branch
3572139-add-anthropic-aisdk-dependencyadds theanthropic-ai/sdkPHP package and creates the plumbing for native API features in future issues.Changes
anthropic-ai/sdk: ^0.5andphp-http/guzzle7-adapter: ^1.0$client->request()escape hatch for untyped params (effort, thinking not yet typed in SDK v0.5)chat()override withrequiresNativeApi()gate,nativeChat(),buildNativePayload(),parseNativeResponse()parseNativeResponse()mapsinput_tokens,output_tokens, andcache_read_input_tokenstoTokenUsageDtoDesign
requiresNativeApi()currently returns FALSE — no native-only features are wired yetparent::chat()requiresNativeApi()AND wire them inbuildNativePayload()togetherphpunit.xmlshipped — CI uses core'sphpunit.xml.distvia GitLab templatesVerification
Comment #4
camoa commentedComment #5
camoa commentedComment #6
camoa commentedHow native API routing works
The routing decision is config-driven via
requiresNativeApi().In this Foundation MR, it always returns
FALSE— no native-only features are wired yet, so all requests continue through the OpenAI compat layer with zero change in behavior.Phase 1 will change it to check configuration keys:
protected function requiresNativeApi(): bool { $config = $this->configuration; return !empty($config['effort']) || !empty($config['thinking_mode']); }These config keys come from
getModelSettings(), which adds UI fields (effort dropdown, thinking mode selector) to the per-model settings form. The Drupal AI framework passes them back as$this->configurationon each request.Request flow
parent::chat()via OpenAI compat (proven path, no regression)nativeChat()via Anthropic SDKThis means the site admin controls the routing per model. If they don't enable any native-only features, everything stays on the compat path. The native SDK path only activates when explicitly
needed.
Each Phase 1 feature added must update both
requiresNativeApi()(the check) andbuildNativePayload()(the implementation) to prevent mismatches.Comment #7
camoa commentedSymfony AI forward compatibility
This work is designed with the Drupal AI → Symfony AI migration in mind (AI 2.0 Autumn 2026, AI 3.0 Early 2027).
The Symfony AI Anthropic bridge already uses the native Messages API (not OpenAI compat), making our approach
directionally aligned.
How our code maps to Symfony AI
buildNativePayload()→Contract::createRequestPayload()+MessageBagNormalizerparseNativeResponse()→ResultConverter::convert()+TokenUsageExtractor::extract()AnthropicNativeClient::requestRaw()→ModelClient::request()Portability of Phase 1 features
Symfony AI merges arbitrary options into the API payload via
array_merge($options, $payload). This means:$options['output_config']['effort']with zero Symfony AI changes$options['thinking']with zero Symfony AI changesResultConverterupdate (upstream contribution opportunity)TokenUsageExtractorupdate (upstream contribution opportunity)Design choices for portability
buildNativePayload()is a clearly-delimited block, easy to extract into Symfony AI optionsPhase 1–2 features provide immediate value for the current module and inform upstream contributions to
symfony/ai-anthropic-platformwhen the migration happens.Comment #8
camoa commentedComment #9
michael.romero commentedHi team,
I’ve been reviewing the code and noticed that the Anthropic SDK is currently defined as:
Using a caret (^) constraint may introduce compatibility risks over time, as future minor releases from Anthropic could include changes that affect the current implementation. To ensure long-term stability and predictable behavior, I would recommend locking the dependency to a specific version instead of allowing automatic minor upgrades.
Additionally, while reviewing the AnthropicProvider class, I observed that it does not define a constructor. Without a constructor, dependency injections cannot be stored in class properties and accessed via $this->..., which limits extensibility and maintainability. Introducing a constructor would allow proper assignment of injected services to properties.
It would also be beneficial to define nativeClient as protected readonly, which would improve immutability and clearly communicate that the client should not be modified after instantiation.
Comment #10
camoa commentedThanks for the review, Michael!
1. Version constraint (^0.5): Keeping as-is. For pre-1.0 packages, Composer's caret already constrains to >=0.5.0, <0.6.0 — only patch updates are allowed. This follows standard Drupal contrib practice and
avoids the maintenance burden of exact pins on downstream consumers.
2. Constructor: The grandparent AiProviderClientBase::__construct() is final, so we can't override it. The create() factory + property assignment is the only available DI pattern here.
3. Readonly: Good call — applying protected readonly to nativeClient. It works with the create() pattern since PHP allows readonly initialization from the declaring class scope.
Comment #12
camoa commented