diff --git a/kouyu_english/lib/core/ai_service.dart b/kouyu_english/lib/core/ai_service.dart index 5a78344..1ef23b9 100644 --- a/kouyu_english/lib/core/ai_service.dart +++ b/kouyu_english/lib/core/ai_service.dart @@ -221,6 +221,11 @@ class AiService { static bool _isHttpUri(Uri uri) => uri.scheme == 'https' || uri.scheme == 'http'; + static bool _isDeepSeek(Uri uri) { + final host = uri.host.toLowerCase(); + return host == 'deepseek.com' || host.endsWith('.deepseek.com'); + } + static bool _isSuccess(http.Response response) => response.statusCode >= 200 && response.statusCode < 300; @@ -373,6 +378,27 @@ class AiService { String reasoningEffort = 'low', }) { final isResponses = uri.path.endsWith('/responses'); + // DeepSeek thinks by default. Every request here is a short JSON answer + // with a small token cap, so reasoning only adds cost and can exhaust the + // cap before the JSON is written. Other providers may reject these + // DeepSeek-specific switches, so they keep the original payload. + if (_isDeepSeek(uri)) { + return isResponses + ? { + 'model': model, + 'input': messages, + 'reasoning': {'effort': 'none'}, + 'temperature': ?temperature, + 'max_output_tokens': ?maxTokens, + } + : { + 'model': model, + 'messages': messages, + 'thinking': {'type': 'disabled'}, + 'temperature': ?temperature, + 'max_tokens': ?maxTokens, + }; + } if (isResponses) { return { 'model': model, diff --git a/kouyu_english/test/deepseek_thinking_test.dart b/kouyu_english/test/deepseek_thinking_test.dart new file mode 100644 index 0000000..d1e9dcc --- /dev/null +++ b/kouyu_english/test/deepseek_thinking_test.dart @@ -0,0 +1,64 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:kouyu_english/core/ai_service.dart'; +import 'package:kouyu_english/core/models.dart'; + +void main() { + const definition = '{"definition":"苹果"}'; + + Future> captureBody(String endpoint) async { + AiService.instance.setFallbackApiKey('test-key'); + Map? body; + final result = await http.runWithClient( + () => AiService.instance.temporaryDefinition( + provider: AiProviderType.compatible, + endpoint: endpoint, + model: 'deepseek-flash', + text: 'apple', + ), + () => MockClient((request) async { + body = jsonDecode(request.body) as Map; + return http.Response( + jsonEncode({ + 'choices': [ + { + 'message': {'content': definition}, + }, + ], + 'output_text': definition, + }), + 200, + headers: {'content-type': 'application/json; charset=utf-8'}, + ); + }), + ); + expect(result, '苹果'); + return body!; + } + + test('DeepSeek chat completions request disables thinking', () async { + final body = await captureBody('https://api.deepseek.com'); + + expect(body['thinking'], {'type': 'disabled'}); + expect(body.containsKey('reasoning_effort'), isFalse); + expect(body['max_tokens'], 200); + }); + + test('DeepSeek responses request sets reasoning effort to none', () async { + final body = await captureBody('https://api.deepseek.com/v1/responses'); + + expect(body['reasoning'], {'effort': 'none'}); + expect(body.containsKey('reasoning_effort'), isFalse); + expect(body.containsKey('thinking'), isFalse); + }); + + test('other compatible endpoints keep their original payload', () async { + final body = await captureBody('https://example.test/v1'); + + expect(body.containsKey('thinking'), isFalse); + expect(body['reasoning_effort'], 'low'); + }); +}