196 lines
5.8 KiB
Dart
196 lines
5.8 KiB
Dart
// Response parser for Anthropic Message API responses
|
|
// Ported from old_repo/services/api/errors.ts and claude.ts
|
|
|
|
import "api_types.dart";
|
|
|
|
// Parse Message API response into ApiMessage model
|
|
class ResponseParser {
|
|
static ApiMessage parseMessageResponse(Map<String, dynamic> json) {
|
|
return ApiMessage.fromJson(json);
|
|
}
|
|
|
|
static ApiMessage parseOpenRouterResponse(Map<String, dynamic> json) {
|
|
return ApiMessage.fromOpenRouterResponse(json);
|
|
}
|
|
|
|
// extract text content from message
|
|
static String extractTextContent(ApiMessage message) {
|
|
final textBlocks = <String>[];
|
|
|
|
for (final block in message.content) {
|
|
if (block is Map<String, dynamic>) {
|
|
final type = block["type"];
|
|
if (type == "text") {
|
|
final text = block["text"];
|
|
if (text is String) {
|
|
textBlocks.add(text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return textBlocks.join("\n");
|
|
}
|
|
|
|
// extract all tool use blocks from message
|
|
static List<ToolUse> extractToolUseBlocks(ApiMessage message) {
|
|
final tools = <ToolUse>[];
|
|
|
|
for (final block in message.content) {
|
|
if (block is Map<String, dynamic>) {
|
|
final type = block["type"];
|
|
if (type == "tool_use") {
|
|
tools.add(ToolUse.fromJson(block));
|
|
}
|
|
}
|
|
}
|
|
|
|
return tools;
|
|
}
|
|
|
|
// check if message is a tool use (or contains only tool use)
|
|
static bool hasToolUse(ApiMessage message) {
|
|
return message.content.any((block) {
|
|
return block is Map<String, dynamic> && block["type"] == "tool_use";
|
|
});
|
|
}
|
|
|
|
// check stop reason
|
|
static bool didStopOnToolUse(ApiMessage message) {
|
|
return message.stopReason == "tool_use";
|
|
}
|
|
|
|
static bool didStopOnMaxTokens(ApiMessage message) {
|
|
return message.stopReason == "max_tokens";
|
|
}
|
|
|
|
static bool didCompleteNormally(ApiMessage message) {
|
|
return message.stopReason == "end_turn";
|
|
}
|
|
}
|
|
|
|
// Parse error responses from the API
|
|
class ErrorParser {
|
|
// check if raw API error is authentication related
|
|
static bool isAuthenticationError(String errorMessage) {
|
|
final lower = errorMessage.toLowerCase();
|
|
return lower.contains("unauthorized") ||
|
|
lower.contains("authentication") ||
|
|
lower.contains("invalid api key") ||
|
|
lower.contains("missing authentication");
|
|
}
|
|
|
|
// check if error is rate limit related
|
|
static bool isRateLimitError(String errorMessage) {
|
|
final lower = errorMessage.toLowerCase();
|
|
return lower.contains("rate limit") ||
|
|
lower.contains("too many requests") ||
|
|
lower.contains("quota");
|
|
}
|
|
|
|
// check if error is related to prompt being too long
|
|
static bool isPromptTooLongError(String errorMessage) {
|
|
final lower = errorMessage.toLowerCase();
|
|
return lower.contains("prompt is too long") ||
|
|
lower.contains("context_length_exceeded");
|
|
}
|
|
|
|
// check if error is media/content related
|
|
static bool isMediaSizeError(String errorMessage) {
|
|
final lower = errorMessage.toLowerCase();
|
|
return (lower.contains("image exceeds") && lower.contains("maximum")) ||
|
|
(lower.contains("image dimensions exceed") &&
|
|
lower.contains("many-image")) ||
|
|
RegExp(
|
|
r"maximum of \d+ pdf pages",
|
|
caseSensitive: false,
|
|
).hasMatch(errorMessage);
|
|
}
|
|
|
|
// parse prompt too long error to extract token counts
|
|
static ({int? actualTokens, int? limitTokens}) parsePromptTooLongError(
|
|
String rawMessage,
|
|
) {
|
|
final match = RegExp(
|
|
r"prompt is too long[^0-9]*(\d+)\s*tokens?\s*>\s*(\d+)",
|
|
caseSensitive: false,
|
|
).firstMatch(rawMessage);
|
|
|
|
return (
|
|
actualTokens: match != null ? int.tryParse(match.group(1)!) : null,
|
|
limitTokens: match != null ? int.tryParse(match.group(2)!) : null,
|
|
);
|
|
}
|
|
|
|
// extract server error message from API response
|
|
static String? extractErrorMessage(Map<String, dynamic>? errorJson) {
|
|
if (errorJson == null) return null;
|
|
|
|
final nestedError = errorJson["error"];
|
|
if (nestedError is Map<String, dynamic>) {
|
|
final nestedMessage = nestedError["message"];
|
|
if (nestedMessage is String && nestedMessage.isNotEmpty) {
|
|
return nestedMessage;
|
|
}
|
|
}
|
|
|
|
// try common error message fields
|
|
return errorJson["message"] as String? ??
|
|
errorJson["error"] as String? ??
|
|
errorJson["detail"] as String?;
|
|
}
|
|
}
|
|
|
|
// Streaming response parser for handling streamed API responses
|
|
class StreamingResponseParser {
|
|
// parse a streamed event from newline-delimited JSON
|
|
static Map<String, dynamic>? parseStreamLine(String line) {
|
|
if (line.trim().isEmpty) return null;
|
|
|
|
try {
|
|
// handle SSE format (data: {...})
|
|
final data = line.startsWith("data: ") ? line.substring(6) : line;
|
|
// simple JSON parsing - in production would use json.decode
|
|
return _parseJson(data);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static Map<String, dynamic>? _parseJson(String jsonStr) {
|
|
// stubbed - would use dart:convert.jsonDecode in real impl
|
|
// for now just return null to indicate parsing would happen
|
|
return null;
|
|
}
|
|
|
|
// check if streamed event is a message delta (partial response)
|
|
static bool isMessageDelta(Map<String, dynamic>? event) {
|
|
if (event == null) return false;
|
|
final type = event["type"];
|
|
return type == "content_block_delta";
|
|
}
|
|
|
|
// check if streamed event marks message completion
|
|
static bool isMessageStop(Map<String, dynamic>? event) {
|
|
if (event == null) return false;
|
|
final type = event["type"];
|
|
return type == "message_stop";
|
|
}
|
|
|
|
// extract partial text from delta event
|
|
static String? extractDeltaText(Map<String, dynamic>? event) {
|
|
if (event == null) return null;
|
|
|
|
try {
|
|
final delta = event["delta"] as Map<String, dynamic>?;
|
|
if (delta == null) return null;
|
|
|
|
final type = delta["type"];
|
|
if (type == "text_delta") {
|
|
return delta["text"] as String?;
|
|
}
|
|
} catch (_) {}
|
|
|
|
return null;
|
|
}
|
|
}
|