parseOpenAiResponse function
OpenAI 互換のレスポンスボディを LLMCompletionResponse に変換する。
Implementation
LLMCompletionResponse parseOpenAiResponse(Map<String, dynamic> data) {
final choices = (data['choices'] as List?) ?? const [];
final choice = choices.isNotEmpty
? choices.first as Map<String, dynamic>
: const <String, dynamic>{};
final message =
(choice['message'] as Map<String, dynamic>?) ?? const <String, dynamic>{};
final parts = <LLMContentPart>[];
final reasoning = message['reasoning_content'] ?? message['reasoning'];
if (reasoning is String && reasoning.isNotEmpty) {
parts.add(LLMReasoningPart(reasoning));
}
final rawContent = message['content'];
if (rawContent is String && rawContent.isNotEmpty) {
parts.add(LLMTextPart(rawContent));
} else if (rawContent is List) {
_appendOpenAiContentParts(rawContent, parts);
}
// 生成画像(OpenRouter は message.images で返す)。
final images = message['images'];
if (images is List) _appendOpenAiImages(images, parts);
final toolCalls = message['tool_calls'];
if (toolCalls is List) {
for (final tc in toolCalls) {
if (tc is Map<String, dynamic>) {
final fn = tc['function'];
final fnMap =
fn is Map<String, dynamic> ? fn : const <String, dynamic>{};
parts.add(
LLMToolCallPart(
id: (tc['id'] ?? '').toString(),
name: (fnMap['name'] ?? '').toString(),
arguments: _decodeArgs(fnMap['arguments']),
),
);
}
}
}
final audio = message['audio'];
if (audio is Map<String, dynamic>) {
final adata = audio['data'];
if (adata is String && adata.isNotEmpty) {
parts.add(
LLMAudioPart.base64(adata, transcript: audio['transcript'] as String?),
);
}
}
final usage =
(data['usage'] as Map<String, dynamic>?) ?? const <String, dynamic>{};
return LLMCompletionResponse(
content: LLMContent(role: LLMRole.model, parts: parts),
usage: _parseOpenAiUsage(usage),
finishReason: LLMFinishReason.parse(choice['finish_reason'] as String?),
model: data['model'] as String?,
id: data['id'] as String?,
);
}