Files
OnBudget/lib/src/features/notification_parsing/application/ai_providers.dart
T
SandersandClaude Opus 4.8 ab66aebc5e Switch AI notification parsing from OpenRouter to DeepSeek API
Replace the OpenRouter HTTP client with a DeepSeek client:
- data/openrouter/ -> data/deepseek/; OpenRouterClient -> DeepSeekClient
  (+ DeepSeekCompletion / DeepSeek*Exception)
- base URL api.deepseek.com, drop OpenRouter-only headers
- DeepSeek has no strict json_schema: use response_format json_object
  and rely on the prompt for shape (ai_schema.dart removed)
- secure-storage key openrouter_api_key -> deepseek_api_key
- default model deepseek/deepseek-v4-flash -> deepseek-chat
- l10n strings, test dart-defines (DEEPSEEK_API_KEY/_TEST_MODEL), docs

Also bundles in-progress findBodyIgnoreRule (body-level ignore rules
applied before the AI call) already present in the working tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 00:23:38 +03:00

51 lines
2.1 KiB
Dart

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../data/deepseek/deepseek_client.dart';
import '../data/parser/ai_parser.dart';
import '../data/secure/ai_key_store.dart';
part 'ai_providers.g.dart';
/// Защищённое хранилище DeepSeek API key.
@Riverpod(keepAlive: true)
AiKeyStore aiKeyStore(Ref ref) =>
const AiKeyStore(FlutterSecureStorage());
/// Общий HTTP-клиент (закрывается при dispose контейнера).
@Riverpod(keepAlive: true)
http.Client httpClient(Ref ref) {
final client = http.Client();
ref.onDispose(client.close);
return client;
}
/// AI-парсер, собранный на текущем API key. `null`, если ключ не задан —
/// воркер в этом случае не зовёт AI. В тестах провайдер переопределяется.
@Riverpod(keepAlive: true)
Future<AiParser?> aiParser(Ref ref) async {
final key = await ref.watch(aiKeyStoreProvider).getApiKey();
if (key == null || key.isEmpty) return null;
final client = DeepSeekClient(
client: ref.watch(httpClientProvider),
apiKey: key,
);
return AiParser(client);
}
/// Поток «есть сеть» — воркер ретраит `pending_ai` при восстановлении (§7).
///
/// `onConnectivityChanged` не реплеит текущее состояние при подписке, поэтому
/// первым значением отдаём результат явной проверки — иначе `.value` остаётся
/// `null` (и offline-ветка воркера ошибочно считает, что сеть есть).
@riverpod
Stream<bool> isOnline(Ref ref) async* {
final connectivity = Connectivity();
bool online(List<ConnectivityResult> results) =>
results.any((r) => r != ConnectivityResult.none);
yield online(await connectivity.checkConnectivity());
yield* connectivity.onConnectivityChanged.map(online);
}