fix(chat): scope reasoning controls by provider
Strict OpenAI-compatible servers reject the provider-specific aliases that were sent alongside reasoning_effort. Use only the documented payload for each provider and require explicit capabilities for non-OpenAI model families so optional reasoning does not break otherwise valid requests.
abdulmominsakib committed
Sep 21, 2026 at 11:54 UTC
dfb5b04730d0277e295141abe873a7d93efaf5a0
4 files changed
+111
-65
lib/features/chat/data/chat_service.dart
+24
-25
@@ -674,7 +674,7 @@ class OpenAICompatibleChatService implements ChatService {
674
'max_tokens': params.maxTokens,
675
'stream': true,
676
};
677
- _applyReasoningControl(body, params);
677
+ _applyOpenAICompatibleReasoningControl(body, params);
678
679
if (integrations != null && integrations.isNotEmpty) {
680
body['integrations'] = integrations.map((i) => i.toApiJson()).toList();
@@ -1219,7 +1219,7 @@ class OpenRouterChatService implements ChatService {
1219
'max_tokens': params.maxTokens,
1220
'stream': true,
1221
};
1222
- _applyReasoningControl(body, params);
1222
+ _applyOpenRouterReasoningControl(body, params);
1223
1224
if (tools != null && tools.isNotEmpty) {
1225
final toolsPayload = toolAdapter.buildToolDefinitionPayload(tools);
@@ -1415,32 +1415,31 @@ class OpenRouterChatService implements ChatService {
1415
}
1416
}
1417
1418
-/// Applies the Think toggle to a request body. Different local/hosted
1419
-/// backends expose "disable reasoning for this hybrid model" a handful of
1420
-/// different ways (a `reasoning` object, a top-level `reasoning_effort`,
1421
-/// DeepSeek's `thinking` object, llama.cpp's `enable_thinking`) — send all of
1422
-/// them so whichever one the connected server actually understands takes effect.
1423
-/// No-op when [ChatParameters.reasoningEnabled] is null, i.e. the active
1424
-/// model doesn't support reasoning.
1425
-///
1426
-/// Ollama has its own native `think` field and must not receive the generic
1427
-/// keys; use [_applyOllamaReasoningControl] for [OllamaChatService].
1428
-void _applyReasoningControl(Map<String, dynamic> body, ChatParameters params) {
1418
+/// Applies the standard OpenAI-compatible `reasoning_effort` field. Avoid
1419
+/// broadcasting provider-specific aliases because strict compatible servers
1420
+/// reject unknown request keys.
1421
+void _applyOpenAICompatibleReasoningControl(
1422
+ Map<String, dynamic> body,
1423
+ ChatParameters params,
1424
+) {
1425
if (params.reasoningEnabled == false) {
1430
- body['reasoning'] = {
1431
- 'enabled': false,
1432
- 'type': 'disabled',
1433
- 'effort': 'none',
1434
- };
1435
- body['thinking'] = {'type': 'disabled'};
1426
body['reasoning_effort'] = 'none';
1437
- body['think'] = false;
1438
- body['enable_thinking'] = false;
1427
} else if (params.reasoningEnabled == true) {
1440
- final effort = params.reasoningEffort.apiValue;
1441
- body['reasoning'] = {'effort': effort};
1442
- body['thinking'] = {'type': 'enabled'};
1443
- body['reasoning_effort'] = effort;
1428
+ body['reasoning_effort'] = params.reasoningEffort.apiValue;
1429
+ }
1430
+}
1431
+
1432
+/// OpenRouter defines a `reasoning` object. Use that object alone instead of
1433
+/// mixing it with the shorthand `reasoning_effort` or unrelated local-server
1434
+/// flags.
1435
+void _applyOpenRouterReasoningControl(
1436
+ Map<String, dynamic> body,
1437
+ ChatParameters params,
1438
+) {
1439
+ if (params.reasoningEnabled == false) {
1440
+ body['reasoning'] = {'enabled': false};
1441
+ } else if (params.reasoningEnabled == true) {
1442
+ body['reasoning'] = {'effort': params.reasoningEffort.apiValue};
1443
}
1444
}
1445
lib/features/servers/data/server_api_service.dart
+1
-17
@@ -372,23 +372,7 @@ class ServerApiService {
372
if (RegExp(r'(^|[/:\-_])o[1-9]([-_/:.]|$)').hasMatch(lower)) {
373
return true;
374
}
375
- if (lower.contains('deepseek-reasoner') ||
376
- lower.contains('deepseek-r1') ||
377
- RegExp(r'(^|[-_/])r1([-_/.]|$)').hasMatch(lower)) {
378
- return true;
379
- }
380
- if (lower.contains('qwq') || lower.contains('qvq')) {
381
- return true;
382
- }
383
- if (lower.contains('reasoner') ||
384
- lower.contains('reasoning') ||
385
- lower.contains('thinking') ||
386
- lower.contains('thought') ||
387
- lower.contains('marco-o1') ||
388
- lower.contains('skywork-o1')) {
389
- return true;
390
- }
391
- return false;
375
+ return RegExp(r'(^|[/_\-])gpt-[5-9]([._:/\-]|$)').hasMatch(lower);
376
}
377
378
List<ModelInfo> _parseOpenAICompatibleModels(dynamic data, Server server) {
test/features/chat/tools/chat_service_adapter_selection_test.dart
+75
-12
@@ -746,7 +746,7 @@ void main() {
746
);
747
748
test(
749
- 'sends reasoning_effort and thinking when reasoning is enabled',
749
+ 'sends only standard reasoning_effort when reasoning is enabled',
750
() async {
751
final interceptor = CapturingStreamInterceptor(['data: [DONE]']);
752
final service = OpenAICompatibleChatService(
@@ -775,13 +775,15 @@ void main() {
775
776
final body = interceptor.capturedRequest!.data as Map<String, dynamic>;
777
expect(body['reasoning_effort'], 'low');
778
- expect(body['reasoning'], {'effort': 'low'});
779
- expect(body['thinking'], {'type': 'enabled'});
778
+ expect(body.containsKey('reasoning'), isFalse);
779
+ expect(body.containsKey('thinking'), isFalse);
780
+ expect(body.containsKey('think'), isFalse);
781
+ expect(body.containsKey('enable_thinking'), isFalse);
782
},
783
);
784
785
test(
784
- 'sends reasoning_effort:none and disabled flags when reasoning is disabled',
786
+ 'sends only reasoning_effort:none when reasoning is disabled',
787
() async {
788
final interceptor = CapturingStreamInterceptor(['data: [DONE]']);
789
final service = OpenAICompatibleChatService(
@@ -809,14 +811,10 @@ void main() {
811
812
final body = interceptor.capturedRequest!.data as Map<String, dynamic>;
813
expect(body['reasoning_effort'], 'none');
812
- expect(body['reasoning'], {
813
- 'enabled': false,
814
- 'type': 'disabled',
815
- 'effort': 'none',
816
- });
817
- expect(body['thinking'], {'type': 'disabled'});
818
- expect(body['think'], isFalse);
819
- expect(body['enable_thinking'], isFalse);
814
+ expect(body.containsKey('reasoning'), isFalse);
815
+ expect(body.containsKey('thinking'), isFalse);
816
+ expect(body.containsKey('think'), isFalse);
817
+ expect(body.containsKey('enable_thinking'), isFalse);
818
},
819
);
820
@@ -952,6 +950,71 @@ void main() {
950
'Summarize this\n\n--- notes.txt ---\nattachment text',
951
);
952
});
953
+
954
+ test('uses only OpenRouter reasoning configuration when enabled', () async {
955
+ final interceptor = CapturingStreamInterceptor(['data: [DONE]']);
956
+ final service = OpenRouterChatService(
957
+ Dio()..interceptors.add(interceptor),
958
+ );
959
+
960
+ await service
961
+ .sendMessage(
962
+ server: _openRouterTestServer(),
963
+ modelId: 'openai/o3',
964
+ messages: [
965
+ Message(
966
+ id: 'reasoning-message',
967
+ conversationId: 'conversation',
968
+ role: MessageRole.user,
969
+ content: 'Think carefully',
970
+ createdAt: DateTime.now(),
971
+ ),
972
+ ],
973
+ params: ChatParameters.defaults().copyWith(
974
+ reasoningEnabled: true,
975
+ reasoningEffort: ReasoningEffort.high,
976
+ ),
977
+ )
978
+ .toList();
979
+
980
+ final body = interceptor.capturedRequest!.data as Map<String, dynamic>;
981
+ expect(body['reasoning'], {'effort': 'high'});
982
+ expect(body.containsKey('reasoning_effort'), isFalse);
983
+ expect(body.containsKey('thinking'), isFalse);
984
+ expect(body.containsKey('think'), isFalse);
985
+ expect(body.containsKey('enable_thinking'), isFalse);
986
+ });
987
+
988
+ test('uses OpenRouter reasoning disable shape without aliases', () async {
989
+ final interceptor = CapturingStreamInterceptor(['data: [DONE]']);
990
+ final service = OpenRouterChatService(
991
+ Dio()..interceptors.add(interceptor),
992
+ );
993
+
994
+ await service
995
+ .sendMessage(
996
+ server: _openRouterTestServer(),
997
+ modelId: 'deepseek/deepseek-reasoner',
998
+ messages: [
999
+ Message(
1000
+ id: 'reasoning-message',
1001
+ conversationId: 'conversation',
1002
+ role: MessageRole.user,
1003
+ content: 'Answer directly',
1004
+ createdAt: DateTime.now(),
1005
+ ),
1006
+ ],
1007
+ params: ChatParameters.defaults().copyWith(reasoningEnabled: false),
1008
+ )
1009
+ .toList();
1010
+
1011
+ final body = interceptor.capturedRequest!.data as Map<String, dynamic>;
1012
+ expect(body['reasoning'], {'enabled': false});
1013
+ expect(body.containsKey('reasoning_effort'), isFalse);
1014
+ expect(body.containsKey('thinking'), isFalse);
1015
+ expect(body.containsKey('think'), isFalse);
1016
+ expect(body.containsKey('enable_thinking'), isFalse);
1017
+ });
1018
});
1019
1020
group('OpenAI-compatible & OpenRouter assistant prefill handling', () {
test/features/servers/server_api_service_test.dart
+11
-11
@@ -252,13 +252,14 @@ void main() {
252
});
253
254
test(
255
- 'detects reasoning capability for OpenAI-compatible models without capabilities payload',
255
+ 'only infers standard reasoning-effort models without capabilities',
256
() async {
257
final mockData = {
258
"object": "list",
259
"data": [
260
{"id": "o1-mini", "object": "model"},
261
{"id": "o3-mini", "object": "model"},
262
+ {"id": "openai/gpt-5-mini", "object": "model"},
263
{"id": "deepseek-reasoner", "object": "model"},
264
{
265
"id": "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
@@ -274,7 +275,7 @@ void main() {
275
276
final models = await service.fetchModels(testServer);
277
277
- expect(models, hasLength(6));
278
+ expect(models, hasLength(7));
279
280
// o1-mini
281
expect(models[0].supportsReasoning, isTrue);
@@ -287,19 +288,18 @@ void main() {
288
expect(models[1].supportedReasoningEfforts, ['low', 'medium', 'high']);
289
expect(models[1].defaultReasoningEffort, 'medium');
290
290
- // deepseek-reasoner
291
+ // gpt-5-mini
292
expect(models[2].supportsReasoning, isTrue);
293
expect(models[2].supportedReasoningEfforts, ['low', 'medium', 'high']);
294
294
- // DeepSeek-R1-Distill-Qwen-32B
295
- expect(models[3].supportsReasoning, isTrue);
296
-
297
- // qwq-32b
298
- expect(models[4].supportsReasoning, isTrue);
299
-
300
- // non-reasoning: Llama-3.1-8B-Instruct
295
+ // Provider-specific reasoning models need an explicit capability
296
+ // payload so LocalMind does not send unsupported request fields.
297
+ expect(models[3].supportsReasoning, isFalse);
298
+ expect(models[4].supportsReasoning, isFalse);
299
expect(models[5].supportsReasoning, isFalse);
302
- expect(models[5].supportedReasoningEfforts, isNull);
300
+
301
+ expect(models[6].supportsReasoning, isFalse);
302
+ expect(models[6].supportedReasoningEfforts, isNull);
303
},
304
);
305