From 0bfe67b1bf7405ca75fe3c112070d86a7613dc1b Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Tue, 19 May 2026 11:26:14 +0200 Subject: [PATCH] Clean up LLM digest provider errors --- .../application/llm_digest_orchestrator.dart | 13 ++++- .../llm_digest_orchestrator_test.dart | 53 ++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/lib/features/ai_digest/application/llm_digest_orchestrator.dart b/lib/features/ai_digest/application/llm_digest_orchestrator.dart index 0cff607..aeae0a1 100644 --- a/lib/features/ai_digest/application/llm_digest_orchestrator.dart +++ b/lib/features/ai_digest/application/llm_digest_orchestrator.dart @@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/app/data/relationship_repository.dart'; import 'package:relationship_saver/app/state/local_data_state.dart'; import 'package:relationship_saver/core/llm/llm_config.dart'; +import 'package:relationship_saver/core/llm/llm_service.dart'; import 'package:relationship_saver/features/ai_digest/application/ai_digest_notifier.dart'; import 'package:relationship_saver/features/ai_digest/application/ai_digest_response_parser.dart'; import 'package:relationship_saver/features/ai_digest/application/anonymized_llm_context_builder.dart'; @@ -185,11 +186,12 @@ class LlmDigestOrchestrator { createdDraftCount: parsed.drafts.length, ); } catch (error) { - await _markFailure('$error'); + final String reason = _digestFailureReason(error); + await _markFailure(reason); return LlmDigestRunResult( started: true, completed: false, - reason: '$error', + reason: reason, ); } } @@ -232,6 +234,13 @@ class LlmDigestOrchestrator { } } +String _digestFailureReason(Object error) { + if (error is LlmProviderException) { + return error.message; + } + return '$error'; +} + const String _systemPrompt = ''' You create private relationship-maintenance digest suggestions. The input contains pseudonymous people only. Never ask for or invent names. diff --git a/test/features/ai_digest/llm_digest_orchestrator_test.dart b/test/features/ai_digest/llm_digest_orchestrator_test.dart index ebf5920..483cda3 100644 --- a/test/features/ai_digest/llm_digest_orchestrator_test.dart +++ b/test/features/ai_digest/llm_digest_orchestrator_test.dart @@ -4,6 +4,8 @@ import 'package:relationship_saver/app/data/relationship_repository.dart'; import 'package:relationship_saver/app/data/storage/local_data_store_in_memory.dart'; import 'package:relationship_saver/app/data/storage/local_data_store_provider.dart'; import 'package:relationship_saver/app/state/local_data_state.dart'; +import 'package:relationship_saver/core/llm/llm_config.dart'; +import 'package:relationship_saver/core/llm/llm_service.dart'; import 'package:relationship_saver/features/ai_digest/application/ai_digest_notifier.dart'; import 'package:relationship_saver/features/ai_digest/application/llm_digest_environment.dart'; import 'package:relationship_saver/features/ai_digest/application/llm_digest_orchestrator.dart'; @@ -117,10 +119,45 @@ void main() { expect(result.started, isFalse); expect(textClient.calls, 0); }); + + test('manual digest reports provider rate limits without Dio noise', () async { + final ThrowingTextClient textClient = ThrowingTextClient( + error: const LlmProviderException( + provider: LlmProvider.openai, + statusCode: 429, + message: + 'OpenAI rejected the digest request with 429 rate limiting or quota pressure.', + ), + ); + final ProviderContainer container = _createContainer( + textClient: textClient, + notifier: FakeNotifier(), + ); + addTearDown(container.dispose); + + await container.read(localRepositoryProvider.future); + await container + .read(localRepositoryProvider.notifier) + .addPerson( + name: 'Taylor Quinn', + relationship: 'Friend', + notes: '', + tags: const ['tea'], + ); + + final LlmDigestRunResult result = await container + .read(llmDigestOrchestratorProvider) + .runManualDigest(); + + expect(result.started, isTrue); + expect(result.completed, isFalse); + expect(result.reason, contains('429 rate limiting')); + expect(result.reason, isNot(contains('DioException'))); + }); } ProviderContainer _createContainer({ - required FakeTextClient textClient, + required LlmDigestTextClient textClient, required FakeNotifier notifier, LlmDigestEnvironment environment = const FakeEnvironment(), }) { @@ -158,6 +195,20 @@ class FakeTextClient implements LlmDigestTextClient { } } +class ThrowingTextClient implements LlmDigestTextClient { + const ThrowingTextClient({required this.error}); + + final Object error; + + @override + Future complete({ + required String systemPrompt, + required String userPrompt, + }) async { + throw error; + } +} + class FakeNotifier implements AiDigestNotifier { int? lastCount;