@tduncan / Cortana / commits / 0c5bf3b

refactor: update parameter count to double precision, improve UI metadata filtering, and add Dio redirect handling for robust server communication

abdulmominsakib committed May 23, 2026 at 21:16 UTC 0c5bf3be341edbd903ce3ecd1487b625b1947917
8 files changed +120 -28
lib/core/providers/service_providers.dart
+83 -1
@@ -4,14 +4,96 @@ import '../constants/app_constants.dart';
4 import '../../features/servers/data/server_api_service.dart';
5
6 final dioProvider = Provider<Dio>((ref) {
7 - return Dio(
7 + final dio = Dio(
8 BaseOptions(
9 connectTimeout: Duration(milliseconds: AppConstants.connectionTimeoutMs),
10 receiveTimeout: Duration(milliseconds: AppConstants.receiveTimeoutMs),
11 ),
12 );
13 + dio.interceptors.add(RedirectInterceptor(dio));
14 + return dio;
15 });
16
17 final serverApiServiceProvider = Provider<ServerApiService>((ref) {
18 return ServerApiService(ref.read(dioProvider));
19 });
20 +
21 +class RedirectInterceptor extends Interceptor {
22 + final Dio _dio;
23 +
24 + RedirectInterceptor(this._dio);
25 +
26 + @override
27 + void onResponse(Response response, ResponseInterceptorHandler handler) async {
28 + final statusCode = response.statusCode;
29 + if (statusCode == 307 || statusCode == 308) {
30 + final location = response.headers.value('location');
31 + if (location != null) {
32 + try {
33 + final redirectedResponse = await _performRedirect(response.requestOptions, location);
34 + return handler.resolve(redirectedResponse);
35 + } catch (_) {
36 + return handler.next(response);
37 + }
38 + }
39 + }
40 + super.onResponse(response, handler);
41 + }
42 +
43 + @override
44 + void onError(DioException err, ErrorInterceptorHandler handler) async {
45 + final response = err.response;
46 + if (response != null && (response.statusCode == 307 || response.statusCode == 308)) {
47 + final location = response.headers.value('location');
48 + if (location != null) {
49 + try {
50 + final redirectedResponse = await _performRedirect(err.requestOptions, location);
51 + return handler.resolve(redirectedResponse);
52 + } catch (e) {
53 + if (e is DioException) {
54 + return handler.next(e);
55 + }
56 + return handler.next(DioException(
57 + requestOptions: err.requestOptions,
58 + error: e,
59 + type: DioExceptionType.unknown,
60 + ));
61 + }
62 + }
63 + }
64 + super.onError(err, handler);
65 + }
66 +
67 + Future<Response<dynamic>> _performRedirect(RequestOptions requestOptions, String location) async {
68 + var redirectUrl = location;
69 + if (!redirectUrl.startsWith('http://') && !redirectUrl.startsWith('https://')) {
70 + final baseUri = Uri.parse(requestOptions.path);
71 + redirectUrl = baseUri.resolve(redirectUrl).toString();
72 + }
73 +
74 + final extra = Map<String, dynamic>.from(requestOptions.extra);
75 + final redirectCount = (extra['redirectCount'] as int? ?? 0) + 1;
76 + if (redirectCount > 5) {
77 + throw DioException(
78 + requestOptions: requestOptions,
79 + error: 'Too many redirects',
80 + type: DioExceptionType.unknown,
81 + );
82 + }
83 + extra['redirectCount'] = redirectCount;
84 +
85 + return _dio.request(
86 + redirectUrl,
87 + data: requestOptions.data,
88 + queryParameters: requestOptions.queryParameters,
89 + options: Options(
90 + method: requestOptions.method,
91 + headers: requestOptions.headers,
92 + contentType: requestOptions.contentType,
93 + responseType: requestOptions.responseType,
94 + extra: extra,
95 + ),
96 + );
97 + }
98 +}
99 +
lib/features/models/data/models/model_info.dart
+10 -4
@@ -4,7 +4,7 @@ class ModelInfo {
4 final String id;
5 final String name;
6 final String? description;
7 - final int? parameterCount;
7 + final double? parameterCount;
8 final int? contextLength;
9 final int? fileSize;
10 final String? quantization;
@@ -42,7 +42,7 @@ class ModelInfo {
42 }
43
44 String? get formattedSize {
45 - if (fileSize == null) return null;
45 + if (fileSize == null || fileSize == 0) return null;
46 if (fileSize! < 1024 * 1024 * 1024) {
47 return '${(fileSize! / (1024 * 1024)).toStringAsFixed(1)} MB';
48 }
@@ -51,14 +51,20 @@ class ModelInfo {
51
52 String? get parameterCountDisplay {
53 if (parameterCount == null) return null;
54 - return '${parameterCount}B';
54 + final formatted = parameterCount!.toStringAsFixed(2);
55 + if (formatted.endsWith('.00')) {
56 + return '${parameterCount!.toInt()}B';
57 + } else if (formatted.endsWith('0')) {
58 + return '${parameterCount!.toStringAsFixed(1)}B';
59 + }
60 + return '${parameterCount!.toStringAsFixed(2)}B';
61 }
62
63 ModelInfo copyWith({
64 String? id,
65 String? name,
66 String? description,
61 - int? parameterCount,
67 + double? parameterCount,
68 int? contextLength,
69 int? fileSize,
70 String? quantization,
lib/features/models/screens/model_picker_sheet.dart
+7 -7
@@ -478,17 +478,17 @@ class _ModelTile extends StatelessWidget {
478 spacing: 6,
479 runSpacing: 4,
480 children: [
481 - if (model.parameterCountDisplay != null)
481 + if (model.parameterCountDisplay != null && model.parameterCountDisplay!.isNotEmpty)
482 _MetadataChip(
483 label: model.parameterCountDisplay!,
484 isDark: isDark,
485 ),
486 - if (model.quantization != null)
486 + if (model.quantization != null && model.quantization!.isNotEmpty)
487 _MetadataChip(
488 label: model.quantization!,
489 isDark: isDark,
490 ),
491 - if (model.formattedSize != null)
491 + if (model.formattedSize != null && model.formattedSize!.isNotEmpty)
492 _MetadataChip(
493 label: model.formattedSize!,
494 isDark: isDark,
@@ -988,8 +988,8 @@ class _OnDeviceModelTile extends ConsumerWidget {
988 id: model.id,
989 name: model.name,
990 description: model.description,
991 - parameterCount: int.tryParse(
992 - model.parameterLabel.replaceAll(RegExp(r'[^0-9]'), ''),
991 + parameterCount: double.tryParse(
992 + model.parameterLabel.replaceAll(RegExp(r'[^0-9\.]'), ''),
993 ),
994 serverType: ServerType.onDevice,
995 serverId: 'on-device',
@@ -1045,8 +1045,8 @@ class _OnDeviceModelTile extends ConsumerWidget {
1045 id: model.id,
1046 name: model.name,
1047 description: model.description,
1048 - parameterCount: int.tryParse(
1049 - model.parameterLabel.replaceAll(RegExp(r'[^0-9]'), ''),
1048 + parameterCount: double.tryParse(
1049 + model.parameterLabel.replaceAll(RegExp(r'[^0-9\.]'), ''),
1050 ),
1051 serverType: ServerType.onDevice,
1052 serverId: 'on-device',
lib/features/servers/data/models/server.dart
+6 -1
@@ -34,7 +34,12 @@ class Server {
34 if (type == ServerType.onDevice) {
35 return 'on-device';
36 }
37 - return 'http://$host:$port';
37 + final trimmedHost = host.trim();
38 + if (trimmedHost.startsWith('http://') || trimmedHost.startsWith('https://')) {
39 + return '$trimmedHost:$port';
40 + }
41 + final protocol = (port == 443) ? 'https' : 'http';
42 + return '$protocol://$trimmedHost:$port';
43 }
44
45 String get chatEndpoint {
lib/features/servers/data/server_api_service.dart
+8 -11
@@ -286,15 +286,13 @@ class ServerApiService {
286 return models;
287 }
288
289 - int? _paramCountFromMeta(dynamic nParams) {
289 + double? _paramCountFromMeta(dynamic nParams) {
290 if (nParams == null) return null;
291 - if (nParams is int) return nParams ~/ 1000000000;
292 - if (nParams is double) return (nParams / 1000000000).round();
291 + if (nParams is int) return nParams / 1000000000;
292 + if (nParams is double) return nParams / 1000000000;
293 if (nParams is String) {
294 - final parsedInt = int.tryParse(nParams);
295 - if (parsedInt != null) return parsedInt ~/ 1000000000;
294 final parsedDouble = double.tryParse(nParams);
297 - if (parsedDouble != null) return (parsedDouble / 1000000000).round();
295 + if (parsedDouble != null) return parsedDouble / 1000000000;
296 }
297 return null;
298 }
@@ -358,18 +356,17 @@ class ServerApiService {
356 .join(' ');
357 }
358
361 - int? _parseParameterSize(String size) {
359 + double? _parseParameterSize(String size) {
360 final cleaned = size.replaceAll('B', '').replaceAll('b', '').trim();
363 - return int.tryParse(cleaned);
361 + return double.tryParse(cleaned);
362 }
363
366 - int? _parseParameterString(String? paramsString) {
364 + double? _parseParameterString(String? paramsString) {
365 if (paramsString == null) return null;
366 final regex = RegExp(r'(\d+(?:\.\d+)?)\s*[Bb]');
367 final match = regex.firstMatch(paramsString);
368 if (match != null) {
371 - final value = double.tryParse(match.group(1) ?? '');
372 - return value?.toInt();
369 + return double.tryParse(match.group(1) ?? '');
370 }
371 return null;
372 }
lib/features/servers/providers/server_providers.dart
+2 -2
@@ -120,8 +120,8 @@ final availableModelsProvider = FutureProvider.family<List<dynamic>, String>((
120 id: m.id,
121 name: m.name,
122 description: m.description,
123 - parameterCount: int.tryParse(
124 - m.parameterLabel.replaceAll(RegExp(r'[^0-9]'), ''),
123 + parameterCount: double.tryParse(
124 + m.parameterLabel.replaceAll(RegExp(r'[^0-9\.]'), ''),
125 ),
126 serverType: ServerType.onDevice,
127 serverId: server.id,
lib/features/servers/views/add_server_screen.dart
+3 -1
@@ -193,8 +193,10 @@ class _AddServerScreenState extends ConsumerState<AddServerScreen> {
193 if (value == null || value.trim().isEmpty) {
194 return l10n.host_required;
195 }
196 + // Clean any protocol prefixes (http:// or https://) for validation
197 + final cleaned = value.trim().replaceFirst(RegExp(r'^https?://'), '');
198 final hostPattern = RegExp(r'^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$');
197 - if (!hostPattern.hasMatch(value.trim())) {
199 + if (!hostPattern.hasMatch(cleaned)) {
200 return l10n.host_valid;
201 }
202 return null;
pubspec.yaml
+1 -1
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
16 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17 # In Windows, build-name is used as the major, minor, and patch parts
18 # of the product and file versions while build-number is used as the build suffix.
19 -version: 1.2.8+27
19 +version: 1.2.9+28
20
21 environment:
22 sdk: ^3.11.0