blob: ec5f2be64259746c588cf16d654ffdce0376f67a [file]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of 'api.dart';
/// The types of context in which should be included in a prompt.
///
/// The schema deprecates [thisServer] and [allServers], asking a server to omit
/// the field or send [none] unless the client declares the `sampling.context`
/// capability.
enum IncludeContext {
none,
thisServer,
allServers;
@Deprecated('Use `IncludeContext.thisServer` instead.')
static const thisService = thisServer;
}
/// A request from the server to sample an LLM via the client.
///
/// The client has full discretion over which model to select. The client should
/// also inform the user before beginning sampling, to allow them to inspect
/// the request (human in the loop) and decide whether to approve it.
extension type CreateMessageRequest.fromMap(Map<String, Object?> _value)
implements Request {
static const methodName = 'sampling/createMessage';
factory CreateMessageRequest({
required List<SamplingMessage> messages,
ModelPreferences? modelPreferences,
String? systemPrompt,
IncludeContext? includeContext,
num? temperature,
required int maxTokens,
List<String>? stopSequences,
ToolChoice? toolChoice,
List<Tool>? tools,
Map<String, Object?>? metadata,
MetaWithProgressToken? meta,
}) => CreateMessageRequest.fromMap({
Keys.messages: messages,
if (modelPreferences != null) Keys.modelPreferences: modelPreferences,
if (systemPrompt != null) Keys.systemPrompt: systemPrompt,
if (includeContext != null) Keys.includeContext: includeContext.name,
if (temperature != null) Keys.temperature: temperature,
Keys.maxTokens: maxTokens,
if (stopSequences != null) Keys.stopSequences: stopSequences,
if (toolChoice != null) Keys.toolChoice: toolChoice,
if (tools != null) Keys.tools: tools,
if (metadata != null) Keys.metadata: metadata,
if (meta != null) Keys.meta: meta,
});
/// The messages to send to the LLM.
List<SamplingMessage> get messages {
final messages = _value[Keys.messages] as List?;
if (messages == null) {
throw ArgumentError('Missing messages field in $CreateMessageRequest.');
}
return messages.cast<SamplingMessage>();
}
/// The server's preferences for which model to select.
///
/// The client MAY ignore these preferences.
ModelPreferences? get modelPreferences =>
_value[Keys.modelPreferences] as ModelPreferences?;
/// An optional system prompt the server wants to use for sampling.
///
/// The client MAY modify or omit this prompt.
String? get systemPrompt => _value[Keys.systemPrompt] as String?;
/// A request to include context from one or more MCP servers (including
/// the caller), to be attached to the prompt.
///
/// The client MAY ignore this request.
IncludeContext? get includeContext {
var includeContext = _value[Keys.includeContext] as String?;
if (includeContext == null) return null;
// This package wrote `thisService` for the schema's `thisServer` up to
// 0.5.2. Treat it as an alias in case a server is still on that version.
if (includeContext == 'thisService') {
includeContext = IncludeContext.thisServer.name;
}
return IncludeContext.values.firstWhere(
(value) => value.name == includeContext,
);
}
/// The temperature to use for sampling.
double? get temperature => (_value[Keys.temperature] as num?)?.toDouble();
/// The maximum number of tokens to sample, as requested by the server.
///
/// The client MAY choose to sample fewer tokens than requested.
int get maxTokens {
final maxTokens = _value[Keys.maxTokens] as int?;
if (maxTokens == null) {
throw ArgumentError(
'Missing ${Keys.maxTokens} field in $CreateMessageRequest.',
);
}
return maxTokens;
}
/// Note: This has no documentation in the specification or schema.
List<String>? get stopSequences =>
(_value[Keys.stopSequences] as List?)?.cast<String>();
/// Controls how the model uses tools (if available).
ToolChoice? get toolChoice => _value[Keys.toolChoice] as ToolChoice?;
/// Tools the model may call during this request.
List<Tool>? get tools => (_value[Keys.tools] as List?)?.cast<Tool>();
/// Optional metadata to pass through to the LLM provider.
///
/// The format of this metadata is provider-specific.
Map<String, Object?>? get metadata =>
(_value[Keys.metadata] as Map?)?.cast<String, Object?>();
}
/// The client's response to a sampling/create_message request from the
/// server.
///
/// The client should inform the user before returning the sampled message, to
/// allow them to inspect the response (human in the loop) and decide whether
/// to allow the server to see it.
extension type CreateMessageResult.fromMap(Map<String, Object?> _value)
implements Result, SamplingMessage {
factory CreateMessageResult({
required Role role,
required List<SamplingMessageContentBlock> content,
required String model,
String? stopReason,
Meta? meta,
}) => CreateMessageResult.fromMap({
Keys.role: role.name,
Keys.content: _encodeContent(content),
Keys.model: model,
if (stopReason != null) Keys.stopReason: stopReason,
if (meta != null) Keys.meta: meta,
});
/// The name of the model that generated the message.
String get model => _value[Keys.model] as String;
/// The reason why sampling stopped, if known.
///
/// Known reasons are "endTurn", "stopSequence", "maxTokens", "toolUse", or
/// any other reason.
String? get stopReason => _value[Keys.stopReason] as String?;
/// The JSON representation of this object.
Map<String, Object?> toJson() => _value;
}
/// Describes a message issued to or received from an LLM API.
extension type SamplingMessage.fromMap(Map<String, Object?> _value) {
factory SamplingMessage({
required Role role,
required List<SamplingMessageContentBlock> content,
}) => SamplingMessage.fromMap({
Keys.role: role.name,
Keys.content: _encodeContent(content),
});
/// The role of the message.
Role get role =>
Role.values.firstWhere((value) => value.name == _value[Keys.role]);
/// The content of the message.
///
/// The schema allows one block or a list of them under `content`, and both
/// read as a list here. One block comes back as a single-element list.
List<SamplingMessageContentBlock> get content {
final content = _value[Keys.content];
if (content == null) {
throw ArgumentError('Missing ${Keys.content} field in $SamplingMessage.');
}
if (content is List) {
return content.cast<SamplingMessageContentBlock>();
}
return [content as SamplingMessageContentBlock];
}
}
/// Writes [content] under `content` as one of the schema's two shapes.
///
/// A single block goes on the wire as that block. Two or more go as a list.
Object? _encodeContent(List<SamplingMessageContentBlock> content) {
if (content.length == 1) return content.single;
return content;
}
/// One block of a [SamplingMessage]'s content, sent to or received from an
/// LLM.
///
/// Could be either [TextContent], [ImageContent], [AudioContent],
/// [ToolUseContent] or [ToolResultContent].
///
/// Switch on the [type] before casting to the more specific types. The two
/// arms a `tools/call` result cannot carry have their own checks,
/// [isToolUse] and [isToolResult]. A [TextContent], [ImageContent] or
/// [AudioContent] read as a [Content] keeps the checks declared there.
///
/// This does not implement [Content]. A plain `tools/call` result never
/// carries [ToolUseContent] or [ToolResultContent]. Keeping sampling content
/// on its own type stops the two from being swapped by accident.
///
/// Doing `is` checks does not work because these are just extension types,
/// they all have the same runtime type (`Map<String, Object?>`).
extension type SamplingMessageContentBlock._(Map<String, Object?> _value) {
factory SamplingMessageContentBlock.fromMap(Map<String, Object?> value) {
assert(value.containsKey(Keys.type));
return SamplingMessageContentBlock._(value);
}
/// Alias for [TextContent.new].
static const text = TextContent.new;
/// Alias for [ImageContent.new].
static const image = ImageContent.new;
/// Alias for [AudioContent.new].
static const audio = AudioContent.new;
/// Alias for [ToolUseContent.new].
static const toolUse = ToolUseContent.new;
/// Alias for [ToolResultContent.new].
static const toolResult = ToolResultContent.new;
/// Whether or not this is a [ToolUseContent].
bool get isToolUse => _value[Keys.type] == ToolUseContent.expectedType;
/// Whether or not this is a [ToolResultContent].
bool get isToolResult => _value[Keys.type] == ToolResultContent.expectedType;
/// The type of content.
///
/// Switch on this to handle each case (see the static `expectedType`
/// getters). [isToolUse] and [isToolResult] cover the two arms a
/// `tools/call` result cannot carry.
String get type => _value[Keys.type] as String;
}
/// A request from the assistant to call a tool.
///
/// From the 2025-11-25 revision.
extension type ToolUseContent.fromMap(Map<String, Object?> _value)
implements SamplingMessageContentBlock, WithMetadata {
static const expectedType = 'tool_use';
factory ToolUseContent({
required String id,
required String name,
required Map<String, Object?> input,
Meta? meta,
}) => ToolUseContent.fromMap({
Keys.id: id,
Keys.input: input,
Keys.name: name,
Keys.type: expectedType,
if (meta != null) Keys.meta: meta,
});
/// The content type, always [expectedType].
String get type {
final type = _value[Keys.type] as String;
assert(type == expectedType);
return type;
}
/// The unique identifier for this tool use.
String get id => _value[Keys.id] as String;
/// The name of the tool to call.
String get name => _value[Keys.name] as String;
/// The arguments to pass to the tool.
Map<String, Object?> get input =>
(_value[Keys.input] as Map).cast<String, Object?>();
}
/// The result of a tool use, provided by the user back to the assistant.
///
/// From the 2025-11-25 revision.
extension type ToolResultContent.fromMap(Map<String, Object?> _value)
implements SamplingMessageContentBlock, WithMetadata {
static const expectedType = 'tool_result';
factory ToolResultContent({
required List<Content> content,
required String toolUseId,
Map<String, Object?>? structuredContent,
bool? isError,
Meta? meta,
}) => ToolResultContent.fromMap({
Keys.content: content,
Keys.toolUseId: toolUseId,
Keys.type: expectedType,
if (structuredContent != null) Keys.structuredContent: structuredContent,
if (isError != null) Keys.isError: isError,
if (meta != null) Keys.meta: meta,
});
/// The content type, always [expectedType].
String get type {
final type = _value[Keys.type] as String;
assert(type == expectedType);
return type;
}
/// The content returned by the tool, either [TextContent], [ImageContent],
/// [AudioContent], [ResourceLink] or [EmbeddedResource].
List<Content> get content {
final content = (_value[Keys.content] as List?)?.cast<Content>();
if (content == null) {
throw ArgumentError(
'Missing ${Keys.content} field in $ToolResultContent',
);
}
return content;
}
/// The structured result returned by the tool.
Map<String, Object?>? get structuredContent =>
_value[Keys.structuredContent] as Map<String, Object?>?;
/// Whether the tool use resulted in an error.
bool? get isError => _value[Keys.isError] as bool?;
/// The identifier of the tool use this result corresponds to.
String get toolUseId => _value[Keys.toolUseId] as String;
}
/// The server's preferences for model selection, requested of the client
/// during sampling.
///
/// Because LLMs can vary along multiple dimensions, choosing the "best" model
/// is rarely straightforward. Different models excel in different areas—some
/// are faster but less capable, others are more capable but more expensive,
/// and so on. This interface allows servers to express their priorities
/// across multiple dimensions to help clients make an appropriate selection
/// for their use case.
///
/// These preferences are always advisory. The client MAY ignore them. It is
/// also up to the client to decide how to interpret these preferences and
/// how to balance them against other considerations.
extension type ModelPreferences.fromMap(Map<String, Object?> _value) {
factory ModelPreferences({
List<ModelHint>? hints,
double? costPriority,
double? speedPriority,
double? intelligencePriority,
}) => ModelPreferences.fromMap({
if (hints != null) Keys.hints: hints,
if (costPriority != null) Keys.costPriority: costPriority,
if (speedPriority != null) Keys.speedPriority: speedPriority,
if (intelligencePriority != null)
Keys.intelligencePriority: intelligencePriority,
});
/// Optional hints to use for model selection.
///
/// If multiple hints are specified, the client MUST evaluate them in order
/// (such that the first match is taken).
///
/// The client SHOULD prioritize these hints over the numeric priorities,
/// but MAY still use the priorities to select from ambiguous matches.
List<ModelHint>? get hints =>
(_value[Keys.hints] as List?)?.cast<ModelHint>();
/// How much to prioritize cost when selecting a model.
///
/// A value of 0 means cost is not important, while a value of 1 means cost
/// is the most important factor.
double? get costPriority => (_value[Keys.costPriority] as num?)?.toDouble();
/// How much to prioritize sampling speed (latency) when selecting a model.
///
/// A value of 0 means speed is not important, while a value of 1 means speed
/// is the most important factor.
double? get speedPriority => (_value[Keys.speedPriority] as num?)?.toDouble();
/// How much to prioritize intelligence and capabilities when selecting a
/// model.
///
/// A value of 0 means intelligence is not important, while a value of 1
/// means intelligence is the most important factor.
double? get intelligencePriority =>
(_value[Keys.intelligencePriority] as num?)?.toDouble();
}
/// Hints to use for model selection.
///
/// Keys not declared here are currently left unspecified by the spec and are
/// up to the client to interpret.
extension type ModelHint.fromMap(Map<String, Object?> _value) {
factory ModelHint({String? name}) =>
ModelHint.fromMap({if (name != null) Keys.name: name});
/// A hint for a model name.
///
/// The client SHOULD treat this as a substring of a model name; for
/// example:
/// - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022`
/// - `sonnet` should match `claude-3-5-sonnet-20241022`,
/// `claude-3-sonnet-20240229`, etc.
/// - `claude` should match any Claude model
///
/// The client MAY also map the string to a different provider's model name
/// or a different model family, as long as it fills a similar niche; for
/// example:
/// - `gemini-1.5-flash` could match `claude-3-haiku-20240307`
String? get name => _value[Keys.name] as String?;
}
/// Controls tool selection behavior for sampling requests.
extension type ToolChoice.fromMap(Map<String, Object?> _value) {
factory ToolChoice({required ToolChoiceMode mode}) =>
ToolChoice.fromMap({Keys.mode: mode.name});
/// Controls the tool use ability of the model:
ToolChoiceMode get mode {
final mode = _value[Keys.mode] as String?;
if (mode == null) {
throw ArgumentError('Missing ${Keys.mode} field in $ToolChoice');
}
return ToolChoiceMode.values.firstWhere((value) => value.name == mode);
}
}
/// The tool selection mode for sampling requests.
enum ToolChoiceMode {
/// Model decides whether to use tools (default).
auto,
/// Model MUST use at least one tool before completing.
///
/// On the wire, this is represented as "required", but that is a reserved
/// keyword in Dart, so we use "require" instead.
require(Keys.required),
/// Model MUST NOT use any tools.
none;
const ToolChoiceMode([this._name]);
final String? _name;
String get name => _name ?? EnumName(this).name;
}