complete method

  1. @override
Future<AiReply> complete({
  1. required String context,
  2. required String prompt,
})
override

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());
}