중단 사유와 폴백

중단 사유와 폴백 (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가 자연스럽게 응답을 마쳤다는 뜻이에요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello!"}] }' | jq 'if .stop_reason == "end_turn" then (.content[] | select(.type == "text") | .text) else . end' ```
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가 `stop_reason: "end_turn"`과 함께 콘텐츠가 전혀 없는(정확히 2~3 토큰, 콘텐츠 없음) 빈 응답을 반환할 때가 있어요. 보통 Claude가 어시스턴트 턴이 완료됐다고 판단했을 때, 특히 도구 결과 뒤에서 발생해요.

흔한 원인:

  • 도구 결과 직후 텍스트 블록을 추가한 경우 (Claude가 도구 결과 뒤에는 항상 사용자가 텍스트를 넣는다는 패턴을 학습해서, 그 패턴을 따르려고 턴을 끝내버려요)
  • Claude의 완료된 응답을 아무것도 추가하지 않고 그대로 다시 보낸 경우 (Claude가 이미 끝났다고 판단했으니 계속 끝난 상태로 남아요)

빈 응답을 막는 방법:

```python Python # 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"}, { "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",
        "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" }
    ]
  }
]
```

메시지 구조를 고친 뒤에도 빈 응답이 계속 온다면, 빈 응답으로 재시도하는 대신 새 사용자 메시지에 계속하라는 프롬프트를 추가해보세요:

```python Python 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" 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
```

모범 사례:

  1. 도구 결과 직후 텍스트 블록을 절대 추가하지 마세요: 이렇게 하면 Claude가 모든 도구 사용 뒤에 사용자 입력이 온다고 학습해버려요.
  2. 수정 없이 빈 응답을 재시도하지 마세요: 빈 응답을 그대로 다시 보내도 소용없어요.
  3. 계속하라는 프롬프트는 최후의 수단으로: 이 해결책들로 문제가 해결되지 않을 때만 쓰세요.

max_tokens

Claude가 요청에서 지정한 max_tokens 한도에 도달해서 멈췄어요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 10, "messages": [{"role": "user", "content": "Explain quantum physics"}] }' | jq '.stop_reason' ```
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
Claude의 응답이 `max_tokens` 한도에 걸려 잘렸는데, 잘린 응답에 불완전한 도구 사용 블록이 들어 있다면, 더 큰 `max_tokens` 값으로 요청을 다시 보내야 완전한 도구 사용을 얻을 수 있어요. ```bash CLI RESPONSE=$(ant messages create --max-tokens 1024 --format jsonl <<'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 )
# 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가 사용자가 설정한 커스텀 중단 시퀀스 중 하나를 만났어요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "stop_sequences": ["END", "STOP"], "messages": [{"role": "user", "content": "Generate text until you say END"}] }' | jq '{stop_reason, stop_sequence}' ```
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를 쓰세요. 도구 실행, 결과 포맷팅, 대화 관리를 자동으로 처리해줘요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "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"] } }], "messages": [{"role": "user", "content": "What is the weather in San Francisco?"}] }' | jq '.stop_reason, (.content[] | select(.type == "tool_use"))' ```
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_turnstop_reason을 가질 리가 없어요. Claude가 사용자의 도구를 호출하려고 멈출 때 stop_reasontool_use이고, 응답 자체 대신 클라이언트 tool_result 블록을 보내서 이어가야 해요.

```bash cURL # The SDKs handle continuation directly. With cURL, inspect stop_reason # on the response and re-POST with the assistant content appended. curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "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"}] }' | jq '{stop_reason, content}' ```
# 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 응답으로 반환해요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "[Unsafe request]"}] }' | jq '{stop_reason, stop_details}' ```
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_detailsrefusal이 아닌 모든 중단 사유에서 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-26 beta 헤더를 추가해야 해요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 20000, "messages": [{"role": "user", "content": "Large input that uses most of context window..."}] }' | jq '.stop_reason' ```
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을 확인하는 습관을 들이세요:

```python Python def handle_response(response): match response.stop_reason: case "tool_use": return handle_tool_use(response) case "max_tokens": return handle_truncation(response) case "model_context_window_exceeded": return handle_context_limit(response) case "pause_turn": return handle_pause(response) case "refusal": return handle_refusal(response) case _: # Handle end_turn and other cases return next( (block.text for block in response.content if block.type == "text"), "", ) ```
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를 보세요.

```python Python def handle_truncated_response(response): text = next((block.text for block in response.content if block.type == "text"), "") if response.stop_reason in ["max_tokens", "model_context_window_exceeded"]: if response.stop_reason == "max_tokens": note = "[Response truncated due to max_tokens limit]" else: note = "[Response truncated due to context window limit]" return f"{text}\n\n{note}" return text ```
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을 반환할 수 있어요. 대화를 계속해서 처리하면 돼요:

```python Python def handle_server_tool_conversation(client, user_query, tools, max_continuations=5): """ Handle server tool conversations that may require multiple continuations.
  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
  • 요청 처리 실패를 나타내요
  • 응답에 오류 정보가 들어 있어요
```bash cURL # cURL exits non-zero on HTTP errors with --fail-with-body; inspect # $? for errors and stop_reason for successful responses. curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello!"}] }' | jq '.stop_reason' ```
# 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 이벤트에서 제공됨
  • 다른 이벤트에서는 제공되지 않음
```bash cURL # The message_delta event in the SSE stream carries stop_reason. curl --no-buffer https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "stream": true, "messages": [{"role": "user", "content": "Hello!"}] }' ```
# 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가 훨씬 적은 코드로 도구 실행을 자동으로 처리해요.

```python Python def complete_tool_workflow(client, user_query, tools): messages = [{"role": "user", "content": user_query}]
  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

응답을 완전하게 만들기

```python Python def get_complete_response(client, prompt, max_attempts=3): messages = [{"role": "user", "content": prompt}] full_response = ""
  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 중단 사유를 쓰면 입력 크기를 계산하지 않고 가능한 최대 토큰을 요청할 수 있어요:

```python Python def get_max_possible_tokens(client, prompt): """ Get as many tokens as possible within the model's context window without needing to calculate input token count """ response = client.beta.messages.create( model="claude-opus-5-5", messages=[{"role": "user", "content": prompt}], max_tokens=20000, # Python SDK requires streaming for max_tokens above ~21k )
  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

다음 단계

Retry refused requests on a fallback model, server-side or in your client. Let the SDK manage the `tool_use` loop, result formatting, and retries for you. Read `stop_reason` from the `message_delta` event when streaming. Handle 4xx and 5xx HTTP errors, which are distinct from stop reasons.

더 알아보기 (Learn more)