llm_claude

pub.flutter-io.cn

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 (JsonSchemaFormat via native output_config.format; JsonFormat via system-message injection)
  • Builder pattern for fluent configuration
  • Configurable retry and timeout policies
  • extraHeaders, e.g. to enable anthropic-beta features

Installation

dependencies:
  llm_claude: ^0.6.0

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()],
);

Tools run automatically; pass LLMChatOptions(autoExecuteTools: false) to handle chunk.message?.toolCalls yourself. Claude names the tool in content_block_start, so chunk.message?.toolCallDeltas reports it before any argument arrives.

A turn cut off by max_tokens finishes as LLMFinishReason.length and still returns its tool blocks: those whose input decodes on toolCalls, the cut one on chunk.message?.invalidToolCalls with its raw input and the parse error. The same applies to unvalidated partial JSON from the fine-grained tool streaming beta. Invalid calls are never executed; the tool loop answers them with a tool error. A refusal turn returns no calls.

extraHeaders makes beta features reachable:

final repo = ClaudeChatRepository(
  apiKey: 'your-api-key',
  // Protocol headers, `x-api-key` and `anthropic-version` always take precedence.
  extraHeaders: {'anthropic-beta': 'fine-grained-tool-streaming-2025-05-14'},
);

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 tests
  • claude-opus-5 — Most capable
  • claude-sonnet-5 — Balanced performance and cost
  • claude-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() and batchEmbed() throw UnsupportedError.
  • JsonSchemaFormat constrains decoding natively via output_config.format on models that support it; JsonFormat always falls back to system-message injection, because the API has no bare-JSON mode.
  • max_tokens defaults to 4096; override via backendOptions['max_tokens'].
  • A thinking budget is clamped to max_tokens - 1 on legacy models. Raise max_tokens alongside thinking_budget or the budget you asked for is not the budget you get.
  • tool_choice accepts 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.

Libraries

llm_claude
Anthropic Claude API backend for LLM interactions.