중단 사유와 폴백
중단 사유와 폴백 (Stop reasons and fallback)
모든 Messages API 응답에는 stop_reason 필드가 들어 있어서, Claude가 왜 생성을 멈췄는지 알려줘요. 이 필드를 확인해서 응답을 그대로 쓸지, 대화를 이어갈지, 재시도할지, 아니면 다른 모델로 폴백할지 결정하면 돼요.
출처: 문서
본문
전체 응답 스키마는 Messages API 레퍼런스를 참고하세요.
한눈에 보는 표 (Quick reference)
| 값 | 발생 시점 | 어떻게 처리할까요 |
|---|---|---|
end_turn |
Claude가 자연스럽게 응답을 마쳤어요. | 응답을 사용하면 돼요. |
max_tokens |
응답이 max_tokens 한도에 도달했어요. |
max_tokens를 올리거나 응답을 이어서 생성해요. |
stop_sequence |
Claude가 stop_sequences 중 하나를 출력했어요. |
stop_sequence를 읽어 어떤 시퀀스가 발동했는지 확인해요. |
tool_use |
Claude가 도구를 호출하고 있어요. | 도구를 실행하고 결과를 반환해요. 서버 도구 호출 중 결과 블록이 아직 없는 경우는 다음 응답에서 완성돼요. |
pause_turn |
서버 도구 루프가 반복 한도에 도달했어요. | 어시스턴트 콘텐츠를 다시 보내서 이어가요. |
refusal |
Claude가 응답을 거부했어요. | stop_details를 읽고 폴백 모델로 재시도해요. |
model_context_window_exceeded |
응답이 모델의 컨텍스트 윈도우를 가득 채웠어요. | 응답을 잘렸다고 취급하면 돼요. |
stop_reason 필드
stop_reason 필드는 성공한 모든 Messages API 응답에 들어 있어요. 요청 처리 실패를 뜻하는 오류와 달리, stop_reason은 Claude가 응답 생성을 왜 마쳤는지 알려줘요.
{
"id": "msg_01234",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "Here's the answer to your question..."
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"stop_details": null,
"usage": {
"input_tokens": 100,
"output_tokens": 50
}
}
stop_reason 값들
end_turn
가장 흔한 중단 사유예요. Claude가 자연스럽게 응답을 마쳤다는 뜻이에요.
ant messages create \
--model claude-opus-5-5 \
--max-tokens 1024 \
--message '{role: user, content: "Hello!"}' \
--format json | jq 'if .stop_reason == "end_turn" then (.content[] | select(.type == "text") | .text) else . end'
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
)
if response.stop_reason == "end_turn":
# Process the complete response
for block in response.content:
if block.type == "text":
print(block.text)
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }]
});
if (response.stop_reason === "end_turn") {
// Process the complete response
const textBlock = response.content.find(
(block): block is Anthropic.TextBlock => block.type === "text"
);
console.log(textBlock?.text);
}
AnthropicClient client = new();
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = [new() { Role = Role.User, Content = "Hello!" }]
});
if (response.StopReason == "end_turn")
{
// Process the complete response
foreach (var block in response.Content)
{
if (block.TryPickText(out var textBlock))
{
Console.WriteLine(textBlock.Text);
}
}
}
client := anthropic.NewClient()
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Hello!")),
},
})
if err != nil {
log.Fatal(err)
}
if response.StopReason == "end_turn" {
// Process the complete response
for _, block := range response.Content {
if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
fmt.Println(textBlock.Text)
}
}
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
Message response = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addUserMessage("Hello!")
.build()
);
if (response.stopReason().map(StopReason.END_TURN::equals).orElse(false)) {
// Process the complete response
response.content().stream()
.flatMap(block -> block.text().stream())
.forEach(textBlock -> IO.println(textBlock.text()));
}
$client = new Client();
$response = $client->messages->create(
maxTokens: 1024,
messages: [['role' => 'user', 'content' => 'Hello!']],
model: 'claude-opus-5-5',
);
if ($response->stopReason === 'end_turn') {
// Process the complete response
foreach ($response->content as $block) {
if ($block->type === 'text') {
echo $block->text, PHP_EOL;
}
}
}
client = Anthropic::Client.new
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }]
)
if response.stop_reason == :end_turn
# Process the complete response
response.content.each do |block|
puts block.text if block.type == :text
end
end
흔한 원인:
- 도구 결과 직후 텍스트 블록을 추가한 경우 (Claude가 도구 결과 뒤에는 항상 사용자가 텍스트를 넣는다는 패턴을 학습해서, 그 패턴을 따르려고 턴을 끝내버려요)
- Claude의 완료된 응답을 아무것도 추가하지 않고 그대로 다시 보낸 경우 (Claude가 이미 끝났다고 판단했으니 계속 끝난 상태로 남아요)
빈 응답을 막는 방법:
# CORRECT: Send tool results directly without additional text
messages = [
{"role": "user", "content": "Calculate the sum of 1234 and 5678"},
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_123",
"name": "calculator",
"input": {"operation": "add", "a": 1234, "b": 5678},
}
],
},
{
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": "toolu_123", "content": "6912"}
],
}, # Just the tool_result, no additional text
]
```
```typescript TypeScript
// INCORRECT: Adding text immediately after tool_result
let messages: Anthropic.MessageParam[] = [
{ role: "user", content: "Calculate the sum of 1234 and 5678" },
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_123",
name: "calculator",
input: { operation: "add", a: 1234, b: 5678 }
}
]
},
{
role: "user",
content: [
{ type: "tool_result", tool_use_id: "toolu_123", content: "6912" },
{ type: "text", text: "Here's the result" } // Don't add text after tool_result
]
}
];
// CORRECT: Send tool results directly without additional text
messages = [
{ role: "user", content: "Calculate the sum of 1234 and 5678" },
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_123",
name: "calculator",
input: { operation: "add", a: 1234, b: 5678 }
}
]
},
{
role: "user",
// Just the tool_result, no additional text
content: [{ type: "tool_result", tool_use_id: "toolu_123", content: "6912" }]
}
];
```
```csharp C#
using System.Text.Json;
using Anthropic.Models.Messages;
var input = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""{"operation":"add","a":1234,"b":5678}"""
)!;
// INCORRECT: Adding text immediately after tool_result
List<MessageParam> messages =
[
new() { Role = Role.User, Content = "Calculate the sum of 1234 and 5678" },
new()
{
Role = Role.Assistant,
Content = new List<ContentBlockParam>
{
new ToolUseBlockParam { ID = "toolu_123", Name = "calculator", Input = input }
}
},
new()
{
Role = Role.User,
Content = new List<ContentBlockParam>
{
new ToolResultBlockParam { ToolUseID = "toolu_123", Content = "6912" },
new TextBlockParam { Text = "Here's the result" } // Don't add text after tool_result
}
}
];
// CORRECT: Send tool results directly without additional text
messages =
[
new() { Role = Role.User, Content = "Calculate the sum of 1234 and 5678" },
new()
{
Role = Role.Assistant,
Content = new List<ContentBlockParam>
{
new ToolUseBlockParam { ID = "toolu_123", Name = "calculator", Input = input }
}
},
new()
{
Role = Role.User,
// Just the tool_result, no additional text
Content = new List<ContentBlockParam>
{
new ToolResultBlockParam { ToolUseID = "toolu_123", Content = "6912" }
}
}
];
```
```go Go
input := map[string]any{"operation": "add", "a": 1234, "b": 5678}
// INCORRECT: Adding text immediately after tool_result
messages := []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Calculate the sum of 1234 and 5678")),
anthropic.NewAssistantMessage(
anthropic.NewToolUseBlock("toolu_123", input, "calculator"),
),
anthropic.NewUserMessage(
anthropic.NewToolResultBlock("toolu_123", "6912", false),
anthropic.NewTextBlock("Here's the result"), // Don't add text after tool_result
),
}
// CORRECT: Send tool results directly without additional text
messages = []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Calculate the sum of 1234 and 5678")),
anthropic.NewAssistantMessage(
anthropic.NewToolUseBlock("toolu_123", input, "calculator"),
),
// Just the tool_result, no additional text
anthropic.NewUserMessage(
anthropic.NewToolResultBlock("toolu_123", "6912", false),
),
}
```
```java Java
ToolUseBlockParam toolUse = ToolUseBlockParam.builder()
.id("toolu_123")
.name("calculator")
.input(ToolUseBlockParam.Input.builder()
.putAdditionalProperty("operation", JsonValue.from("add"))
.putAdditionalProperty("a", JsonValue.from(1234))
.putAdditionalProperty("b", JsonValue.from(5678))
.build())
.build();
// INCORRECT: Adding text immediately after tool_result
List<MessageParam> messages = List.of(
MessageParam.builder().role(MessageParam.Role.USER)
.content("Calculate the sum of 1234 and 5678").build(),
MessageParam.builder().role(MessageParam.Role.ASSISTANT)
.contentOfBlockParams(List.of(ContentBlockParam.ofToolUse(toolUse))).build(),
MessageParam.builder().role(MessageParam.Role.USER)
.contentOfBlockParams(List.of(
ContentBlockParam.ofToolResult(
ToolResultBlockParam.builder().toolUseId("toolu_123").content("6912").build()),
// Don't add text after tool_result
ContentBlockParam.ofText(TextBlockParam.builder().text("Here's the result").build())
)).build()
);
// CORRECT: Send tool results directly without additional text
messages = List.of(
MessageParam.builder().role(MessageParam.Role.USER)
.content("Calculate the sum of 1234 and 5678").build(),
MessageParam.builder().role(MessageParam.Role.ASSISTANT)
.contentOfBlockParams(List.of(ContentBlockParam.ofToolUse(toolUse))).build(),
// Just the tool_result, no additional text
MessageParam.builder().role(MessageParam.Role.USER)
.contentOfBlockParams(List.of(
ContentBlockParam.ofToolResult(
ToolResultBlockParam.builder().toolUseId("toolu_123").content("6912").build())
)).build()
);
```
```php PHP
// INCORRECT: Adding text immediately after tool_result
$messages = [
['role' => 'user', 'content' => 'Calculate the sum of 1234 and 5678'],
[
'role' => 'assistant',
'content' => [
[
'type' => 'tool_use',
'id' => 'toolu_123',
'name' => 'calculator',
'input' => ['operation' => 'add', 'a' => 1234, 'b' => 5678],
],
],
],
[
'role' => 'user',
'content' => [
['type' => 'tool_result', 'tool_use_id' => 'toolu_123', 'content' => '6912'],
// Don't add text after tool_result
['type' => 'text', 'text' => "Here's the result"],
],
],
];
// CORRECT: Send tool results directly without additional text
$messages = [
['role' => 'user', 'content' => 'Calculate the sum of 1234 and 5678'],
[
'role' => 'assistant',
'content' => [
[
'type' => 'tool_use',
'id' => 'toolu_123',
'name' => 'calculator',
'input' => ['operation' => 'add', 'a' => 1234, 'b' => 5678],
],
],
],
[
'role' => 'user',
// Just the tool_result, no additional text
'content' => [
['type' => 'tool_result', 'tool_use_id' => 'toolu_123', 'content' => '6912'],
],
],
];
```
```ruby Ruby
# INCORRECT: Adding text immediately after tool_result
messages = [
{ role: "user", content: "Calculate the sum of 1234 and 5678" },
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_123",
name: "calculator",
input: { operation: "add", a: 1234, b: 5678 }
}
]
},
{
role: "user",
content: [
{ type: "tool_result", tool_use_id: "toolu_123", content: "6912" },
# Don't add text after tool_result
{ type: "text", text: "Here's the result" }
]
}
]
# CORRECT: Send tool results directly without additional text
messages = [
{ role: "user", content: "Calculate the sum of 1234 and 5678" },
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_123",
name: "calculator",
input: { operation: "add", a: 1234, b: 5678 }
}
]
},
{
role: "user",
# Just the tool_result, no additional text
content: [
{ type: "tool_result", tool_use_id: "toolu_123", content: "6912" }
]
}
]
```
메시지 구조를 고친 뒤에도 빈 응답이 계속 온다면, 빈 응답으로 재시도하는 대신 새 사용자 메시지에 계속하라는 프롬프트를 추가해보세요:
# Check if response is empty
if response.stop_reason == "end_turn" and not response.content:
# INCORRECT: Don't just retry with the empty response
# This won't work because Claude already decided it's done
# CORRECT: Add a continuation prompt in a NEW user message
messages.append({"role": "user", "content": "Please continue"})
response = client.messages.create(
model="claude-opus-5-5", max_tokens=1024, messages=messages
)
return response
```
```typescript TypeScript
async function handleEmptyResponse(
client: Anthropic,
messages: Anthropic.MessageParam[]
): Promise<Anthropic.Message> {
let response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
messages
});
// Check if response is empty
if (response.stop_reason === "end_turn" && response.content.length === 0) {
// INCORRECT: Don't just retry with the empty response
// This won't work because Claude already decided it's done
// CORRECT: Add a continuation prompt in a NEW user message
messages.push({ role: "user", content: "Please continue" });
response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
messages
});
}
return response;
}
```
```csharp C#
static async Task<Message> HandleEmptyResponse(AnthropicClient client, List<MessageParam> messages)
{
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = messages
});
// Check if response is empty
if (response.StopReason == "end_turn" && response.Content.Count == 0)
{
// CORRECT: Add a continuation prompt in a NEW user message
messages.Add(new() { Role = Role.User, Content = "Please continue" });
response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = messages
});
}
return response;
}
```
```go Go
func handleEmptyResponse(client anthropic.Client, messages []anthropic.MessageParam) (*anthropic.Message, error) {
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: messages,
})
if err != nil {
return nil, err
}
// Check if response is empty
if response.StopReason == "end_turn" && len(response.Content) == 0 {
// CORRECT: Add a continuation prompt in a NEW user message
messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock("Please continue")))
response, err = client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: messages,
})
if err != nil {
return nil, err
}
}
return response, nil
}
```
```java Java
static Message handleEmptyResponse(AnthropicClient client, List<MessageParam> messages) {
Message response = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.messages(messages)
.build()
);
// Check if response is empty
boolean isEndTurn = response.stopReason().map(StopReason.END_TURN::equals).orElse(false);
if (isEndTurn && response.content().isEmpty()) {
// CORRECT: Add a continuation prompt in a NEW user message
List<MessageParam> extended = new ArrayList<>(messages);
extended.add(MessageParam.builder()
.role(MessageParam.Role.USER)
.content("Please continue")
.build());
response = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.messages(extended)
.build()
);
}
return response;
}
```
```php PHP
function handle_empty_response(Client $client, array $messages)
{
$response = $client->messages->create(
maxTokens: 1024,
messages: $messages,
model: 'claude-opus-5-5',
);
// Check if response is empty
if ($response->stopReason === 'end_turn' && count($response->content) === 0) {
// CORRECT: Add a continuation prompt in a NEW user message
$messages[] = ['role' => 'user', 'content' => 'Please continue'];
$response = $client->messages->create(
maxTokens: 1024,
messages: $messages,
model: 'claude-opus-5-5',
);
}
return $response;
}
```
```ruby Ruby
def handle_empty_response(client, messages)
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: messages
)
# Check if response is empty
if response.stop_reason == :end_turn && response.content.empty?
# CORRECT: Add a continuation prompt in a NEW user message
messages << { role: "user", content: "Please continue" }
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: messages
)
end
response
end
```
모범 사례:
- 도구 결과 직후 텍스트 블록을 절대 추가하지 마세요: 이렇게 하면 Claude가 모든 도구 사용 뒤에 사용자 입력이 온다고 학습해버려요.
- 수정 없이 빈 응답을 재시도하지 마세요: 빈 응답을 그대로 다시 보내도 소용없어요.
- 계속하라는 프롬프트는 최후의 수단으로: 이 해결책들로 문제가 해결되지 않을 때만 쓰세요.
max_tokens
Claude가 요청에서 지정한 max_tokens 한도에 도달해서 멈췄어요.
ant messages create \
--model claude-opus-5-5 \
--max-tokens 10 \
--message '{role: user, content: "Explain quantum physics"}' \
--format json | jq '.stop_reason'
client = anthropic.Anthropic()
# Request with limited tokens
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=10,
messages=[{"role": "user", "content": "Explain quantum physics"}],
)
if response.stop_reason == "max_tokens":
# Response was truncated
print("Response was cut off at token limit")
# Consider making another request to continue
const client = new Anthropic();
// Request with limited tokens
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 10,
messages: [{ role: "user", content: "Explain quantum physics" }]
});
if (response.stop_reason === "max_tokens") {
// Response was truncated
console.log("Response was cut off at token limit");
// Consider making another request to continue
}
AnthropicClient client = new();
// Request with limited tokens
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 10,
Messages = [new() { Role = Role.User, Content = "Explain quantum physics" }]
});
if (response.StopReason == "max_tokens")
{
// Response was truncated
Console.WriteLine("Response was cut off at token limit");
// Consider making another request to continue
}
client := anthropic.NewClient()
// Request with limited tokens
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 10,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Explain quantum physics")),
},
})
if err != nil {
log.Fatal(err)
}
if response.StopReason == "max_tokens" {
// Response was truncated
fmt.Println("Response was cut off at token limit")
// Consider making another request to continue
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
// Request with limited tokens
Message response = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(10L)
.addUserMessage("Explain quantum physics")
.build()
);
if (response.stopReason().map(StopReason.MAX_TOKENS::equals).orElse(false)) {
// Response was truncated
IO.println("Response was cut off at token limit");
// Consider making another request to continue
}
$client = new Client();
// Request with limited tokens
$response = $client->messages->create(
maxTokens: 10,
messages: [['role' => 'user', 'content' => 'Explain quantum physics']],
model: 'claude-opus-5-5',
);
if ($response->stopReason === 'max_tokens') {
// Response was truncated
echo 'Response was cut off at token limit', PHP_EOL;
// Consider making another request to continue
}
client = Anthropic::Client.new
# Request with limited tokens
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 10,
messages: [{ role: "user", content: "Explain quantum physics" }]
)
if response.stop_reason == :max_tokens
# Response was truncated
puts "Response was cut off at token limit"
# Consider making another request to continue
end
# Check if the response was truncated mid tool use
STOP_REASON=$(jq -r '.stop_reason' <<<"$RESPONSE")
LAST_TYPE=$(jq -r '.content[-1].type' <<<"$RESPONSE")
if [ "$STOP_REASON" = "max_tokens" ] && [ "$LAST_TYPE" = "tool_use" ]; then
# Retry with a higher max_tokens
ant messages create --max-tokens 4096 <<'YAML'
model: claude-opus-5-5
tools:
- name: get_weather
description: Get the current weather in a given location
input_schema:
type: object
properties:
location:
type: string
required:
- location
messages:
- role: user
content: What is the weather in San Francisco?
YAML
fi
```
```python Python
# Check if response was truncated during tool use
if response.stop_reason == "max_tokens":
# Check if the last content block is an incomplete tool_use
last_block = response.content[-1]
if last_block.type == "tool_use":
# Send the request with higher max_tokens
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=4096, # Increased limit
messages=messages,
tools=tools,
)
```
```typescript TypeScript
// Check if response was truncated during tool use
if (response.stop_reason === "max_tokens") {
// Check if the last content block is an incomplete tool_use
const lastBlock = response.content[response.content.length - 1];
if (lastBlock.type === "tool_use") {
// Send the request with higher max_tokens
response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 4096, // Increased limit
messages: messages,
tools: tools
});
}
}
```
```csharp C#
using System.Linq;
using Anthropic;
using Anthropic.Models.Messages;
AnthropicClient client = new();
var parameters = new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = messages,
Tools = tools
};
var response = await client.Messages.Create(parameters);
if (response.StopReason == "max_tokens")
{
var lastBlock = response.Content.Last();
if (lastBlock.TryPickToolUse(out _))
{
response = await client.Messages.Create(parameters with { MaxTokens = 4096 });
}
}
```
```go Go
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: messages,
Tools: tools,
})
if err != nil {
log.Fatal(err)
}
if response.StopReason == "max_tokens" {
lastBlock := response.Content[len(response.Content)-1]
switch lastBlock.AsAny().(type) {
case anthropic.ToolUseBlock:
response, err = client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 4096,
Messages: messages,
Tools: tools,
})
if err != nil {
log.Fatal(err)
}
}
}
```
```java Java
// Check if response was truncated during tool use
if (response.stopReason().isPresent() && response.stopReason().get().equals(StopReason.MAX_TOKENS)) {
ContentBlock lastBlock = response.content().get(response.content().size() - 1);
if (lastBlock.toolUse().isPresent()) {
// Send the request with higher max_tokens
response = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(4096L) // Increased limit
.messages(messages)
.tools(tools)
.build()
);
}
}
```
```php PHP
$response = $client->messages->create(
maxTokens: 1024,
messages: $messages,
model: 'claude-opus-5-5',
tools: $tools,
);
if ($response->stopReason === 'max_tokens') {
$lastBlock = end($response->content);
if ($lastBlock->type === 'tool_use') {
$response = $client->messages->create(
maxTokens: 4096,
messages: $messages,
model: 'claude-opus-5-5',
tools: $tools,
);
}
}
```
```ruby Ruby
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: messages,
tools: tools
)
if response.stop_reason == :max_tokens
last_block = response.content.last
if last_block.type == :tool_use
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 4096,
messages: messages,
tools: tools
)
end
end
```
stop_sequence
Claude가 사용자가 설정한 커스텀 중단 시퀀스 중 하나를 만났어요.
ant messages create \
--model claude-opus-5-5 \
--max-tokens 1024 \
--stop-sequence END --stop-sequence STOP \
--message '{role: user, content: "Generate text until you say END"}' \
--format json | jq '{stop_reason, stop_sequence}'
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
stop_sequences=["END", "STOP"],
messages=[{"role": "user", "content": "Generate text until you say END"}],
)
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
stop_sequences: ["END", "STOP"],
messages: [{ role: "user", content: "Generate text until you say END" }]
});
if (response.stop_reason === "stop_sequence") {
console.log(`Stopped at sequence: ${response.stop_sequence}`);
}
AnthropicClient client = new();
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
StopSequences = ["END", "STOP"],
Messages = [new() { Role = Role.User, Content = "Generate text until you say END" }]
});
if (response.StopReason == "stop_sequence")
{
Console.WriteLine($"Stopped at sequence: {response.StopSequence}");
}
client := anthropic.NewClient()
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
StopSequences: []string{"END", "STOP"},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Generate text until you say END")),
},
})
if err != nil {
log.Fatal(err)
}
if response.StopReason == "stop_sequence" {
fmt.Printf("Stopped at sequence: %s\n", response.StopSequence)
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
Message response = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addStopSequence("END")
.addStopSequence("STOP")
.addUserMessage("Generate text until you say END")
.build()
);
if (response.stopReason().map(StopReason.STOP_SEQUENCE::equals).orElse(false)) {
IO.println("Stopped at sequence: " + response.stopSequence().orElse(""));
}
$client = new Client();
$response = $client->messages->create(
maxTokens: 1024,
messages: [['role' => 'user', 'content' => 'Generate text until you say END']],
model: 'claude-opus-5-5',
stopSequences: ['END', 'STOP'],
);
if ($response->stopReason === 'stop_sequence') {
echo "Stopped at sequence: {$response->stopSequence}", PHP_EOL;
}
client = Anthropic::Client.new
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
stop_sequences: ["END", "STOP"],
messages: [{ role: "user", content: "Generate text until you say END" }]
)
if response.stop_reason == :stop_sequence
puts "Stopped at sequence: #{response.stop_sequence}"
end
tool_use
Claude가 도구를 호출하고 있고, 사용자가 그 도구를 실행하길 기대하고 있어요.
참고: 대부분의 도구 사용 구현에서는 tool runner를 쓰세요. 도구 실행, 결과 포맷팅, 대화 관리를 자동으로 처리해줘요.
ant messages create --format json <<'YAML' | jq '.stop_reason, (.content[] | select(.type == "tool_use"))'
model: claude-opus-5-5
max_tokens: 1024
messages:
- role: user
content: What is the weather in San Francisco?
tools:
- name: get_weather
description: Get the current weather in a given location
input_schema:
type: object
properties:
location: {type: string, description: City and state}
required: [location]
YAML
client = anthropic.Anthropic()
weather_tool = {
"name": "get_weather",
"description": "Get the current weather in a given location",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City and state"},
},
"required": ["location"],
},
}
def execute_tool(name, tool_input):
"""Execute a tool and return the result."""
return f"Weather in {tool_input.get('location', 'unknown')}: 72°F"
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=[weather_tool],
messages=[{"role": "user", "content": "What is the weather in San Francisco?"}],
)
if response.stop_reason == "tool_use":
# Extract and execute the tool
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
# Return result to Claude for final response
const client = new Anthropic();
const weatherTool: Anthropic.Tool = {
name: "get_weather",
description: "Get the current weather in a given location",
input_schema: {
type: "object",
properties: {
location: { type: "string", description: "City and state" }
},
required: ["location"]
}
};
function executeTool(name: string, input: Record<string, string>): string {
return `Weather in ${input.location ?? "unknown"}: 72°F`;
}
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
tools: [weatherTool],
messages: [{ role: "user", content: "What is the weather in San Francisco?" }]
});
if (response.stop_reason === "tool_use") {
// Extract and execute the tool
for (const block of response.content) {
if (block.type === "tool_use") {
const result = executeTool(block.name, block.input as Record<string, string>);
// Return result to Claude for final response
}
}
}
AnthropicClient client = new();
var weatherTool = new Tool
{
Name = "get_weather",
Description = "Get the current weather in a given location",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["location"] = JsonSerializer.SerializeToElement(
new { type = "string", description = "City and state" }
),
},
Required = ["location"]
}
};
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Tools = [weatherTool],
Messages = [new() { Role = Role.User, Content = "What is the weather in San Francisco?" }]
});
if (response.StopReason == "tool_use")
{
// Extract and execute the tool
foreach (var block in response.Content)
{
if (block.TryPickToolUse(out var toolUse))
{
// Execute toolUse.Name with toolUse.Input and return the result to Claude
}
}
}
client := anthropic.NewClient()
weatherTool := anthropic.ToolParam{
Name: "get_weather",
Description: anthropic.String("Get the current weather in a given location"),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"location": map[string]string{"type": "string", "description": "City and state"},
},
Required: []string{"location"},
},
}
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Tools: []anthropic.ToolUnionParam{{OfTool: &weatherTool}},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("What is the weather in San Francisco?")),
},
})
if err != nil {
log.Fatal(err)
}
if response.StopReason == "tool_use" {
// Extract and execute the tool
for _, block := range response.Content {
if toolUse, ok := block.AsAny().(anthropic.ToolUseBlock); ok {
fmt.Println(toolUse.Name, toolUse.Input)
// Return result to Claude for final response
}
}
}
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
Tool weatherTool = Tool.builder()
.name("get_weather")
.description("Get the current weather in a given location")
.inputSchema(Tool.InputSchema.builder()
.properties(JsonValue.from(Map.of(
"location", Map.of("type", "string", "description", "City and state")
)))
.putAdditionalProperty("required", JsonValue.from(List.of("location")))
.build())
.build();
Message response = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addTool(weatherTool)
.addUserMessage("What is the weather in San Francisco?")
.build()
);
if (response.stopReason().map(StopReason.TOOL_USE::equals).orElse(false)) {
// Extract and execute the tool
for (ContentBlock block : response.content()) {
block.toolUse().ifPresent(toolUse -> {
// Execute toolUse.name() with toolUse.input() and return the result to Claude
});
}
}
$client = new Client();
$weatherTool = [
'name' => 'get_weather',
'description' => 'Get the current weather in a given location',
'input_schema' => [
'type' => 'object',
'properties' => [
'location' => ['type' => 'string', 'description' => 'City and state'],
],
'required' => ['location'],
],
];
$response = $client->messages->create(
maxTokens: 1024,
messages: [['role' => 'user', 'content' => 'What is the weather in San Francisco?']],
model: 'claude-opus-5-5',
tools: [$weatherTool],
);
if ($response->stopReason === 'tool_use') {
// Extract and execute the tool
foreach ($response->content as $block) {
if ($block->type === 'tool_use') {
// Execute $block->name with $block->input and return the result to Claude
}
}
}
client = Anthropic::Client.new
weather_tool = {
name: "get_weather",
description: "Get the current weather in a given location",
input_schema: {
type: "object",
properties: {
location: { type: "string", description: "City and state" }
},
required: ["location"]
}
}
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
tools: [weather_tool],
messages: [{ role: "user", content: "What is the weather in San Francisco?" }]
)
if response.stop_reason == :tool_use
# Extract and execute the tool
response.content.each do |block|
next unless block.type == :tool_use
# Execute block.name with block.input and return the result to Claude
end
end
tool_use 응답에는 결과 블록과 짝이 맞지 않는 id를 가진 server_tool_use 블록이 포함될 수도 있어요. 그 서버 도구 호출은 아직 끝나지 않았고, 이 응답에는 그 결과가 실려 있지 않아요. 흔한 경우로, Claude가 서버 도구 하나와 클라이언트 도구 하나를 같은 병렬 도구 호출 그룹 안에서 호출할 때, API는 서버 도구를 실행하지 않고 응답을 반환해요. 그래야 사용자가 먼저 클라이언트 도구를 실행할 수 있거든요. 이 상태를 알려주는 별도 표시는 없어요. 각 server_tool_use 또는 mcp_tool_use 블록의 id에 짝이 맞는 결과 블록이 있는지 확인해서 감지하면 돼요.
참고: 프로그래매틱 도구 호출에서는 같은 응답 형태가 다른 의미를 가져요. 클라이언트
tool_use블록은 Claude가 직접 만든 게 아니라code_execution도구 안에서 실행 중인 코드에서 만들어진 거고,caller필드가 그 코드를 호출한code_execution블록을 가리켜요. 그 코드는 이미 시작됐고, 사용자의tool_result블록을 기다리며 일시 중지된 상태예요. 결과 블록을 보내면 지연된 도구를 시작하는 대신 실행을 재개해요.code_execution블록 자체의 결과 블록은 코드가 끝나면 도착하는데, 이는 여러 라운드의 도구 결과가 걸릴 수 있어요. 두 경우 모두 후속 사용자 메시지 자체는 같아요. 프로그래매틱 도구 호출에서는 응답의container필드에서id도 함께 돌려보내야 하며, 그 페이지에 그 방법이 나와 있어요.
{
"stop_reason": "tool_use",
"content": [
{
"type": "server_tool_use",
"id": "srvtoolu_01HxbWnMRmbWyMfUtJKC45rA",
"name": "web_search",
"input": { "query": "example article" }
},
{
"type": "tool_use",
"id": "toolu_01PjgRJLbXrXEMZwDNYLnBqk",
"name": "run_command",
"input": { "command": "uname -a" }
}
]
}
연속(continuation)은 응답의 모든 tool_use 블록 하나당 하나씩, tool_result 블록만으로 이루어진 사용자 메시지예요(도구 호출 처리 참고). 두 가지 추가 규칙이 있어요: 그 메시지에는 tool_result 블록 외에 아무것도 들어있으면 안 되고, 요청은 같은 tools 배열을 유지해야 해요. 대기 중인 서버 도구를 더 이상 정의하지 않는 재개 요청은 web_search 툴이 제공되지 않았다는 but no web_search tool was provided 메시지로 끝나는 400 오류로 실패해요. API는 사용자가 보낸 결과를 아직 열려 있는 어시스턴트 턴에 붙이고, 지연된 서버 도구를 실행한 뒤(일시 중지된 코드 실행이라면 재개하고) 턴을 계속 진행해요. Claude가 직접 호출한 서버 도구의 경우 다음 응답의 content는 이전 응답의 server_tool_use id에 답하는 결과 블록으로 시작해요.
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01PjgRJLbXrXEMZwDNYLnBqk",
"content": "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux"
}
]
}
그 사용자 메시지에서 tool_result 블록 뒤에 텍스트 같은 걸 추가하면 어시스턴트 턴이 끝나버려요. Claude가 직접 호출한 서버 도구라면, 요청은 해결되지 않은 서버 도구를 이름으로 지목하는 400 invalid_request_error로 실패해요:
`web_search` tool use with id `srvtoolu_01HxbWnMRmbWyMfUtJKC45rA` was found without a corresponding `web_search_tool_result` block
tool_result를 빠뜨리거나 다른 콘텐츠 뒤에 넣으면, 더 일찍 표준 tool_use ids were found without tool_result blocks immediately after 오류로 실패해요. Claude에 입력을 더 주고 싶다면, 턴이 끝난 뒤 별도의 사용자 메시지로 보내면 돼요.
pause_turn
서버 도구(예: 웹 검색)를 실행하는 동안 서버 측 샘플링 루프가 반복 한도에 도달하면 반환돼요. 기본 한도는 요청당 10회 반복이에요.
이런 일이 생기면 응답에 대응하는 결과 블록이 없는 server_tool_use 블록이 포함될 수 있어요. Claude가 처리를 마치게 하려면 응답을 그대로 다시 보내서 대화를 이어가면 돼요. 클라이언트 tool_use 블록이 사용자를 기다리며 남아 있는 응답은 pause_turn의 stop_reason을 가질 리가 없어요. Claude가 사용자의 도구를 호출하려고 멈출 때 stop_reason은 tool_use이고, 응답 자체 대신 클라이언트 tool_result 블록을 보내서 이어가야 해요.
# Inspect stop_reason; if it is pause_turn, re-run with the assistant
# response appended to --message.
ant messages create --format json <<'YAML' | jq '{stop_reason, content}'
model: claude-opus-5-5
max_tokens: 4096
tools:
- {type: web_search_20250305, name: web_search}
messages:
- {role: user, content: "Search for latest AI news"}
YAML
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=4096,
tools=[{"type": "web_search_20250305", "name": "web_search"}],
messages=[{"role": "user", "content": "Search for latest AI news"}],
)
if response.stop_reason == "pause_turn":
# Continue the conversation by sending the response back
messages = [
{"role": "user", "content": "Search for latest AI news"},
{"role": "assistant", "content": response.content},
]
continuation = client.messages.create(
model="claude-opus-5-5",
max_tokens=4096,
messages=messages,
tools=[{"type": "web_search_20250305", "name": "web_search"}],
)
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 4096,
tools: [{ type: "web_search_20250305", name: "web_search" }],
messages: [{ role: "user", content: "Search for latest AI news" }]
});
if (response.stop_reason === "pause_turn") {
// Continue the conversation by sending the response back
const continuation = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 4096,
tools: [{ type: "web_search_20250305", name: "web_search" }],
messages: [
{ role: "user", content: "Search for latest AI news" },
{ role: "assistant", content: response.content }
]
});
}
List<ToolUnion> tools = [new ToolUnion(new WebSearchTool20250305())];
MessageParam userMessage = new() { Role = Role.User, Content = "Search for latest AI news" };
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 4096,
Tools = tools,
Messages = [userMessage]
});
if (response.StopReason == "pause_turn")
{
// Continue the conversation by sending the response back
var continuation = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 4096,
Tools = tools,
Messages =
[
userMessage,
new()
{
Role = Role.Assistant,
Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList()
}
]
});
}
tools := []anthropic.ToolUnionParam{
{OfWebSearchTool20250305: &anthropic.WebSearchTool20250305Param{}},
}
userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock("Search for latest AI news"))
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 4096,
Tools: tools,
Messages: []anthropic.MessageParam{userMessage},
})
if err != nil {
log.Fatal(err)
}
if response.StopReason == "pause_turn" {
// Continue the conversation by sending the response back
var contentParams []anthropic.ContentBlockParamUnion
for _, block := range response.Content {
contentParams = append(contentParams, block.ToParam())
}
continuation, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 4096,
Tools: tools,
Messages: []anthropic.MessageParam{userMessage, anthropic.NewAssistantMessage(contentParams...)},
})
if err != nil {
log.Fatal(err)
}
_ = continuation
}
Message response = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(4096L)
.addTool(WebSearchTool20250305.builder().build())
.addUserMessage("Search for latest AI news")
.build()
);
if (response.stopReason().map(StopReason.PAUSE_TURN::equals).orElse(false)) {
// Continue the conversation by sending the response back
Message continuation = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(4096L)
.addTool(WebSearchTool20250305.builder().build())
.addUserMessage("Search for latest AI news")
.addMessage(response)
.build()
);
}
$tools = [['type' => 'web_search_20250305', 'name' => 'web_search']];
$userMessage = ['role' => 'user', 'content' => 'Search for latest AI news'];
$response = $client->messages->create(
maxTokens: 4096,
messages: [$userMessage],
model: 'claude-opus-5-5',
tools: $tools,
);
if ($response->stopReason === 'pause_turn') {
// Continue the conversation by sending the response back
$continuation = $client->messages->create(
maxTokens: 4096,
messages: [
$userMessage,
['role' => 'assistant', 'content' => $response->content],
],
model: 'claude-opus-5-5',
tools: $tools,
);
}
tools = [{ type: "web_search_20250305", name: "web_search" }]
user_message = { role: "user", content: "Search for latest AI news" }
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 4096,
tools: tools,
messages: [user_message]
)
if response.stop_reason == :pause_turn
# Continue the conversation by sending the response back
continuation = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 4096,
tools: tools,
messages: [user_message, { role: "assistant", content: response.content }]
)
end
참고: 서버 도구를 사용하는 모든 에이전트 루프에서
pause_turn을 처리해야 해요. 어시스턴트의 응답을 messages 배열에 추가하고 API 요청을 한 번 더 보내서 Claude가 계속하도록 하세요.
refusal
Claude가 응답 생성을 거부했어요. 안전 분류기가 이 중단 사유를 오류가 아닌 정상 HTTP 200 응답으로 반환해요.
ant messages create \
--model claude-opus-5-5 \
--max-tokens 1024 \
--message '{role: user, content: "[Unsafe request]"}' \
--format json | jq '{stop_reason, stop_details}'
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "[Unsafe request]"}],
)
if response.stop_reason == "refusal":
# Claude declined to respond
print("Claude was unable to process this request")
# Consider rephrasing or modifying the request
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "[Unsafe request]" }]
});
if (response.stop_reason === "refusal") {
// Claude declined to respond
console.log("Claude was unable to process this request");
// Consider rephrasing or modifying the request
}
AnthropicClient client = new();
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = [new() { Role = Role.User, Content = "[Unsafe request]" }]
});
if (response.StopReason == "refusal")
{
// Claude declined to respond
Console.WriteLine("Claude was unable to process this request");
// Consider rephrasing or modifying the request
}
client := anthropic.NewClient()
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("[Unsafe request]")),
},
})
if err != nil {
log.Fatal(err)
}
if response.StopReason == "refusal" {
// Claude declined to respond
fmt.Println("Claude was unable to process this request")
// Consider rephrasing or modifying the request
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
Message response = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addUserMessage("[Unsafe request]")
.build()
);
if (response.stopReason().map(StopReason.REFUSAL::equals).orElse(false)) {
// Claude declined to respond
IO.println("Claude was unable to process this request");
// Consider rephrasing or modifying the request
}
$client = new Client();
$response = $client->messages->create(
maxTokens: 1024,
messages: [['role' => 'user', 'content' => '[Unsafe request]']],
model: 'claude-opus-5-5',
);
if ($response->stopReason === 'refusal') {
// Claude declined to respond
echo 'Claude was unable to process this request', PHP_EOL;
// Consider rephrasing or modifying the request
}
client = Anthropic::Client.new
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "[Unsafe request]" }]
)
if response.stop_reason == :refusal
# Claude declined to respond
puts "Claude was unable to process this request"
# Consider rephrasing or modifying the request
end
팁: Claude Sonnet 4.5나 Claude Opus 4.1(후자는 Bedrock과 Google Cloud를 제외하고 퇴역)을 쓰는 동안
refusal중단 사유를 자주 마주친다면, API 호출을 사용 제한이 다른 Haiku 4.5(claude-haiku-4-5-20251001)로 바꿔보세요. Sonnet 4.5의 API 안전 필터 이해하기에서 더 자세히 알아볼 수 있어요.
거부가 발생하면 stop_details 객체가 그 원인이 된 정책 범주를 식별해줘요. 범주와 전체 거부 응답 형태는 Refusals and fallback에서 다뤄요. stop_details는 refusal이 아닌 모든 중단 사유에서 null이에요.
Claude Fable 5.1, Claude Fable 5, Claude Opus 5.5, Claude Opus 5에서 거부된 요청은 보통 다른 Claude 모델로 재시도하면 처리할 수 있어요. Refusals and fallback에서 서버 측 또는 클라이언트에서 그 재시도를 설정하는 방법을 보여줘요. Claude Fable 5.1, Claude Fable 5, Claude Opus 5.5, Claude Opus 5부터 직접 재시도를 만든다면, fallback credit에서 프롬프트 캐시 비용을 두 번 내지 않는 방법을 다뤄요.
model_context_window_exceeded
Claude가 모델의 컨텍스트 윈도우 한도에 도달해서 멈췄어요. 이러면 정확한 입력 크기를 몰라도 최대한 많은 토큰을 요청할 수 있어요.
참고: 이 중단 사유는 현재 SDK의
beta네임스페이스에만 타입이 정의되어 있어서, 아래 예시들은client.beta.messages를 호출하고Beta접두사가 붙은 타입을 사용해요. Sonnet 4.5 이상 모델에서는 API가 beta 헤더 없이 이 값을 반환해요. 이전 모델에서는 이걸 활성화하려면model-context-window-exceeded-2025-08-26beta 헤더를 추가해야 해요.
ant messages create \
--model claude-opus-5-5 \
--max-tokens 20000 \
--message '{role: user, content: "Large input that uses most of context window..."}' \
--format json | jq '.stop_reason'
# Request with maximum tokens to get as much as possible
response = client.beta.messages.create(
model="claude-opus-5-5",
max_tokens=20000, # Python SDK requires streaming for max_tokens above ~21k
messages=[
{"role": "user", "content": "Large input that uses most of context window..."}
],
)
if response.stop_reason == "model_context_window_exceeded":
# Response hit context window limit before max_tokens
print("Response reached model's context window limit")
# The response is still valid but was limited by context window
// Request with maximum tokens to get as much as possible
const response = await client.beta.messages.create({
model: "claude-opus-5-5",
max_tokens: 20000,
messages: [{ role: "user", content: "Large input that uses most of context window..." }]
});
if (response.stop_reason === "model_context_window_exceeded") {
// Response hit context window limit before max_tokens
console.log("Response reached model's context window limit");
// The response is still valid but was limited by context window
}
using Anthropic.Models.Beta.Messages;
using Model = Anthropic.Models.Messages.Model;
// Request with maximum tokens to get as much as possible
var response = await client.Beta.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 20000,
Messages = [new() { Role = Role.User, Content = "Large input that uses most of context window..." }]
});
if (response.StopReason?.Value() == BetaStopReason.ModelContextWindowExceeded)
{
// Response hit context window limit before max_tokens
Console.WriteLine("Response reached model's context window limit");
// The response is still valid but was limited by context window
}
// Request with maximum tokens to get as much as possible
response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 20000,
Messages: []anthropic.BetaMessageParam{
anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Large input that uses most of context window...")),
},
})
if err != nil {
log.Fatal(err)
}
if response.StopReason == anthropic.BetaStopReasonModelContextWindowExceeded {
// Response hit context window limit before max_tokens
fmt.Println("Response reached model's context window limit")
// The response is still valid but was limited by context window
}
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaStopReason;
import com.anthropic.models.beta.messages.MessageCreateParams;
// Request with maximum tokens to get as much as possible
BetaMessage response = client.beta().messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(20000L)
.addUserMessage("Large input that uses most of context window...")
.build()
);
if (response.stopReason().map(BetaStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED::equals).orElse(false)) {
// Response hit context window limit before max_tokens
IO.println("Response reached model's context window limit");
// The response is still valid but was limited by context window
}
// Request with maximum tokens to get as much as possible
$response = $client->beta->messages->create(
maxTokens: 20000,
messages: [['role' => 'user', 'content' => 'Large input that uses most of context window...']],
model: 'claude-opus-5-5',
);
if ($response->stopReason === 'model_context_window_exceeded') {
// Response hit context window limit before max_tokens
echo 'Response reached model\'s context window limit', PHP_EOL;
// The response is still valid but was limited by context window
}
# Request with maximum tokens to get as much as possible
response = client.beta.messages.create(
model: "claude-opus-5-5",
max_tokens: 20000,
messages: [{ role: "user", content: "Large input that uses most of context window..." }]
)
if response.stop_reason == :model_context_window_exceeded
# Response hit context window limit before max_tokens
puts "Response reached model's context window limit"
# The response is still valid but was limited by context window
end
stop reason을 처리하는 모범 사례
항상 stop_reason을 확인하세요
응답 처리 로직에서 항상 stop_reason을 확인하는 습관을 들이세요:
function handleResponse(response: Anthropic.Beta.BetaMessage): string {
switch (response.stop_reason) {
case "tool_use":
return handleToolUse(response);
case "max_tokens":
return handleTruncation(response);
case "model_context_window_exceeded":
return handleContextLimit(response);
case "pause_turn":
return handlePause(response);
case "refusal":
return handleRefusal(response);
default: {
// Handle end_turn and other cases
const textBlock = response.content.find(
(block): block is Anthropic.Beta.BetaTextBlock => block.type === "text"
);
return textBlock?.text ?? "";
}
}
}
static string HandleResponse(BetaMessage response)
{
return response.StopReason?.Value() switch
{
BetaStopReason.ToolUse => HandleToolUse(response),
BetaStopReason.MaxTokens => HandleTruncation(response),
BetaStopReason.ModelContextWindowExceeded => HandleContextLimit(response),
BetaStopReason.PauseTurn => HandlePause(response),
BetaStopReason.Refusal => HandleRefusal(response),
// Handle end_turn and other cases
_ => response.Content.Select(b => b.Value).OfType<BetaTextBlock>().FirstOrDefault()?.Text ?? "",
};
}
func handleResponse(response *anthropic.BetaMessage) string {
switch response.StopReason {
case anthropic.BetaStopReasonToolUse:
return handleToolUse(response)
case anthropic.BetaStopReasonMaxTokens:
return handleTruncation(response)
case anthropic.BetaStopReasonModelContextWindowExceeded:
return handleContextLimit(response)
case anthropic.BetaStopReasonPauseTurn:
return handlePause(response)
case anthropic.BetaStopReasonRefusal:
return handleRefusal(response)
default:
// Handle end_turn and other cases
for _, block := range response.Content {
if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok {
return textBlock.Text
}
}
return ""
}
}
static String handleResponse(BetaMessage response) {
BetaStopReason reason = response.stopReason().orElse(BetaStopReason.END_TURN);
if (reason.equals(BetaStopReason.TOOL_USE)) {
return handleToolUse(response);
} else if (reason.equals(BetaStopReason.MAX_TOKENS)) {
return handleTruncation(response);
} else if (reason.equals(BetaStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED)) {
return handleContextLimit(response);
} else if (reason.equals(BetaStopReason.PAUSE_TURN)) {
return handlePause(response);
} else if (reason.equals(BetaStopReason.REFUSAL)) {
return handleRefusal(response);
}
// Handle end_turn and other cases
return response.content().stream()
.filter(BetaContentBlock::isText)
.findFirst()
.map(block -> block.asText().text())
.orElse("");
}
function handle_response($response): string
{
return match ($response->stopReason) {
'tool_use' => handle_tool_use($response),
'max_tokens' => handle_truncation($response),
'model_context_window_exceeded' => handle_context_limit($response),
'pause_turn' => handle_pause($response),
'refusal' => handle_refusal($response),
// Handle end_turn and other cases
default => array_find($response->content, static fn ($block): bool => $block->type === 'text')?->text ?? '',
};
}
def handle_response(response)
case response.stop_reason
when :tool_use then handle_tool_use(response)
when :max_tokens then handle_truncation(response)
when :model_context_window_exceeded then handle_context_limit(response)
when :pause_turn then handle_pause(response)
when :refusal then handle_refusal(response)
else
# Handle end_turn and other cases
response.content.find { it.type == :text }&.text
end
end
잘린 응답을 자연스럽게 처리하기
토큰 한도나 컨텍스트 윈도우 때문에 응답이 잘렸다면, 출력이 불완전하다는 걸 알리는 안내를 붙여서 읽는 사람이 알게 하세요. 응답이 끊긴 지점부터 계속 생성하려면 Ensuring complete responses를 보세요.
function handleTruncatedResponse(response: Anthropic.Beta.BetaMessage): string {
const textBlock = response.content.find(
(block): block is Anthropic.Beta.BetaTextBlock => block.type === "text"
);
const text = textBlock?.text ?? "";
if (
response.stop_reason === "max_tokens" ||
response.stop_reason === "model_context_window_exceeded"
) {
const note =
response.stop_reason === "max_tokens"
? "[Response truncated due to max_tokens limit]"
: "[Response truncated due to context window limit]";
return `${text}\n\n${note}`;
}
return text;
}
static string HandleTruncatedResponse(BetaMessage response)
{
var text = response.Content.Select(b => b.Value).OfType<BetaTextBlock>().FirstOrDefault()?.Text ?? "";
var reason = response.StopReason?.Value();
if (reason is BetaStopReason.MaxTokens or BetaStopReason.ModelContextWindowExceeded)
{
var note = reason == BetaStopReason.MaxTokens
? "[Response truncated due to max_tokens limit]"
: "[Response truncated due to context window limit]";
return $"{text}\n\n{note}";
}
return text;
}
func handleTruncatedResponse(response *anthropic.BetaMessage) string {
text := ""
for _, block := range response.Content {
if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok {
text = textBlock.Text
break
}
}
if response.StopReason == anthropic.BetaStopReasonMaxTokens ||
response.StopReason == anthropic.BetaStopReasonModelContextWindowExceeded {
note := "[Response truncated due to context window limit]"
if response.StopReason == anthropic.BetaStopReasonMaxTokens {
note = "[Response truncated due to max_tokens limit]"
}
return text + "\n\n" + note
}
return text
}
static String handleTruncatedResponse(BetaMessage response) {
String text = response.content().stream()
.filter(BetaContentBlock::isText)
.findFirst()
.map(block -> block.asText().text())
.orElse("");
BetaStopReason reason = response.stopReason().orElse(BetaStopReason.END_TURN);
if (reason.equals(BetaStopReason.MAX_TOKENS)
|| reason.equals(BetaStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED)) {
String note = reason.equals(BetaStopReason.MAX_TOKENS)
? "[Response truncated due to max_tokens limit]"
: "[Response truncated due to context window limit]";
return text + "\n\n" + note;
}
return text;
}
function handle_truncated_response($response): string
{
$text = array_find($response->content, static fn ($block): bool => $block->type === 'text')?->text ?? '';
if (in_array($response->stopReason, ['max_tokens', 'model_context_window_exceeded'], true)) {
$note = $response->stopReason === 'max_tokens'
? '[Response truncated due to max_tokens limit]'
: '[Response truncated due to context window limit]';
return "{$text}\n\n{$note}";
}
return $text;
}
def handle_truncated_response(response)
text = response.content.find { it.type == :text }&.text
if [:max_tokens, :model_context_window_exceeded].include?(response.stop_reason)
note = if response.stop_reason == :max_tokens
"[Response truncated due to max_tokens limit]"
else
"[Response truncated due to context window limit]"
end
return "#{text}\n\n#{note}"
end
text
end
pause_turn용 재시도 로직 구현하기
서버 도구를 사용할 때, 서버 측 샘플링 루프가 반복 한도(기본 10)에 도달하면 API가 pause_turn을 반환할 수 있어요. 대화를 계속해서 처리하면 돼요:
The server runs a sampling loop when executing server tools. If the loop
reaches its iteration limit, the API returns pause_turn. Continue the
conversation by sending the response back to let Claude finish.
"""
messages = [{"role": "user", "content": user_query}]
for _ in range(max_continuations):
response = client.messages.create(
model="claude-opus-5-5", max_tokens=4096, messages=messages, tools=tools
)
if response.stop_reason != "pause_turn":
# Claude finished processing - return the final response
return response
# pause_turn: replace the full message list to maintain alternating roles
messages = [
{"role": "user", "content": user_query},
{"role": "assistant", "content": response.content},
]
# Reached max continuations - return the last response
return response
```typescript TypeScript
async function handleServerToolConversation(
client: Anthropic,
userQuery: string,
tools: Anthropic.ToolUnion[],
maxContinuations = 5
): Promise<Anthropic.Message> {
let messages: Anthropic.MessageParam[] = [{ role: "user", content: userQuery }];
let response: Anthropic.Message;
for (let i = 0; i < maxContinuations; i++) {
response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 4096,
messages,
tools
});
if (response.stop_reason !== "pause_turn") {
// Claude finished processing - return the final response
return response;
}
// pause_turn: replace the full message list to maintain alternating roles
messages = [
{ role: "user", content: userQuery },
{ role: "assistant", content: response.content }
];
}
// Reached max continuations - return the last response
return response!;
}
static async Task<Message> HandleServerToolConversation(
AnthropicClient client,
string userQuery,
List<ToolUnion> tools,
int maxContinuations = 5)
{
List<MessageParam> messages = [new() { Role = Role.User, Content = userQuery }];
Message response = null!;
for (var i = 0; i < maxContinuations; i++)
{
response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 4096,
Messages = messages,
Tools = tools
});
if (response.StopReason != "pause_turn")
{
// Claude finished processing - return the final response
return response;
}
// pause_turn: replace the full message list to maintain alternating roles
messages =
[
new() { Role = Role.User, Content = userQuery },
new()
{
Role = Role.Assistant,
Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList()
}
];
}
// Reached max continuations - return the last response
return response;
}
func handleServerToolConversation(
client anthropic.Client,
userQuery string,
tools []anthropic.ToolUnionParam,
maxContinuations int,
) (*anthropic.Message, error) {
messages := []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock(userQuery))}
var response *anthropic.Message
var err error
for range maxContinuations {
response, err = client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 4096,
Messages: messages,
Tools: tools,
})
if err != nil {
return nil, err
}
if response.StopReason != "pause_turn" {
// Claude finished processing - return the final response
return response, nil
}
// pause_turn: replace the full message list to maintain alternating roles
var contentParams []anthropic.ContentBlockParamUnion
for _, block := range response.Content {
contentParams = append(contentParams, block.ToParam())
}
messages = []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(userQuery)),
anthropic.NewAssistantMessage(contentParams...),
}
}
// Reached max continuations - return the last response
return response, nil
}
static Message handleServerToolConversation(
AnthropicClient client,
String userQuery,
List<Tool> tools,
int maxContinuations
) {
Message response = null;
for (int i = 0; i < maxContinuations; i++) {
// Rebuild the params each iteration so messages aren't accumulated
MessageCreateParams.Builder params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(4096L)
.addUserMessage(userQuery);
tools.forEach(params::addTool);
if (response != null) {
params.addMessage(response);
}
response = client.messages().create(params.build());
if (!response.stopReason().map(StopReason.PAUSE_TURN::equals).orElse(false)) {
// Claude finished processing - return the final response
return response;
}
// pause_turn: loop again and send the response back
}
// Reached max continuations - return the last response
return response;
}
function handle_server_tool_conversation(
Client $client,
string $userQuery,
array $tools,
int $maxContinuations = 5
) {
$messages = [['role' => 'user', 'content' => $userQuery]];
$response = null;
for ($i = 0; $i < $maxContinuations; $i++) {
$response = $client->messages->create(
maxTokens: 4096,
messages: $messages,
model: 'claude-opus-5-5',
tools: $tools,
);
if ($response->stopReason !== 'pause_turn') {
// Claude finished processing - return the final response
return $response;
}
// pause_turn: replace the full message list to maintain alternating roles
$messages = [
['role' => 'user', 'content' => $userQuery],
['role' => 'assistant', 'content' => $response->content],
];
}
// Reached max continuations - return the last response
return $response;
}
def handle_server_tool_conversation(client, user_query, tools, max_continuations: 5)
messages = [{ role: "user", content: user_query }]
response = nil
max_continuations.times do
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 4096,
messages: messages,
tools: tools
)
# Claude finished processing - return the final response
return response unless response.stop_reason == :pause_turn
# pause_turn: replace the full message list to maintain alternating roles
messages = [
{ role: "user", content: user_query },
{ role: "assistant", content: response.content }
]
end
# Reached max continuations - return the last response
response
end
중단 사유 vs. 오류
stop_reason 값과 실제 오류를 구분하는 게 중요해요:
중단 사유 (성공한 응답)
- 응답 본문의 일부
- 생성이 왜 정상적으로 멈췄는지 알려줘요
- 응답에 유효한 콘텐츠가 들어 있어요
오류 (실패한 요청)
- HTTP 상태 코드 4xx 또는 5xx
- 요청 처리 실패를 나타내요
- 응답에 오류 정보가 들어 있어요
# The CLI exits non-zero on API errors; stop_reason appears on success.
ant messages create \
--model claude-opus-5-5 \
--max-tokens 1024 \
--message '{role: user, content: "Hello!"}' \
--format json | jq '.stop_reason'
client = anthropic.Anthropic()
try:
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
)
# Handle successful response with stop_reason
if response.stop_reason == "max_tokens":
print("Response was truncated")
except anthropic.APIStatusError as e:
# Handle actual errors
match e.status_code:
case 429:
print("Rate limit exceeded")
case 500:
print("Server error")
const client = new Anthropic();
try {
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }]
});
// Handle successful response with stop_reason
if (response.stop_reason === "max_tokens") {
console.log("Response was truncated");
}
} catch (err) {
// Handle actual errors
if (err instanceof Anthropic.APIError) {
switch (err.status) {
case 429:
console.log("Rate limit exceeded");
break;
case 500:
console.log("Server error");
break;
}
} else {
throw err;
}
}
AnthropicClient client = new();
try
{
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = [new() { Role = Role.User, Content = "Hello!" }]
});
// Handle successful response with stop_reason
if (response.StopReason == "max_tokens")
{
Console.WriteLine("Response was truncated");
}
}
catch (AnthropicRateLimitException)
{
// Handle actual errors
Console.WriteLine("Rate limit exceeded");
}
catch (Anthropic5xxException)
{
Console.WriteLine("Server error");
}
client := anthropic.NewClient()
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Hello!")),
},
})
if err != nil {
// Handle actual errors
var apiErr *anthropic.Error
if errors.As(err, &apiErr) {
switch apiErr.StatusCode {
case 429:
fmt.Println("Rate limit exceeded")
case 500:
fmt.Println("Server error")
}
}
log.Fatal(err)
}
// Handle successful response with stop_reason
if response.StopReason == "max_tokens" {
fmt.Println("Response was truncated")
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
try {
Message response = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addUserMessage("Hello!")
.build()
);
// Handle successful response with stop_reason
if (response.stopReason().map(StopReason.MAX_TOKENS::equals).orElse(false)) {
IO.println("Response was truncated");
}
} catch (RateLimitException e) {
// Handle actual errors
IO.println("Rate limit exceeded");
} catch (AnthropicServiceException e) {
if (e.statusCode() == 500) {
IO.println("Server error");
}
}
$client = new Client();
try {
$response = $client->messages->create(
maxTokens: 1024,
messages: [['role' => 'user', 'content' => 'Hello!']],
model: 'claude-opus-5-5',
);
// Handle successful response with stop_reason
if ($response->stopReason === 'max_tokens') {
echo 'Response was truncated', PHP_EOL;
}
} catch (RateLimitException $e) {
// Handle actual errors
echo 'Rate limit exceeded', PHP_EOL;
} catch (InternalServerException $e) {
echo 'Server error', PHP_EOL;
}
client = Anthropic::Client.new
begin
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }]
)
# Handle successful response with stop_reason
if response.stop_reason == :max_tokens
puts "Response was truncated"
end
rescue Anthropic::Errors::RateLimitError
# Handle actual errors
puts "Rate limit exceeded"
rescue Anthropic::Errors::APIStatusError => e
puts "Server error" if e.status == 500
end
스트리밍 고려사항
스트리밍을 사용할 때 stop_reason은:
- 초기
message_start이벤트에서는null message_delta이벤트에서 제공됨- 다른 이벤트에서는 제공되지 않음
# stop_reason appears in the message_delta event.
ant messages create --stream --format jsonl \
--model claude-opus-5-5 \
--max-tokens 1024 \
--message '{role: user, content: "Hello!"}' |
jq -c 'select(.type == "message_delta") | .delta.stop_reason'
client = anthropic.Anthropic()
with client.messages.stream(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
) as stream:
for event in stream:
if event.type == "message_delta":
stop_reason = event.delta.stop_reason
if stop_reason:
print(f"Stream ended with: {stop_reason}")
const client = new Anthropic();
const stream = client.messages.stream({
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }]
});
for await (const event of stream) {
if (event.type === "message_delta" && event.delta.stop_reason) {
console.log(`Stream ended with: ${event.delta.stop_reason}`);
}
}
AnthropicClient client = new();
var parameters = new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = [new() { Role = Role.User, Content = "Hello!" }]
};
await foreach (var streamEvent in client.Messages.CreateStreaming(parameters))
{
switch (streamEvent.Value)
{
case RawMessageDeltaEvent deltaEvent when deltaEvent.Delta.StopReason is not null:
Console.WriteLine($"Stream ended with: {deltaEvent.Delta.StopReason}");
break;
}
}
client := anthropic.NewClient()
stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Hello!")),
},
})
// Accumulate events into the final Message, which carries stop_reason.
message := anthropic.Message{}
for stream.Next() {
if err := message.Accumulate(stream.Current()); err != nil {
log.Fatal(err)
}
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}
if message.StopReason != "" {
fmt.Printf("Stream ended with: %s\n", message.StopReason)
}
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addUserMessage("Hello!")
.build();
// Accumulate events into the final Message, which carries stop_reason.
MessageAccumulator accumulator = MessageAccumulator.create();
try (StreamResponse<RawMessageStreamEvent> streamResponse =
client.messages().createStreaming(params)) {
streamResponse.stream().forEach(accumulator::accumulate);
}
accumulator.message().stopReason().ifPresent(stopReason ->
IO.println("Stream ended with: " + stopReason)
);
$client = new Client();
$stream = $client->messages->createStream(
maxTokens: 1024,
messages: [['role' => 'user', 'content' => 'Hello!']],
model: 'claude-opus-5-5',
);
foreach ($stream as $event) {
if ($event instanceof \Anthropic\Messages\RawMessageDeltaEvent && $event->delta->stopReason !== null) {
echo "Stream ended with: {$event->delta->stopReason}", PHP_EOL;
}
}
client = Anthropic::Client.new
stream = client.messages.stream(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }]
)
stream.each do |event|
next unless event.type == :message_delta
stop_reason = event.delta.stop_reason
puts "Stream ended with: #{stop_reason}" if stop_reason
end
흔한 패턴
도구 사용 워크플로 처리하기
팁: tool runner를 쓰면 훨씬 간단해요: 아래 예시는 수동 도구 처리를 보여줘요. 대부분의 경우 tool runner가 훨씬 적은 코드로 도구 실행을 자동으로 처리해요.
while True:
response = client.messages.create(
model="claude-opus-5-5", max_tokens=1024, messages=messages, tools=tools
)
if response.stop_reason == "tool_use":
# Execute tools and continue
tool_results = execute_tools(response.content)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
else:
# Final response
return response
```typescript TypeScript
async function completeToolWorkflow(
client: Anthropic,
userQuery: string,
tools: Anthropic.ToolUnion[]
): Promise<Anthropic.Message> {
const messages: Anthropic.MessageParam[] = [{ role: "user", content: userQuery }];
while (true) {
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
messages,
tools
});
if (response.stop_reason === "tool_use") {
// Execute tools and continue
const toolResults = executeTools(response.content);
messages.push({ role: "assistant", content: response.content });
messages.push({ role: "user", content: toolResults });
} else {
// Final response
return response;
}
}
}
static async Task<Message> CompleteToolWorkflow(
AnthropicClient client,
string userQuery,
List<ToolUnion> tools)
{
List<MessageParam> messages = [new() { Role = Role.User, Content = userQuery }];
while (true)
{
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = messages,
Tools = tools
});
if (response.StopReason == "tool_use")
{
// Execute tools and continue
var toolResults = ExecuteTools(response.Content);
messages.Add(new()
{
Role = Role.Assistant,
Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList()
});
messages.Add(new() { Role = Role.User, Content = toolResults });
}
else
{
// Final response
return response;
}
}
}
func completeToolWorkflow(
client anthropic.Client,
userQuery string,
tools []anthropic.ToolUnionParam,
) (*anthropic.Message, error) {
messages := []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock(userQuery))}
for {
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: messages,
Tools: tools,
})
if err != nil {
return nil, err
}
if response.StopReason != "tool_use" {
// Final response
return response, nil
}
// Execute tools and continue
toolResults := executeTools(response.Content)
var contentParams []anthropic.ContentBlockParamUnion
for _, block := range response.Content {
contentParams = append(contentParams, block.ToParam())
}
messages = append(messages, anthropic.NewAssistantMessage(contentParams...))
messages = append(messages, anthropic.NewUserMessage(toolResults...))
}
}
static Message completeToolWorkflow(
AnthropicClient client,
String userQuery,
List<Tool> tools
) {
List<MessageParam> messages = new ArrayList<>();
messages.add(MessageParam.builder().role(MessageParam.Role.USER).content(userQuery).build());
while (true) {
MessageCreateParams.Builder params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.messages(messages);
tools.forEach(params::addTool);
Message response = client.messages().create(params.build());
if (!response.stopReason().map(StopReason.TOOL_USE::equals).orElse(false)) {
// Final response
return response;
}
// Execute tools and continue
List<ToolResultBlockParam> toolResults = executeTools(response.content());
messages.add(response.toParam());
messages.add(MessageParam.builder()
.role(MessageParam.Role.USER)
.contentOfBlockParams(toolResults.stream().map(ContentBlockParam::ofToolResult).toList())
.build());
}
}
function complete_tool_workflow(Client $client, string $userQuery, array $tools)
{
$messages = [['role' => 'user', 'content' => $userQuery]];
while (true) {
$response = $client->messages->create(
maxTokens: 1024,
messages: $messages,
model: 'claude-opus-5-5',
tools: $tools,
);
if ($response->stopReason !== 'tool_use') {
// Final response
return $response;
}
// Execute tools and continue
$toolResults = execute_tools($response->content);
$messages[] = ['role' => 'assistant', 'content' => $response->content];
$messages[] = ['role' => 'user', 'content' => $toolResults];
}
}
def complete_tool_workflow(client, user_query, tools)
messages = [{ role: "user", content: user_query }]
loop do
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: messages,
tools: tools
)
# Final response
return response unless response.stop_reason == :tool_use
# Execute tools and continue
tool_results = execute_tools(response.content)
messages << { role: "assistant", content: response.content }
messages << { role: "user", content: tool_results }
end
end
응답을 완전하게 만들기
for _ in range(max_attempts):
response = client.messages.create(
model="claude-opus-5-5", messages=messages, max_tokens=4096
)
full_response += next(
(block.text for block in response.content if block.type == "text"), ""
)
if response.stop_reason != "max_tokens":
break
# Continue from where it left off
messages = [
{"role": "user", "content": prompt},
{"role": "assistant", "content": full_response},
{"role": "user", "content": "Please continue from where you left off."},
]
return full_response
```typescript TypeScript
async function getCompleteResponse(
client: Anthropic,
prompt: string,
maxAttempts = 3
): Promise<string> {
let messages: Anthropic.MessageParam[] = [{ role: "user", content: prompt }];
let fullResponse = "";
for (let i = 0; i < maxAttempts; i++) {
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 4096,
messages
});
const textBlock = response.content.find(
(block): block is Anthropic.TextBlock => block.type === "text"
);
fullResponse += textBlock?.text ?? "";
if (response.stop_reason !== "max_tokens") {
break;
}
// Continue from where it left off
messages = [
{ role: "user", content: prompt },
{ role: "assistant", content: fullResponse },
{ role: "user", content: "Please continue from where you left off." }
];
}
return fullResponse;
}
static async Task<string> GetCompleteResponse(AnthropicClient client, string prompt, int maxAttempts = 3)
{
List<MessageParam> messages = [new() { Role = Role.User, Content = prompt }];
var fullResponse = "";
for (var i = 0; i < maxAttempts; i++)
{
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 4096,
Messages = messages
});
foreach (var block in response.Content)
{
if (block.TryPickText(out var textBlock))
{
fullResponse += textBlock.Text;
break;
}
}
if (response.StopReason != "max_tokens")
{
break;
}
// Continue from where it left off
messages =
[
new() { Role = Role.User, Content = prompt },
new() { Role = Role.Assistant, Content = fullResponse },
new() { Role = Role.User, Content = "Please continue from where you left off." }
];
}
return fullResponse;
}
func getCompleteResponse(client anthropic.Client, prompt string, maxAttempts int) (string, error) {
messages := []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock(prompt))}
fullResponse := ""
for range maxAttempts {
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 4096,
Messages: messages,
})
if err != nil {
return "", err
}
for _, block := range response.Content {
if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
fullResponse += textBlock.Text
break
}
}
if response.StopReason != "max_tokens" {
break
}
// Continue from where it left off
messages = []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
anthropic.NewAssistantMessage(anthropic.NewTextBlock(fullResponse)),
anthropic.NewUserMessage(anthropic.NewTextBlock("Please continue from where you left off.")),
}
}
return fullResponse, nil
}
static String getCompleteResponse(AnthropicClient client, String prompt, int maxAttempts) {
List<MessageParam> messages = List.of(
MessageParam.builder().role(MessageParam.Role.USER).content(prompt).build()
);
StringBuilder fullResponse = new StringBuilder();
for (int i = 0; i < maxAttempts; i++) {
Message response = client.messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(4096L)
.messages(messages)
.build()
);
response.content().stream()
.filter(ContentBlock::isText)
.findFirst()
.ifPresent(block -> fullResponse.append(block.asText().text()));
if (!response.stopReason().map(StopReason.MAX_TOKENS::equals).orElse(false)) {
break;
}
// Continue from where it left off
messages = List.of(
MessageParam.builder().role(MessageParam.Role.USER).content(prompt).build(),
MessageParam.builder().role(MessageParam.Role.ASSISTANT).content(fullResponse.toString()).build(),
MessageParam.builder().role(MessageParam.Role.USER).content("Please continue from where you left off.").build()
);
}
return fullResponse.toString();
}
function get_complete_response(Client $client, string $prompt, int $maxAttempts = 3): string
{
$messages = [['role' => 'user', 'content' => $prompt]];
$fullResponse = '';
for ($i = 0; $i < $maxAttempts; $i++) {
$response = $client->messages->create(
maxTokens: 4096,
messages: $messages,
model: 'claude-opus-5-5',
);
$fullResponse .= array_find($response->content, static fn ($block): bool => $block->type === 'text')?->text ?? '';
if ($response->stopReason !== 'max_tokens') {
break;
}
// Continue from where it left off
$messages = [
['role' => 'user', 'content' => $prompt],
['role' => 'assistant', 'content' => $fullResponse],
['role' => 'user', 'content' => 'Please continue from where you left off.'],
];
}
return $fullResponse;
}
def get_complete_response(client, prompt, max_attempts: 3)
messages = [{ role: "user", content: prompt }]
full_response = +""
max_attempts.times do
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 4096,
messages: messages
)
full_response << response.content.find { it.type == :text }&.text.to_s
break unless response.stop_reason == :max_tokens
# Continue from where it left off
messages = [
{ role: "user", content: prompt },
{ role: "assistant", content: full_response },
{ role: "user", content: "Please continue from where you left off." }
]
end
full_response
end
입력 크기를 몰라도 최대 토큰 얻기
model_context_window_exceeded 중단 사유를 쓰면 입력 크기를 계산하지 않고 가능한 최대 토큰을 요청할 수 있어요:
match response.stop_reason:
case "model_context_window_exceeded":
# Got the maximum possible tokens given input size
print(
f"Generated {response.usage.output_tokens} tokens (context limit reached)"
)
case "max_tokens":
# Got exactly the requested tokens
print(
f"Generated {response.usage.output_tokens} tokens (max_tokens reached)"
)
case _:
# Natural completion
print(
f"Generated {response.usage.output_tokens} tokens (natural completion)"
)
return next((block.text for block in response.content if block.type == "text"), "")
```typescript TypeScript
async function getMaxPossibleTokens(client: Anthropic, prompt: string): Promise<string> {
const response = await client.beta.messages.create({
model: "claude-opus-5-5",
max_tokens: 20000,
messages: [{ role: "user", content: prompt }]
});
const tokens = response.usage.output_tokens;
switch (response.stop_reason) {
case "model_context_window_exceeded":
// Got the maximum possible tokens given input size
console.log(`Generated ${tokens} tokens (context limit reached)`);
break;
case "max_tokens":
// Got exactly the requested tokens
console.log(`Generated ${tokens} tokens (max_tokens reached)`);
break;
default:
// Natural completion
console.log(`Generated ${tokens} tokens (natural completion)`);
}
const textBlock = response.content.find(
(block): block is Anthropic.Beta.BetaTextBlock => block.type === "text"
);
return textBlock?.text ?? "";
}
using Anthropic.Models.Beta.Messages;
using Model = Anthropic.Models.Messages.Model;
static async Task<string> GetMaxPossibleTokens(AnthropicClient client, string prompt)
{
var response = await client.Beta.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 20000,
Messages = [new() { Role = Role.User, Content = prompt }]
});
var tokens = response.Usage.OutputTokens;
var reason = response.StopReason?.Value();
if (reason == BetaStopReason.ModelContextWindowExceeded)
{
// Got the maximum possible tokens given input size
Console.WriteLine($"Generated {tokens} tokens (context limit reached)");
}
else if (reason == BetaStopReason.MaxTokens)
{
// Got exactly the requested tokens
Console.WriteLine($"Generated {tokens} tokens (max_tokens reached)");
}
else
{
// Natural completion
Console.WriteLine($"Generated {tokens} tokens (natural completion)");
}
return response.Content.Select(b => b.Value).OfType<BetaTextBlock>().FirstOrDefault()?.Text ?? "";
}
func getMaxPossibleTokens(client anthropic.Client, prompt string) (string, error) {
response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 20000,
Messages: []anthropic.BetaMessageParam{
anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(prompt)),
},
})
if err != nil {
return "", err
}
tokens := response.Usage.OutputTokens
switch response.StopReason {
case anthropic.BetaStopReasonModelContextWindowExceeded:
// Got the maximum possible tokens given input size
fmt.Printf("Generated %d tokens (context limit reached)\n", tokens)
case anthropic.BetaStopReasonMaxTokens:
// Got exactly the requested tokens
fmt.Printf("Generated %d tokens (max_tokens reached)\n", tokens)
default:
// Natural completion
fmt.Printf("Generated %d tokens (natural completion)\n", tokens)
}
for _, block := range response.Content {
if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok {
return textBlock.Text, nil
}
}
return "", nil
}
import com.anthropic.models.beta.messages.BetaContentBlock;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaStopReason;
import com.anthropic.models.beta.messages.MessageCreateParams;
static String getMaxPossibleTokens(AnthropicClient client, String prompt) {
BetaMessage response = client.beta().messages().create(
MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(20000L)
.addUserMessage(prompt)
.build()
);
long tokens = response.usage().outputTokens();
BetaStopReason reason = response.stopReason().orElse(BetaStopReason.END_TURN);
if (reason.equals(BetaStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED)) {
// Got the maximum possible tokens given input size
IO.println("Generated " + tokens + " tokens (context limit reached)");
} else if (reason.equals(BetaStopReason.MAX_TOKENS)) {
// Got exactly the requested tokens
IO.println("Generated " + tokens + " tokens (max_tokens reached)");
} else {
// Natural completion
IO.println("Generated " + tokens + " tokens (natural completion)");
}
return response.content().stream()
.filter(BetaContentBlock::isText)
.findFirst()
.map(block -> block.asText().text())
.orElse("");
}
function get_max_possible_tokens(Client $client, string $prompt): string
{
$response = $client->beta->messages->create(
maxTokens: 20000,
messages: [['role' => 'user', 'content' => $prompt]],
model: 'claude-opus-5-5',
);
$tokens = $response->usage->outputTokens;
echo match ($response->stopReason) {
// Got the maximum possible tokens given input size
'model_context_window_exceeded' => "Generated {$tokens} tokens (context limit reached)",
// Got exactly the requested tokens
'max_tokens' => "Generated {$tokens} tokens (max_tokens reached)",
// Natural completion
default => "Generated {$tokens} tokens (natural completion)",
}, PHP_EOL;
return array_find($response->content, static fn ($block): bool => $block->type === 'text')?->text ?? '';
}
def get_max_possible_tokens(client, prompt)
response = client.beta.messages.create(
model: "claude-opus-5-5",
max_tokens: 20000,
messages: [{ role: "user", content: prompt }]
)
tokens = response.usage.output_tokens
case response.stop_reason
when :model_context_window_exceeded
# Got the maximum possible tokens given input size
puts "Generated #{tokens} tokens (context limit reached)"
when :max_tokens
# Got exactly the requested tokens
puts "Generated #{tokens} tokens (max_tokens reached)"
else
# Natural completion
puts "Generated #{tokens} tokens (natural completion)"
end
response.content.find { it.type == :text }.text
end
다음 단계
더 알아보기 (Learn more)
- Refusals and fallback — 거부된 요청을 폴백 모델로 재시도하는 방법
- Handle tool calls — 도구 호출 결과를 돌려보내는 방법
- Server tools — 서버 측에서 실행되는 도구
- Streaming messages — 스트리밍 응답에서
stop_reason읽기 - Errors — 중단 사유와 구별되는 HTTP 오류 처리