5.7 KiB
Claude API — PHP
Note: The PHP SDK is the official Anthropic SDK for PHP. A beta tool runner is available via
$client->beta->messages->toolRunner(). Structured output helpers are supported viaStructuredOutputModelclasses. Agent SDK is not available. Bedrock, Vertex AI, and Foundry clients are supported.
Installation
composer require "anthropic-ai/sdk"
Client Initialization
use Anthropic\Client;
// Using API key from environment variable
$client = new Client(apiKey: getenv("ANTHROPIC_API_KEY"));
Amazon Bedrock
use Anthropic\Bedrock\MantleClient;
// Messages-API Bedrock endpoint. Reads AWS credentials from env.
$client = new MantleClient(awsRegion: 'us-east-1');
Model IDs on Bedrock take an anthropic. prefix — e.g. model: 'anthropic.{{OPUS_ID}}'.
Google Vertex AI
use Anthropic\Vertex;
// Constructor is private. Parameter is `location`, not `region`.
$client = Vertex\Client::fromEnvironment(
location: 'us-east5',
projectId: 'my-project-id',
);
Anthropic Foundry
use Anthropic\Foundry;
// Constructor is private. baseUrl or resource is required.
$client = Foundry\Client::withCredentials(
apiKey: getenv('ANTHROPIC_FOUNDRY_API_KEY'),
baseUrl: 'https://<resource>.services.ai.azure.com/anthropic/v1',
);
Basic Message Request
$message = $client->messages->create(
model: '{{OPUS_ID}}',
maxTokens: 16000,
messages: [
['role' => 'user', 'content' => 'What is the capital of France?'],
],
);
// content is an array of polymorphic blocks (TextBlock, ToolUseBlock,
// ThinkingBlock). Accessing ->text on content[0] without checking the block
// type will throw if the first block is not a TextBlock (e.g., when extended
// thinking is enabled and a ThinkingBlock comes first). Always guard:
foreach ($message->content as $block) {
if ($block->type === 'text') {
echo $block->text;
}
}
If you only want the first text block:
foreach ($message->content as $block) {
if ($block->type === 'text') {
echo $block->text;
break;
}
}
Extended Thinking
Adaptive thinking is the recommended mode for Claude 4.6+ models. Claude decides dynamically when and how much to think.
use Anthropic\Messages\ThinkingBlock;
$message = $client->messages->create(
model: '{{OPUS_ID}}',
maxTokens: 16000,
thinking: ['type' => 'adaptive', 'display' => 'summarized'], // display opt-in: default is omitted (empty thinking text) on Fable 5 / Mythos 5 / Opus 4.8 / 4.7
messages: [
['role' => 'user', 'content' => 'Solve: 27 * 453'],
],
);
// ThinkingBlock(s) precede TextBlock in content
foreach ($message->content as $block) {
if ($block instanceof ThinkingBlock) {
echo "Thinking:\n{$block->thinking}\n\n";
// $block->signature is an opaque string — preserve verbatim if
// passing thinking blocks back in multi-turn conversations
} elseif ($block->type === 'text') {
echo "Answer: {$block->text}\n";
}
}
Fable 5, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6: Use adaptive thinking (above).
['type' => 'enabled', 'budgetTokens' => N]is removed on Fable 5, Opus 4.8, and 4.7 (400 if sent); deprecated on Opus 4.6 and Sonnet 4.6. Older models: Usethinking: ['type' => 'enabled', 'budgetTokens' => N](budget must be <maxTokens, min 1024).
$block->type === 'thinking' also works for the check; instanceof narrows for PHPStan.
Prompt Caching
system: takes an array of text blocks; set cacheControl on the last block. Array-shape syntax (camelCase keys) is idiomatic. For placement patterns and the silent-invalidator audit checklist, see shared/prompt-caching.md.
$message = $client->messages->create(
model: '{{OPUS_ID}}',
maxTokens: 16000,
system: [
['type' => 'text', 'text' => $longSystemPrompt, 'cacheControl' => ['type' => 'ephemeral']],
],
messages: [['role' => 'user', 'content' => 'Summarize the key points']],
);
For 1-hour TTL: 'cacheControl' => ['type' => 'ephemeral', 'ttl' => '1h']. There's also a top-level cacheControl: on messages->create(...) that auto-places on the last cacheable block.
Verify hits via $message->usage->cacheCreationInputTokens / $message->usage->cacheReadInputTokens.
Stop Details
When stopReason is 'refusal', the response includes structured stopDetails:
if ($message->stopReason === 'refusal' && $message->stopDetails !== null) {
echo "Category: " . $message->stopDetails->category . "\n"; // e.g. "cyber", "bio", "reasoning_extraction", "frontier_llm", or null — see docs for the full set
echo "Explanation: " . $message->stopDetails->explanation . "\n";
}
Refusal fallbacks ({{FABLE_NAME}}) — opt in by default. Fallbacks are opt-in: without them a refused request simply stops. New {{FABLE_ID}} code should include the server-side fallbacks parameter (beta header server-side-fallback-2026-06-01, fallback model {{OPUS_ID}}, on the beta messages call) by default. The exact PHP binding (and the client-side middleware for providers without server-side support) is not documented here — WebFetch the PHP SDK repo's examples/ from shared/live-sources.md; full semantics in shared/model-migration.md → Migrating to {{FABLE_NAME}} → refusal stop reason.
Error Type
APIStatusException exposes a ->type property for programmatic error classification:
try {
$client->messages->create(...);
} catch (\Anthropic\Core\Exceptions\APIStatusException $e) {
echo $e->type?->value; // "rate_limit_error", "overloaded_error", etc.
}