llm_claude 0.5.0
llm_claude: ^0.5.0 copied to clipboard
Anthropic Claude API backend for LLM interactions. Provides streaming chat and tool calling via the Anthropic Messages API.
llm_claude #
Anthropic Claude backend for LLM interactions in Dart. Part of the dart-llm ecosystem.
Available on pub.flutter-io.cn.
Features #
- Streaming chat responses via the Anthropic Messages API
- Tool/function calling with automatic multi-turn tool-loop execution
- Vision (image) support
- Thinking mode (extended reasoning) support
- Structured output (
JsonSchemaFormatvia nativeoutput_config.format;JsonFormatvia system-message injection) - Builder pattern for fluent configuration
- Configurable retry and timeout policies
Installation #
dependencies:
llm_claude: ^0.3.2
Prerequisites #
You need an Anthropic API key. Get one from console.anthropic.com.
Important: Never commit your API key to version control. Use environment variables or a .env file.
Usage #
Basic Chat #
import 'package:llm_claude/llm_claude.dart';
final repo = ClaudeChatRepository(apiKey: 'your-api-key');
final stream = repo.streamChat('claude-haiku-4-5-20251001', messages: [
LLMMessage(role: LLMRole.user, content: 'Hello!'),
]);
await for (final chunk in stream) {
stdout.write(chunk.message?.content ?? '');
}
System Message #
final stream = repo.streamChat('claude-haiku-4-5-20251001', messages: [
LLMMessage(role: LLMRole.system, content: 'You are a concise assistant.'),
LLMMessage(role: LLMRole.user, content: 'Explain quantum entanglement.'),
]);
Tool Calling #
class WeatherTool extends LLMTool {
@override
String get name => 'get_weather';
@override
String get description => 'Get the current weather for a location.';
@override
List<LLMToolParam> get parameters => [
LLMToolParam(
name: 'location',
type: 'string',
description: 'City name',
isRequired: true,
),
];
@override
Future<dynamic> execute(Map<String, dynamic> args, {dynamic extra}) async {
return {'temperature': 22, 'condition': 'sunny'};
}
}
final stream = repo.streamChat(
'claude-haiku-4-5-20251001',
messages: [LLMMessage(role: LLMRole.user, content: 'What is the weather in Oslo?')],
tools: [WeatherTool()],
);
Structured Output #
Which mechanism is used depends on the format and the model:
| Modern & transitional models | Legacy models | |
|---|---|---|
JsonSchemaFormat |
native output_config.format with type: json_schema |
system-message injection |
JsonFormat |
system-message injection | system-message injection |
There is no native bare-JSON mode in the Anthropic API, so JsonFormat always
injects — it appends a JSON instruction after any user-defined system content.
Legacy here means Opus/Sonnet 4.5 and earlier, Haiku 4.5 and earlier, and the
Claude 3 family; see Models.
import 'package:llm_core/llm_core.dart';
// Simple JSON mode
final stream = repo.streamChat(
'claude-haiku-4-5-20251001',
messages: [LLMMessage(role: LLMRole.user, content: 'List three fruits as JSON.')],
options: const LLMChatOptions(responseFormat: JsonFormat()),
);
// JSON Schema mode
const schema = {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'age': {'type': 'integer'},
},
'required': ['name', 'age'],
};
final stream = repo.streamChat(
'claude-haiku-4-5-20251001',
messages: [LLMMessage(role: LLMRole.user, content: 'Return a person object.')],
options: const LLMChatOptions(
responseFormat: JsonSchemaFormat(name: 'Person', schema: schema),
),
);
Thinking Mode #
Extended reasoning (thinking) is supported on compatible models:
final stream = repo.streamChat(
'claude-haiku-4-5-20251001',
messages: [LLMMessage(role: LLMRole.user, content: 'Solve this step by step: ...')],
think: true,
options: const LLMChatOptions(
backendOptions: {
// haiku-4-5 is a legacy model, so thinking uses a token budget — and the
// budget is clamped to `max_tokens - 1`. Raise max_tokens alongside it or
// the effective budget is 4095, not 16000.
'max_tokens': 24000,
'thinking_budget': 16000,
},
),
);
await for (final chunk in stream) {
if (chunk.message?.thinking != null) {
// Extended reasoning content
stdout.write(chunk.message!.thinking!);
} else {
stdout.write(chunk.message?.content ?? '');
}
}
Non-Streaming Response #
final response = await repo.chatResponse('claude-haiku-4-5-20251001', messages: [
LLMMessage(role: LLMRole.user, content: 'Hello!'),
]);
print(response.content);
Using LLMChatOptions #
// `thinking_budget` is only read when `think: true` is set; without it the
// key is dropped.
final options = LLMChatOptions(
tools: [WeatherTool()],
toolAttempts: 5,
backendOptions: {
'max_tokens': 8192,
'thinking_budget': 10000,
},
);
final stream = repo.streamChat('claude-haiku-4-5-20251001', messages: messages, options: options);
Advanced Configuration #
Builder Pattern #
final repo = ClaudeChatRepository.builder()
.apiKey('your-api-key')
.baseUrl('https://api.anthropic.com')
.maxToolAttempts(10)
.retryConfig(RetryConfig(
maxAttempts: 3,
initialDelay: Duration(seconds: 1),
maxDelay: Duration(seconds: 30),
))
.timeoutConfig(TimeoutConfig(
connectionTimeout: Duration(seconds: 10),
readTimeout: Duration(minutes: 5),
))
.build();
Retry Configuration #
final repo = ClaudeChatRepository(
apiKey: 'your-api-key',
retryConfig: RetryConfig(
maxAttempts: 3,
initialDelay: Duration(seconds: 1),
maxDelay: Duration(seconds: 30),
retryableStatusCodes: [429, 500, 502, 503, 504],
),
);
Timeout Configuration #
final repo = ClaudeChatRepository(
apiKey: 'your-api-key',
timeoutConfig: TimeoutConfig(
connectionTimeout: Duration(seconds: 10),
readTimeout: Duration(minutes: 5),
totalTimeout: Duration(minutes: 10),
),
);
Models #
See Anthropic Models for available models:
claude-haiku-4-5-20251001— Low-cost current Haiku model used by live testsclaude-opus-5— Most capableclaude-sonnet-5— Balanced performance and costclaude-haiku-4-5— Fastest and cheapest
Model families differ in the request shape they accept, and llm_claude
selects it automatically from the model id:
| Model family | Thinking | temperature / top_p / top_k |
Structured output |
|---|---|---|---|
| Opus 4.7+, Sonnet 5, Fable 5, Mythos 5 | adaptive |
rejected (400) — omitted automatically | native output_config |
| Opus 4.6, Sonnet 4.6 | adaptive |
accepted | native output_config |
| Opus 4.5 and earlier, Haiku 4.5, Claude 3 | budget_tokens |
accepted | system-prompt injection |
Sending budget_tokens to a current model — or a sampling parameter to Opus
4.7+ — is a 400, not a warning. LLMChatOptions.reasoningBudget is
translated to an output_config.effort level on models that no longer accept
token budgets, so the setting is honored rather than dropped.
On current models an explicit LLMChatOptions.reasoningEffort wins over a
budget-derived level (effort-native path); on legacy models the budget wins,
and an effort-only request converts through claudeBudgetForEffort
(budget-native path).
An unrecognized model id is treated as a current model, so a newly released Claude works without a library update.
Notes #
- Claude does not support embeddings.
embed()andbatchEmbed()throwUnsupportedError. JsonSchemaFormatconstrains decoding natively viaoutput_config.formaton models that support it;JsonFormatalways falls back to system-message injection, because the API has no bare-JSON mode.max_tokensdefaults to 4096; override viabackendOptions['max_tokens'].- A thinking budget is clamped to
max_tokens - 1on legacy models. Raisemax_tokensalongsidethinking_budgetor the budget you asked for is not the budget you get. tool_choiceaccepts the shorthands'auto','any','required','none'and a bare tool name, in addition to the API's own object form.- Retries are off unless you pass a
RetryConfig.