feat(models): Add safe parsing for boolean and integer values
This commit is contained in:
@@ -69,9 +69,9 @@ abstract class ChatStatusUpdate with _$ChatStatusUpdate {
|
|||||||
const factory ChatStatusUpdate({
|
const factory ChatStatusUpdate({
|
||||||
String? action,
|
String? action,
|
||||||
String? description,
|
String? description,
|
||||||
bool? done,
|
@JsonKey(fromJson: _safeBool) bool? done,
|
||||||
bool? hidden,
|
@JsonKey(fromJson: _safeBool) bool? hidden,
|
||||||
int? count,
|
@JsonKey(fromJson: _safeInt) int? count,
|
||||||
String? query,
|
String? query,
|
||||||
@JsonKey(fromJson: _safeStringList, toJson: _stringListToJson)
|
@JsonKey(fromJson: _safeStringList, toJson: _stringListToJson)
|
||||||
@Default(<String>[])
|
@Default(<String>[])
|
||||||
@@ -178,6 +178,29 @@ List<String> _safeStringList(dynamic value) {
|
|||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Safely parse a boolean from various formats (bool, String, int).
|
||||||
|
bool? _safeBool(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is bool) return value;
|
||||||
|
if (value is String) {
|
||||||
|
final lower = value.toLowerCase();
|
||||||
|
if (lower == 'true' || lower == '1') return true;
|
||||||
|
if (lower == 'false' || lower == '0') return false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value is num) return value != 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safely parse an integer from various formats (int, double, String).
|
||||||
|
int? _safeInt(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is int) return value;
|
||||||
|
if (value is double) return value.toInt();
|
||||||
|
if (value is String) return int.tryParse(value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
List<String> _stringListToJson(List<String> value) =>
|
List<String> _stringListToJson(List<String> value) =>
|
||||||
List<String>.from(value, growable: false);
|
List<String>.from(value, growable: false);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
|
|
||||||
part 'folder.freezed.dart';
|
part 'folder.freezed.dart';
|
||||||
|
|
||||||
|
bool? _safeBool(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is bool) return value;
|
||||||
|
if (value is String) {
|
||||||
|
final lower = value.toLowerCase();
|
||||||
|
if (lower == 'true' || lower == '1') return true;
|
||||||
|
if (lower == 'false' || lower == '0') return false;
|
||||||
|
}
|
||||||
|
if (value is num) return value != 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
sealed class Folder with _$Folder {
|
sealed class Folder with _$Folder {
|
||||||
const factory Folder({
|
const factory Folder({
|
||||||
@@ -72,7 +84,7 @@ sealed class Folder with _$Folder {
|
|||||||
userId: json['user_id'] as String?,
|
userId: json['user_id'] as String?,
|
||||||
createdAt: parseTimestamp(json['created_at']),
|
createdAt: parseTimestamp(json['created_at']),
|
||||||
updatedAt: parseTimestamp(json['updated_at']),
|
updatedAt: parseTimestamp(json['updated_at']),
|
||||||
isExpanded: json['is_expanded'] as bool? ?? false,
|
isExpanded: _safeBool(json['is_expanded']) ?? false,
|
||||||
conversationIds: conversationIds,
|
conversationIds: conversationIds,
|
||||||
meta: json['meta'] as Map<String, dynamic>?,
|
meta: json['meta'] as Map<String, dynamic>?,
|
||||||
data: json['data'] as Map<String, dynamic>?,
|
data: json['data'] as Map<String, dynamic>?,
|
||||||
|
|||||||
@@ -3,6 +3,18 @@ import 'toggle_filter.dart';
|
|||||||
|
|
||||||
part 'model.freezed.dart';
|
part 'model.freezed.dart';
|
||||||
|
|
||||||
|
bool? _safeBool(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is bool) return value;
|
||||||
|
if (value is String) {
|
||||||
|
final lower = value.toLowerCase();
|
||||||
|
if (lower == 'true' || lower == '1') return true;
|
||||||
|
if (lower == 'false' || lower == '0') return false;
|
||||||
|
}
|
||||||
|
if (value is num) return value != 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
sealed class Model with _$Model {
|
sealed class Model with _$Model {
|
||||||
const Model._();
|
const Model._();
|
||||||
@@ -180,7 +192,7 @@ sealed class Model with _$Model {
|
|||||||
description: json['description'] as String?,
|
description: json['description'] as String?,
|
||||||
isMultimodal: isMultimodal,
|
isMultimodal: isMultimodal,
|
||||||
supportsStreaming: supportsStreaming,
|
supportsStreaming: supportsStreaming,
|
||||||
supportsRAG: json['supportsRAG'] as bool? ?? false,
|
supportsRAG: _safeBool(json['supportsRAG']) ?? false,
|
||||||
supportedParameters: supportedParamsList,
|
supportedParameters: supportedParamsList,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
'architecture': architecture,
|
'architecture': architecture,
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
|
|
||||||
part 'toggle_filter.freezed.dart';
|
part 'toggle_filter.freezed.dart';
|
||||||
|
|
||||||
|
bool? _safeBool(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is bool) return value;
|
||||||
|
if (value is String) {
|
||||||
|
final lower = value.toLowerCase();
|
||||||
|
if (lower == 'true' || lower == '1') return true;
|
||||||
|
if (lower == 'false' || lower == '0') return false;
|
||||||
|
}
|
||||||
|
if (value is num) return value != 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents a toggleable filter that can be enabled/disabled per chat.
|
/// Represents a toggleable filter that can be enabled/disabled per chat.
|
||||||
///
|
///
|
||||||
/// These filters are created by OpenWebUI when a filter function has
|
/// These filters are created by OpenWebUI when a filter function has
|
||||||
@@ -34,7 +46,7 @@ sealed class ToggleFilter with _$ToggleFilter {
|
|||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
description: json['description'] as String?,
|
description: json['description'] as String?,
|
||||||
icon: json['icon'] as String?,
|
icon: json['icon'] as String?,
|
||||||
hasUserValves: json['has_user_valves'] as bool? ?? false,
|
hasUserValves: _safeBool(json['has_user_valves']) ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
|
|
||||||
part 'user.freezed.dart';
|
part 'user.freezed.dart';
|
||||||
|
|
||||||
|
bool? _safeBool(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is bool) return value;
|
||||||
|
if (value is String) {
|
||||||
|
final lower = value.toLowerCase();
|
||||||
|
if (lower == 'true' || lower == '1') return true;
|
||||||
|
if (lower == 'false' || lower == '0') return false;
|
||||||
|
}
|
||||||
|
if (value is num) return value != 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
sealed class User with _$User {
|
sealed class User with _$User {
|
||||||
const User._();
|
const User._();
|
||||||
@@ -27,7 +39,8 @@ sealed class User with _$User {
|
|||||||
json['profile_image_url'] as String? ??
|
json['profile_image_url'] as String? ??
|
||||||
json['profileImage'] as String?,
|
json['profileImage'] as String?,
|
||||||
role: json['role'] as String? ?? 'user',
|
role: json['role'] as String? ?? 'user',
|
||||||
isActive: json['is_active'] as bool? ?? json['isActive'] as bool? ?? true,
|
isActive:
|
||||||
|
_safeBool(json['is_active']) ?? _safeBool(json['isActive']) ?? true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -913,6 +913,15 @@ class ApiService {
|
|||||||
if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty)
|
if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty)
|
||||||
'attachment_ids': List<String>.from(msg.attachmentIds!),
|
'attachment_ids': List<String>.from(msg.attachmentIds!),
|
||||||
if (sanitizedFiles != null) 'files': sanitizedFiles,
|
if (sanitizedFiles != null) 'files': sanitizedFiles,
|
||||||
|
// Mirror status updates, follow-ups, code executions, and sources
|
||||||
|
if (msg.statusHistory.isNotEmpty)
|
||||||
|
'statusHistory': msg.statusHistory.map((s) => s.toJson()).toList(),
|
||||||
|
if (msg.followUps.isNotEmpty)
|
||||||
|
'followUps': List<String>.from(msg.followUps),
|
||||||
|
if (msg.codeExecutions.isNotEmpty)
|
||||||
|
'codeExecutions': msg.codeExecutions.map((e) => e.toJson()).toList(),
|
||||||
|
if (msg.sources.isNotEmpty)
|
||||||
|
'sources': msg.sources.map((s) => s.toJson()).toList(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update parent's childrenIds
|
// Update parent's childrenIds
|
||||||
@@ -939,6 +948,15 @@ class ApiService {
|
|||||||
if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty)
|
if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty)
|
||||||
'attachment_ids': List<String>.from(msg.attachmentIds!),
|
'attachment_ids': List<String>.from(msg.attachmentIds!),
|
||||||
if (sanitizedArrayFiles != null) 'files': sanitizedArrayFiles,
|
if (sanitizedArrayFiles != null) 'files': sanitizedArrayFiles,
|
||||||
|
// Mirror status updates, follow-ups, code executions, and sources
|
||||||
|
if (msg.statusHistory.isNotEmpty)
|
||||||
|
'statusHistory': msg.statusHistory.map((s) => s.toJson()).toList(),
|
||||||
|
if (msg.followUps.isNotEmpty)
|
||||||
|
'followUps': List<String>.from(msg.followUps),
|
||||||
|
if (msg.codeExecutions.isNotEmpty)
|
||||||
|
'codeExecutions': msg.codeExecutions.map((e) => e.toJson()).toList(),
|
||||||
|
if (msg.sources.isNotEmpty)
|
||||||
|
'sources': msg.sources.map((s) => s.toJson()).toList(),
|
||||||
});
|
});
|
||||||
|
|
||||||
previousId = messageId;
|
previousId = messageId;
|
||||||
@@ -965,6 +983,15 @@ class ApiService {
|
|||||||
'modelIdx': 0,
|
'modelIdx': 0,
|
||||||
'done': true,
|
'done': true,
|
||||||
if (ver.files != null) 'files': _sanitizeFilesForWebUI(ver.files),
|
if (ver.files != null) 'files': _sanitizeFilesForWebUI(ver.files),
|
||||||
|
// Mirror follow-ups, code executions, and sources for versions
|
||||||
|
if (ver.followUps.isNotEmpty)
|
||||||
|
'followUps': List<String>.from(ver.followUps),
|
||||||
|
if (ver.codeExecutions.isNotEmpty)
|
||||||
|
'codeExecutions': ver.codeExecutions
|
||||||
|
.map((e) => e.toJson())
|
||||||
|
.toList(),
|
||||||
|
if (ver.sources.isNotEmpty)
|
||||||
|
'sources': ver.sources.map((s) => s.toJson()).toList(),
|
||||||
};
|
};
|
||||||
// Link into parent (parentForVersions is always non-null here)
|
// Link into parent (parentForVersions is always non-null here)
|
||||||
if (messagesMap.containsKey(parentForVersions)) {
|
if (messagesMap.containsKey(parentForVersions)) {
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ Map<String, dynamic> parseConversationSummary(Map<String, dynamic> chatData) {
|
|||||||
final updatedAtRaw = chatData['updated_at'] ?? chatData['updatedAt'];
|
final updatedAtRaw = chatData['updated_at'] ?? chatData['updatedAt'];
|
||||||
final createdAtRaw = chatData['created_at'] ?? chatData['createdAt'];
|
final createdAtRaw = chatData['created_at'] ?? chatData['createdAt'];
|
||||||
|
|
||||||
final pinned = chatData['pinned'] as bool? ?? false;
|
final pinned = _safeBool(chatData['pinned']) ?? false;
|
||||||
final archived = chatData['archived'] as bool? ?? false;
|
final archived = _safeBool(chatData['archived']) ?? false;
|
||||||
final shareId = chatData['share_id']?.toString();
|
final shareId = chatData['share_id']?.toString();
|
||||||
final folderId = chatData['folder_id']?.toString();
|
final folderId = chatData['folder_id']?.toString();
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ Map<String, dynamic> parseFullConversation(Map<String, dynamic> chatData) {
|
|||||||
final createdAt = _parseTimestamp(
|
final createdAt = _parseTimestamp(
|
||||||
chatData['created_at'] ?? chatData['createdAt'],
|
chatData['created_at'] ?? chatData['createdAt'],
|
||||||
);
|
);
|
||||||
final pinned = chatData['pinned'] as bool? ?? false;
|
final pinned = _safeBool(chatData['pinned']) ?? false;
|
||||||
final archived = chatData['archived'] as bool? ?? false;
|
final archived = _safeBool(chatData['archived']) ?? false;
|
||||||
final shareId = chatData['share_id']?.toString();
|
final shareId = chatData['share_id']?.toString();
|
||||||
final folderId = chatData['folder_id']?.toString();
|
final folderId = chatData['folder_id']?.toString();
|
||||||
|
|
||||||
@@ -289,20 +289,17 @@ Map<String, dynamic> _parseOpenWebUIMessageToJson(
|
|||||||
files = allFiles.isNotEmpty ? allFiles : null;
|
files = allFiles.isNotEmpty ? allFiles : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final statusHistoryRaw =
|
final statusHistoryRaw = historyMsg != null
|
||||||
historyMsg != null && historyMsg.containsKey('statusHistory')
|
? historyMsg['statusHistory'] ?? historyMsg['status_history']
|
||||||
? historyMsg['statusHistory']
|
: msgData['statusHistory'] ?? msgData['status_history'];
|
||||||
: msgData['statusHistory'];
|
final followUpsRaw = historyMsg != null
|
||||||
final followUpsRaw = historyMsg != null && historyMsg.containsKey('followUps')
|
? historyMsg['followUps'] ?? historyMsg['follow_ups']
|
||||||
? historyMsg['followUps']
|
|
||||||
: msgData['followUps'] ?? msgData['follow_ups'];
|
: msgData['followUps'] ?? msgData['follow_ups'];
|
||||||
final codeExecRaw = historyMsg != null
|
final codeExecRaw = historyMsg != null
|
||||||
? historyMsg['code_executions'] ?? historyMsg['codeExecutions']
|
? historyMsg['codeExecutions'] ?? historyMsg['code_executions']
|
||||||
: msgData['code_executions'] ?? msgData['codeExecutions'];
|
: msgData['codeExecutions'] ?? msgData['code_executions'];
|
||||||
final sourcesRaw = historyMsg != null
|
final sourcesRaw = historyMsg != null
|
||||||
? historyMsg.containsKey('sources')
|
? historyMsg['sources'] ?? historyMsg['citations']
|
||||||
? historyMsg['sources']
|
|
||||||
: historyMsg['citations']
|
|
||||||
: msgData['sources'] ?? msgData['citations'];
|
: msgData['sources'] ?? msgData['citations'];
|
||||||
|
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
@@ -311,7 +308,7 @@ Map<String, dynamic> _parseOpenWebUIMessageToJson(
|
|||||||
'content': contentString,
|
'content': contentString,
|
||||||
'timestamp': _parseTimestamp(msgData['timestamp']).toIso8601String(),
|
'timestamp': _parseTimestamp(msgData['timestamp']).toIso8601String(),
|
||||||
'model': msgData['model']?.toString(),
|
'model': msgData['model']?.toString(),
|
||||||
'isStreaming': msgData['isStreaming'] as bool? ?? false,
|
'isStreaming': _safeBool(msgData['isStreaming']) ?? false,
|
||||||
if (attachmentIds != null) 'attachmentIds': attachmentIds,
|
if (attachmentIds != null) 'attachmentIds': attachmentIds,
|
||||||
if (files != null) 'files': files,
|
if (files != null) 'files': files,
|
||||||
'metadata': _coerceJsonMap(msgData['metadata']),
|
'metadata': _coerceJsonMap(msgData['metadata']),
|
||||||
@@ -384,10 +381,16 @@ DateTime _parseTimestamp(dynamic timestamp) {
|
|||||||
|
|
||||||
List<Map<String, dynamic>> _parseStatusHistoryField(dynamic raw) {
|
List<Map<String, dynamic>> _parseStatusHistoryField(dynamic raw) {
|
||||||
if (raw is List) {
|
if (raw is List) {
|
||||||
return raw
|
final results = <Map<String, dynamic>>[];
|
||||||
.whereType<Map>()
|
for (final entry in raw) {
|
||||||
.map((entry) => _coerceJsonMap(entry))
|
if (entry is! Map) continue;
|
||||||
.toList(growable: false);
|
try {
|
||||||
|
results.add(_coerceJsonMap(entry));
|
||||||
|
} catch (_) {
|
||||||
|
// Skip malformed status entries to prevent error boundary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
return const <Map<String, dynamic>>[];
|
return const <Map<String, dynamic>>[];
|
||||||
}
|
}
|
||||||
@@ -508,6 +511,20 @@ String _stringOr(dynamic value, String fallback) {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Safely parse a boolean from various formats (bool, String, int).
|
||||||
|
bool? _safeBool(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is bool) return value;
|
||||||
|
if (value is String) {
|
||||||
|
final lower = value.toLowerCase();
|
||||||
|
if (lower == 'true' || lower == '1') return true;
|
||||||
|
if (lower == 'false' || lower == '0') return false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value is num) return value != 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
String _synthesizeToolDetailsFromToolCalls(List<Map> calls) {
|
String _synthesizeToolDetailsFromToolCalls(List<Map> calls) {
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
for (final rawCall in calls) {
|
for (final rawCall in calls) {
|
||||||
|
|||||||
Reference in New Issue
Block a user