complete method
Asks the model to map prompt to an action.
context is the project-specific brief: the available task and job keys
plus the command reference. Throws AiException on transport or protocol
failure.
Implementation
@override
Future<AiReply> complete({
required String context,
required String prompt,
}) async {
final body = await post(
'${config.baseUrl}/chat/completions',
{
'model': config.model,
'max_tokens': config.maxTokens,
'messages': [
{'role': 'system', 'content': context},
{'role': 'user', 'content': prompt},
],
'tools': [
{
'type': 'function',
'function': {
'name': AiProvider.toolName,
'description': AiProvider.toolDescription,
'parameters': AiProvider.toolSchema,
},
},
],
'tool_choice': 'auto',
},
{
'Content-Type': 'application/json',
if (config.hasApiKey) 'Authorization': 'Bearer ${config.apiKey}',
},
);
final choices = body['choices'];
if (choices is! List || choices.isEmpty) {
throw AiException('$name returned no choices');
}
final message = choices.first['message'];
if (message is! Map) {
throw AiException('$name returned a malformed choice');
}
final toolCalls = message['tool_calls'];
if (toolCalls is List && toolCalls.isNotEmpty) {
// A reply cut off by the token ceiling still carries a tool call, but its
// argument JSON is only as complete as the model got. Running that means
// running a command it never finished choosing.
if (choices.first['finish_reason'] == 'length') {
throw AiException(
'the reply was cut off by the token limit, so the command it was '
'choosing is incomplete — raise `max-tokens` (currently '
'${config.maxTokens}) and try again',
);
}
final function = toolCalls.first['function'];
// Arguments arrive as a JSON *string* here, unlike Anthropic's parsed map.
final arguments = _decodeArguments(function?['arguments']);
return AiReply(action: AiAction.fromJson(arguments));
}
return AiReply(text: message['content']?.toString());
}