2 files changed
+147
-8
lib/features/servers/data/server_api_service.dart
+59
-8
@@ -195,7 +195,20 @@ class ServerApiService {
195
options: Options(headers: buildServerAuthHeaders(server)),
196
);
197
_throwIfErrorResponse(response.data);
198
- return response.data['instance_id'] as String?;
198
+ final data = response.data;
199
+ dynamic map = data;
200
+ if (data is String && data.trim().startsWith('{')) {
201
+ try {
202
+ map = jsonDecode(data);
203
+ } catch (_) {}
204
+ }
205
+ if (map is Map) {
206
+ return map['instance_id']?.toString() ??
207
+ map['id']?.toString() ??
208
+ map['model']?.toString() ??
209
+ modelId;
210
+ }
211
+ return modelId;
212
} on DioException catch (e) {
213
throw Exception(_extractApiErrorMessage(e.response?.data) ?? e.message);
214
}
@@ -698,9 +711,32 @@ class ServerApiService {
711
}
712
713
void _throwIfErrorResponse(dynamic data) {
701
- final message = _extractApiErrorMessage(data);
702
- if (message != null) {
703
- throw Exception(message);
714
+ if (data == null) return;
715
+ dynamic map = data;
716
+ if (data is String) {
717
+ final trimmed = data.trim();
718
+ if (trimmed.startsWith('{')) {
719
+ try {
720
+ map = jsonDecode(trimmed);
721
+ } catch (_) {
722
+ return;
723
+ }
724
+ } else {
725
+ return;
726
+ }
727
+ }
728
+ if (map is! Map) return;
729
+
730
+ final hasError = map.containsKey('error') && map['error'] != null;
731
+ final isFailedStatus = map['status'] == 'error' ||
732
+ map['status'] == 'failed' ||
733
+ map['success'] == false;
734
+
735
+ if (hasError || isFailedStatus) {
736
+ final message = _extractApiErrorMessage(map);
737
+ if (message != null && message.isNotEmpty) {
738
+ throw Exception(message);
739
+ }
740
}
741
}
742
@@ -738,16 +774,31 @@ class ServerApiService {
774
775
final message = map['message']?.toString();
776
final type = map['type']?.toString();
741
- final detail = map['detail']?.toString();
777
+ final detail = map['detail'];
778
if (message != null && message.trim().isNotEmpty) {
779
if (type != null && type.trim().isNotEmpty) return '$type: $message';
780
return message;
781
}
746
- if (detail != null && detail.trim().isNotEmpty) {
782
+
783
+ final errorDescription = map['error_description']?.toString();
784
+ if (errorDescription != null && errorDescription.trim().isNotEmpty) {
785
+ return errorDescription;
786
+ }
787
+
788
+ if (detail is String && detail.trim().isNotEmpty) {
789
+ if (type != null && type.trim().isNotEmpty) return '$type: $detail';
790
return detail;
791
+ } else if (detail is List && detail.isNotEmpty) {
792
+ final msgs = detail
793
+ .map((d) => d is Map ? d['msg']?.toString() : d?.toString())
794
+ .where((m) => m != null && m.trim().isNotEmpty)
795
+ .join(', ');
796
+ if (msgs.isNotEmpty) return msgs;
797
}
749
- if (type != null && type.trim().isNotEmpty) {
750
- return type;
798
+
799
+ final title = map['title']?.toString();
800
+ if (title != null && title.trim().isNotEmpty) {
801
+ return title;
802
}
803
804
return null;
test/features/servers/server_api_service_test.dart
+88
@@ -679,4 +679,92 @@ void main() {
679
expect(models.single.pricingLabel, isNull);
680
});
681
});
682
+
683
+ group('ServerApiService - LM Studio load model behavior', () {
684
+ late Server lmStudioServer;
685
+
686
+ setUp(() {
687
+ lmStudioServer = Server(
688
+ id: 'test-lmstudio',
689
+ name: 'Test LM Studio',
690
+ type: ServerType.lmStudio,
691
+ host: 'localhost',
692
+ port: 1234,
693
+ createdAt: DateTime.now(),
694
+ lastConnectedAt: DateTime.now(),
695
+ );
696
+ });
697
+
698
+ test('loadModelWithInstanceId succeeds with LM Studio response having "type": "llm"', () async {
699
+ final successData = {
700
+ 'type': 'llm',
701
+ 'instance_id': 'liquid/lfm2-1.2b',
702
+ 'load_config': {
703
+ 'context_length': 4096,
704
+ },
705
+ };
706
+
707
+ final dio = Dio()..interceptors.add(TestInterceptor(successData));
708
+ final service = ServerApiService(dio);
709
+
710
+ final instanceId = await service.loadModelWithInstanceId(
711
+ lmStudioServer,
712
+ 'liquid/lfm2-1.2b',
713
+ contextLength: 4096,
714
+ );
715
+
716
+ expect(instanceId, 'liquid/lfm2-1.2b');
717
+ });
718
+
719
+ test('loadModelWithInstanceId throws when response contains error object', () async {
720
+ final errorData = {
721
+ 'error': {
722
+ 'type': 'invalid_request_error',
723
+ 'message': 'Context length exceeds model limit',
724
+ },
725
+ };
726
+
727
+ final dio = Dio()..interceptors.add(TestInterceptor(errorData));
728
+ final service = ServerApiService(dio);
729
+
730
+ expect(
731
+ () => service.loadModelWithInstanceId(
732
+ lmStudioServer,
733
+ 'liquid/lfm2-1.2b',
734
+ contextLength: 999999,
735
+ ),
736
+ throwsA(
737
+ predicate(
738
+ (e) =>
739
+ e is Exception &&
740
+ e.toString().contains('invalid_request_error: Context length exceeds model limit'),
741
+ ),
742
+ ),
743
+ );
744
+ });
745
+
746
+ test('loadModelWithInstanceId throws when response contains status error', () async {
747
+ final errorData = {
748
+ 'status': 'error',
749
+ 'message': 'Failed to allocate memory',
750
+ };
751
+
752
+ final dio = Dio()..interceptors.add(TestInterceptor(errorData));
753
+ final service = ServerApiService(dio);
754
+
755
+ expect(
756
+ () => service.loadModelWithInstanceId(
757
+ lmStudioServer,
758
+ 'liquid/lfm2-1.2b',
759
+ ),
760
+ throwsA(
761
+ predicate(
762
+ (e) =>
763
+ e is Exception &&
764
+ e.toString().contains('Failed to allocate memory'),
765
+ ),
766
+ ),
767
+ );
768
+ });
769
+ });
770
}