// 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 json) { return ApiMessage.fromJson(json); } static ApiMessage parseOpenRouterResponse(Map json) { return ApiMessage.fromOpenRouterResponse(json); } // extract text content from message static String extractTextContent(ApiMessage message) { final textBlocks = []; for (final block in message.content) { if (block is Map) { 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 extractToolUseBlocks(ApiMessage message) { final tools = []; for (final block in message.content) { if (block is Map) { 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 && 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? errorJson) { if (errorJson == null) return null; final nestedError = errorJson["error"]; if (nestedError is Map) { 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? 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? _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? 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? event) { if (event == null) return false; final type = event["type"]; return type == "message_stop"; } // extract partial text from delta event static String? extractDeltaText(Map? event) { if (event == null) return null; try { final delta = event["delta"] as Map?; if (delta == null) return null; final type = delta["type"]; if (type == "text_delta") { return delta["text"] as String?; } } catch (_) {} return null; } }