diff --git a/packages/genai/README.md b/packages/genai/README.md index aebeb239..d3b7a091 100644 --- a/packages/genai/README.md +++ b/packages/genai/README.md @@ -2,8 +2,6 @@ `genai` is a lightweight and extensible Dart package designed to simplify AI requests and agentic operations. It provides an easy to use and seamless interface for various AI Providers such as (openai, gemini, antrhopic etc). ---- - ## 🔧 Features - **Unified request modeling** via `HttpRequestModel` @@ -13,8 +11,6 @@ - **Built-in utilities** for parsing headers and content types - **Support for both REST and GraphQL APIs** ---- - ## 📦 Installation To install the `genai` package, add it to your `pubspec.yaml`: @@ -30,8 +26,6 @@ Then run the following command in your terminal to fetch the package: flutter pub get ``` ---- - ## 🚀 Quick Start ### Response Mode (Callback Style) @@ -51,6 +45,7 @@ GenerativeAI.callGenerativeModel( ``` ### Streaming Mode (Callback Style) + ```dart final LLMModel geminiModel = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); final ModelController controller = model.provider.modelController; @@ -68,6 +63,7 @@ GenerativeAI.callGenerativeModel( ``` ### Procedural(Manual) Request Building + ```dart final LLMModel model = LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'); final ModelController controller = model.provider.modelController; @@ -82,9 +78,6 @@ final answer = await GenerativeAI.executeGenAIRequest(model, genAIRequest); print(answer); ``` ---- - - ## 🤝 Contributing @@ -97,4 +90,4 @@ We welcome contributions to the `genai` package! If you'd like to contribute, pl ## License -This project is licensed under the [Apache License 2.0](https://github.com/foss42/apidash/blob/main/packages/genai/LICENSE). \ No newline at end of file +This project is licensed under the [Apache License 2.0](https://github.com/foss42/apidash/blob/main/packages/genai/LICENSE). diff --git a/packages/genai/analysis_options.yaml b/packages/genai/analysis_options.yaml index a5744c1c..9a1eabb4 100644 --- a/packages/genai/analysis_options.yaml +++ b/packages/genai/analysis_options.yaml @@ -1,4 +1,11 @@ include: package:flutter_lints/flutter.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +analyzer: + errors: + invalid_annotation_target: ignore + exclude: + - "**/*.freezed.dart" + - "**/*.g.dart" + +linter: + rules: diff --git a/packages/genai/genai_example/lib/main.dart b/packages/genai/genai_example/lib/main.dart index cee60173..85c7b12c 100644 --- a/packages/genai/genai_example/lib/main.dart +++ b/packages/genai/genai_example/lib/main.dart @@ -27,17 +27,12 @@ class AIExample extends StatefulWidget { } class _AIExampleState extends State { + late final Future aM; + @override void initState() { super.initState(); - () async { - await LLMManager.fetchAvailableLLMs(); //fetch latest LLMs - await LLMManager.loadAvailableLLMs(); //Load Saved LLMs - setState(() {}); - }(); - LLMManager.fetchAvailableLLMs().then((_) { - LLMManager.loadAvailableLLMs().then((_) {}); - }); + aM = ModelManager.fetchAvailableModels(); //fetch latest LLMs systemPromptController.text = 'Give me a 200 word essay on the given topic'; inputPromptController.text = 'Apple'; } @@ -46,28 +41,33 @@ class _AIExampleState extends State { setState(() { output = ""; }); - GenerativeAI.callGenerativeModel( - LLMProvider.fromName( - selectedProvider, - ).getLLMByIdentifier(selectedModel![0]), + callGenerativeModel( + AIRequestModel( + modelProvider: selectedProvider, + modelRequestData: kModelProvidersMap[selectedProvider] + ?.defaultRequestData + .copyWith( + model: selectedModel, + apiKey: credentialController.value.text, + systemPrompt: systemPromptController.value.text, + userPrompt: inputPromptController.value.text, + stream: stream, + ), + ), onAnswer: (x) { setState(() { output += "$x "; }); }, onError: (e) { - print(e); + debugPrint(e); }, - systemPrompt: systemPromptController.value.text, - userPrompt: inputPromptController.value.text, - credential: credentialController.value.text, - stream: stream, ); } String output = ""; - String selectedProvider = 'ollama'; - List? selectedModel; + ModelAPIProvider selectedProvider = ModelAPIProvider.ollama; + String selectedModel = ""; TextEditingController systemPromptController = TextEditingController(); TextEditingController inputPromptController = TextEditingController(); @@ -77,107 +77,119 @@ class _AIExampleState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('GenAI Example')), - body: SingleChildScrollView( - padding: EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text('Providers'), - SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ...LLMManager.avaiableModels.keys.map( - (x) => Container( - child: GestureDetector( - onTap: () { - setState(() { - selectedProvider = x; - }); - }, - child: Chip( - label: Text(x), - backgroundColor: selectedProvider == x - ? Colors.blue[50] - : Colors.transparent, - ), - ), - padding: EdgeInsets.only(right: 10), - ), - ), - ], - ), - SizedBox(height: 20), - Text('Models'), - SizedBox(height: 10), - Wrap( - spacing: 5, - runSpacing: 5, - children: [ - ...(LLMManager.avaiableModels[selectedProvider] ?? []).map( - (x) => Container( - child: GestureDetector( - onTap: () { - setState(() { - selectedModel = x; - }); - }, - child: Chip( - label: Text(x[1].toString()), - backgroundColor: selectedModel == x - ? Colors.blue[50] - : Colors.transparent, - ), - ), - padding: EdgeInsets.only(right: 10), - ), - ), - ], - ), - SizedBox(height: 30), - Container( - width: 400, + body: FutureBuilder( + future: aM, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data != null) { + final data = snapshot.data!; + final mappedData = data.map; + return SingleChildScrollView( + padding: EdgeInsets.all(20), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text('Input Prompt'), - TextField(controller: inputPromptController), + Text('Providers'), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...data.modelProviders.map( + (x) => Container( + padding: EdgeInsets.only(right: 10), + child: GestureDetector( + onTap: () { + setState(() { + selectedProvider = x.providerId!; + }); + }, + child: Chip( + label: Text(x.providerName ?? ""), + backgroundColor: selectedProvider == x.providerId + ? Colors.blue[50] + : Colors.transparent, + ), + ), + ), + ), + ], + ), SizedBox(height: 20), - Text('System Prompt'), - TextField(controller: systemPromptController), - SizedBox(height: 20), - Text('Credential'), - TextField(controller: credentialController), + Text('Models'), + SizedBox(height: 10), + Wrap( + spacing: 5, + runSpacing: 5, + children: [ + ...(mappedData[selectedProvider]?.models ?? []).map( + (x) => Container( + padding: EdgeInsets.only(right: 10), + child: GestureDetector( + onTap: () { + setState(() { + selectedModel = x.id!; + }); + }, + child: Chip( + label: Text(x.name ?? ""), + backgroundColor: selectedModel == x.id + ? Colors.blue[50] + : Colors.transparent, + ), + ), + ), + ), + ], + ), + SizedBox(height: 30), + Container( + width: 400, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Input Prompt'), + TextField(controller: inputPromptController), + SizedBox(height: 20), + Text('System Prompt'), + TextField(controller: systemPromptController), + SizedBox(height: 20), + Text('Credential'), + TextField(controller: credentialController), + SizedBox(height: 20), + ], + ), + ), + SizedBox(height: 30), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: () { + generateAIResponse(); + }, + child: Text('Generate Response (SINGLE-RESPONSE)'), + ), + SizedBox(width: 20), + ElevatedButton( + onPressed: () { + generateAIResponse(stream: true); + }, + child: Text('Generate Response (STREAM)'), + ), + ], + ), + SizedBox(height: 30), + Divider(), SizedBox(height: 20), + + Text(output), ], ), - ), - SizedBox(height: 30), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - ElevatedButton( - onPressed: () { - generateAIResponse(); - }, - child: Text('Generate Response (SINGLE-RESPONSE)'), - ), - SizedBox(width: 20), - ElevatedButton( - onPressed: () { - generateAIResponse(stream: true); - }, - child: Text('Generate Response (STREAM)'), - ), - ], - ), - SizedBox(height: 30), - Divider(), - SizedBox(height: 20), - - Text(output), - ], - ), + ); + } + return CircularProgressIndicator(); + }, ), ); } diff --git a/packages/genai/genai_example/pubspec.lock b/packages/genai/genai_example/pubspec.lock index 2b38738d..7fe46cb5 100644 --- a/packages/genai/genai_example/pubspec.lock +++ b/packages/genai/genai_example/pubspec.lock @@ -104,22 +104,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" fixnum: dependency: transitive description: @@ -146,11 +130,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" freezed_annotation: dependency: transitive description: @@ -254,6 +233,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + nanoid: + dependency: transitive + description: + name: nanoid + sha256: be3f8752d9046c825df2f3914195151eb876f3ad64b9d833dd0b799b77b8759e + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -262,30 +249,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" petitparser: dependency: transitive description: @@ -294,22 +257,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" pointycastle: dependency: transitive description: @@ -325,62 +272,6 @@ packages: relative: true source: path version: "0.0.3" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" - url: "https://pub.dev" - source: hosted - version: "2.5.3" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" - url: "https://pub.dev" - source: hosted - version: "2.4.10" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -466,14 +357,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" xml: dependency: transitive description: @@ -484,4 +367,4 @@ packages: version: "6.5.0" sdks: dart: ">=3.8.0 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/packages/genai/lib/consts.dart b/packages/genai/lib/consts.dart new file mode 100644 index 00000000..6921a8c1 --- /dev/null +++ b/packages/genai/lib/consts.dart @@ -0,0 +1,10 @@ +import 'models/models.dart'; + +const kKeyStream = 'stream'; + +final kAvailableModels = AvailableModels.fromJson(kModelsData); + +const kModelRemoteUrl = + 'https://raw.githubusercontent.com/foss42/apidash/package/genai/packages/genai/models.json'; + +const kBaseOllamaUrl = 'http://localhost:11434'; diff --git a/packages/genai/lib/genai.dart b/packages/genai/lib/genai.dart index 2b4be834..7d58af8d 100644 --- a/packages/genai/lib/genai.dart +++ b/packages/genai/lib/genai.dart @@ -1,11 +1,4 @@ -// Module Exports -export 'models/ai_request_model.dart'; -export 'providers/providers.dart'; -export 'generative_ai.dart'; -export 'llm_config.dart'; -export 'llm_input_payload.dart'; -export 'llm_manager.dart'; -export 'llm_model.dart'; -export 'llm_provider.dart'; -export 'llm_request.dart'; -export 'llm_saveobject.dart'; +export 'models/models.dart'; +export 'interface/interface.dart'; +export 'utils/utils.dart'; +export 'widgets/widgets.dart'; diff --git a/packages/genai/lib/generative_ai.dart b/packages/genai/lib/generative_ai.dart deleted file mode 100644 index 6fec8d72..00000000 --- a/packages/genai/lib/generative_ai.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; -import 'package:better_networking/better_networking.dart'; -import 'llm_config.dart'; -import 'llm_model.dart'; -import 'llm_request.dart'; - -class GenerativeAI { - static Future executeGenAIRequest( - LLMModel model, - LLMRequestDetails requestDetails, - ) async { - final mC = model.provider.modelController; - final headers = requestDetails.headers; - // print(jsonEncode(requestDetails.body)); - final (response, _, _) = await sendHttpRequest( - (Random().nextDouble() * 9999999 + 1).toString(), - APIType.rest, - HttpRequestModel( - method: HTTPVerb.post, - headers: [ - ...headers.entries.map( - (x) => NameValueModel.fromJson({x.key: x.value}), - ), - ], - url: requestDetails.endpoint, - bodyContentType: ContentType.json, - body: jsonEncode(requestDetails.body), - ), - ); - if (response == null) return null; - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - // print(data); - return mC.outputFormatter(data); - } else { - print(requestDetails.endpoint); - print(response.body); - throw Exception( - 'LLM_EXCEPTION: ${response.statusCode}\n${response.body}', - ); - } - } - - static Future> streamGenAIRequest( - LLMModel model, - LLMRequestDetails requestDetails, - ) async { - final modelController = model.provider.modelController; - - final headers = { - 'Content-Type': 'application/json', - ...requestDetails.headers, - }; - - final httpStream = await streamHttpRequest( - requestDetails.hashCode.toString(), - APIType.rest, - HttpRequestModel( - method: HTTPVerb.post, - headers: headers.entries - .map((entry) => NameValueModel(name: entry.key, value: entry.value)) - .toList(), - url: requestDetails.endpoint, - bodyContentType: ContentType.json, - body: jsonEncode(requestDetails.body), - ), - ); - - final streamController = StreamController(); - - final subscription = httpStream.listen( - (dat) { - if (dat == null) { - streamController.addError('STREAMING ERROR: NULL DATA'); - return; - } - - final chunk = dat.$2; - final error = dat.$4; - - if (chunk == null) { - streamController.addError(error ?? 'NULL ERROR'); - return; - } - - final ans = chunk.body; - - final lines = ans.split('\n'); - for (final line in lines) { - if (!line.startsWith('data: ') || line.contains('[DONE]')) continue; - final jsonStr = line.substring(6).trim(); - try { - final jsonData = jsonDecode(jsonStr); - final formattedOutput = modelController.streamOutputFormatter( - jsonData, - ); - streamController.sink.add(formattedOutput); - } catch (e) { - print('⚠️ JSON decode error in SSE: $e\Sending as Regular Text'); - streamController.sink.add(jsonStr); - } - } - }, - onError: (error) { - streamController.addError('STREAM ERROR: $error'); - streamController.close(); - }, - onDone: () { - streamController.close(); - }, - cancelOnError: true, - ); - - streamController.onCancel = () async { - await subscription.cancel(); - }; - - return streamController.stream; - } - - static callGenerativeModel( - LLMModel model, { - required Function(String?) onAnswer, - required Function(dynamic) onError, - required String systemPrompt, - required String userPrompt, - String? credential, - String? endpoint, - Map? configurations, - bool stream = false, - }) async { - final c = model.provider.modelController; - final payload = c.inputPayload; - payload.systemPrompt = systemPrompt; - payload.userPrompt = userPrompt; - if (credential != null) { - payload.credential = credential; - } - if (configurations != null) { - payload.configMap.addAll(configurations); - } - if (endpoint != null) { - payload.endpoint = endpoint; - } - try { - if (stream) { - final streamRequest = c.createRequest(model, payload, stream: true); - final answerStream = await streamGenAIRequest(model, streamRequest); - processGenAIStreamOutput(answerStream, (w) { - onAnswer('$w '); - }, onError); - } else { - final request = c.createRequest(model, payload); - final answer = await executeGenAIRequest(model, request); - onAnswer(answer); - } - } catch (e) { - onError(e); - } - } - - static void processGenAIStreamOutput( - Stream stream, - Function(String) onWord, - Function(dynamic) onError, - ) { - String buffer = ''; - stream.listen( - (chunk) { - if (chunk == null || chunk.isEmpty) return; - buffer += chunk; - // Split on spaces but preserve last partial word - final parts = buffer.split(RegExp(r'\s+')); - if (parts.length > 1) { - // Keep the last part in buffer (it may be incomplete) - buffer = parts.removeLast(); - for (final word in parts) { - if (word.trim().isNotEmpty) { - onWord(word); - } - } - } - }, - onDone: () { - // Print any remaining word when stream is finished - if (buffer.trim().isNotEmpty) { - onWord(buffer); - } - }, - onError: (e) { - onError(e); - }, - ); - } -} diff --git a/packages/genai/lib/interface/consts.dart b/packages/genai/lib/interface/consts.dart new file mode 100644 index 00000000..cd09eec5 --- /dev/null +++ b/packages/genai/lib/interface/consts.dart @@ -0,0 +1,71 @@ +import '../consts.dart'; +import '../models/models.dart'; +import 'model_providers/model_providers.dart'; + +enum ModelAPIProvider { openai, anthropic, gemini, azureopenai, ollama } + +final kModelProvidersMap = { + ModelAPIProvider.openai: OpenAIModel.instance, + ModelAPIProvider.anthropic: AnthropicModel.instance, + ModelAPIProvider.gemini: GeminiModel.instance, + ModelAPIProvider.azureopenai: AzureOpenAIModel.instance, + ModelAPIProvider.ollama: OllamaModel.instance, +}; + +const kAnthropicUrl = 'https://api.anthropic.com/v1/messages'; +const kGeminiUrl = 'https://generativelanguage.googleapis.com/v1beta/models'; +const kOpenAIUrl = 'https://api.openai.com/v1/chat/completions'; +const kOllamaUrl = '$kBaseOllamaUrl/v1/chat/completions'; + +final kDefaultModelRequestData = ModelRequestData( + url: '', + model: '', + apiKey: '', + systemPrompt: '', + userPrompt: '', + modelConfigs: [ + kDefaultModelConfigTemperature, + kDefaultModelConfigTopP, + kDefaultModelConfigMaxTokens, + ], + stream: false, +); + +final kDefaultModelConfigTemperature = ModelConfig( + id: 'temperature', + name: 'Temperature', + description: 'The Temperature of the Model', + type: ConfigType.slider, + value: ConfigSliderValue(value: (0, 0.5, 1)), +); + +final kDefaultModelConfigTopP = ModelConfig( + id: 'top_p', + name: 'Top P', + description: 'The Top P of the Model', + type: ConfigType.slider, + value: ConfigSliderValue(value: (0, 0.95, 1)), +); + +final kDefaultModelConfigMaxTokens = ModelConfig( + id: 'max_tokens', + name: 'Maximum Tokens', + description: 'The maximum number of tokens allowed in the output', + type: ConfigType.numeric, + value: ConfigNumericValue(value: 1024), +); + +final kDefaultModelConfigStream = ModelConfig( + id: 'stream', + name: 'Enable Streaming Mode', + description: 'The LLM output will be sent in a stream instead of all at once', + type: ConfigType.boolean, + value: ConfigBooleanValue(value: false), +); + +final kDefaultGeminiModelConfigTopP = kDefaultModelConfigTopP.copyWith( + id: 'topP', +); + +final kDefaultGeminiModelConfigMaxTokens = kDefaultModelConfigMaxTokens + .copyWith(id: 'maxOutputTokens'); diff --git a/packages/genai/lib/interface/interface.dart b/packages/genai/lib/interface/interface.dart new file mode 100644 index 00000000..4ab63517 --- /dev/null +++ b/packages/genai/lib/interface/interface.dart @@ -0,0 +1,2 @@ +export 'model_providers/model_providers.dart'; +export 'consts.dart'; diff --git a/packages/genai/lib/interface/model_providers/anthropic.dart b/packages/genai/lib/interface/model_providers/anthropic.dart new file mode 100644 index 00000000..88c18009 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/anthropic.dart @@ -0,0 +1,48 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class AnthropicModel extends ModelProvider { + static final instance = AnthropicModel(); + + @override + ModelRequestData get defaultRequestData => + kDefaultModelRequestData.copyWith(url: kAnthropicUrl); + + @override + HttpRequestModel? createRequest(ModelRequestData? requestData) { + if (requestData == null) { + return null; + } + return HttpRequestModel( + method: HTTPVerb.post, + url: requestData.url, + headers: const [ + NameValueModel(name: "anthropic-version", value: "2023-06-01"), + ], + authModel: AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel(key: requestData.apiKey), + ), + body: kJsonEncoder.convert({ + "model": requestData.model, + "messages": [ + {"role": "system", "content": requestData.systemPrompt}, + {"role": "user", "content": requestData.userPrompt}, + ], + ...requestData.getModelConfigMap(), + if (requestData.stream ?? false) ...{'stream': true}, + }), + ); + } + + @override + String? outputFormatter(Map x) { + return x['content']?[0]['text']; + } + + @override + String? streamOutputFormatter(Map x) { + return x['text']; + } +} diff --git a/packages/genai/lib/interface/model_providers/azureopenai.dart b/packages/genai/lib/interface/model_providers/azureopenai.dart new file mode 100644 index 00000000..5f4da780 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/azureopenai.dart @@ -0,0 +1,50 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class AzureOpenAIModel extends ModelProvider { + static final instance = AzureOpenAIModel(); + @override + ModelRequestData get defaultRequestData => kDefaultModelRequestData; + + @override + HttpRequestModel? createRequest(ModelRequestData? requestData) { + if (requestData == null) { + return null; + } + if (requestData.url.isEmpty) { + throw Exception('MODEL ENDPOINT IS EMPTY'); + } + return HttpRequestModel( + method: HTTPVerb.post, + url: requestData.url, + authModel: AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel(key: requestData.apiKey, name: 'api-key'), + ), + body: kJsonEncoder.convert({ + "model": requestData.model, + "messages": [ + {"role": "system", "content": requestData.systemPrompt}, + if (requestData.userPrompt.isNotEmpty) ...{ + {"role": "user", "content": requestData.userPrompt}, + } else ...{ + {"role": "user", "content": "Generate"}, + }, + ], + ...requestData.getModelConfigMap(), + if (requestData.stream ?? false) ...{'stream': true}, + }), + ); + } + + @override + String? outputFormatter(Map x) { + return x["choices"]?[0]["message"]?["content"]?.trim(); + } + + @override + String? streamOutputFormatter(Map x) { + return x["choices"]?[0]["delta"]?["content"]; + } +} diff --git a/packages/genai/lib/interface/model_providers/gemini.dart b/packages/genai/lib/interface/model_providers/gemini.dart new file mode 100644 index 00000000..4ca3c313 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/gemini.dart @@ -0,0 +1,72 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class GeminiModel extends ModelProvider { + static final instance = GeminiModel(); + + @override + ModelRequestData get defaultRequestData => kDefaultModelRequestData.copyWith( + url: kGeminiUrl, + modelConfigs: [ + kDefaultModelConfigTemperature, + kDefaultGeminiModelConfigTopP, + kDefaultGeminiModelConfigMaxTokens, + ], + ); + + @override + HttpRequestModel? createRequest(ModelRequestData? requestData) { + if (requestData == null) { + return null; + } + List params = []; + String endpoint = "${requestData.url}/${requestData.model}:"; + if (requestData.stream ?? false) { + endpoint += 'streamGenerateContent'; + params.add(const NameValueModel(name: "alt", value: "sse")); + } else { + endpoint += 'generateContent'; + } + + return HttpRequestModel( + method: HTTPVerb.post, + url: endpoint, + authModel: AuthModel( + type: APIAuthType.apiKey, + apikey: AuthApiKeyModel( + key: requestData.apiKey, + location: 'query', + name: 'key', + ), + ), + body: kJsonEncoder.convert({ + "contents": [ + { + "role": "user", + "parts": [ + {"text": requestData.userPrompt}, + ], + }, + ], + "systemInstruction": { + "role": "system", + "parts": [ + {"text": requestData.systemPrompt}, + ], + }, + "generationConfig": requestData.getModelConfigMap(), + }), + ); + } + + @override + String? outputFormatter(Map x) { + return x['candidates']?[0]?['content']?['parts']?[0]?['text']; + } + + @override + String? streamOutputFormatter(Map x) { + return x['candidates']?[0]?['content']?['parts']?[0]?['text']; + } +} diff --git a/packages/genai/lib/providers/providers.dart b/packages/genai/lib/interface/model_providers/model_providers.dart similarity index 81% rename from packages/genai/lib/providers/providers.dart rename to packages/genai/lib/interface/model_providers/model_providers.dart index ae945182..b3fa202f 100644 --- a/packages/genai/lib/providers/providers.dart +++ b/packages/genai/lib/interface/model_providers/model_providers.dart @@ -2,4 +2,4 @@ export 'anthropic.dart'; export 'gemini.dart'; export 'azureopenai.dart'; export 'openai.dart'; -export 'ollama.dart'; \ No newline at end of file +export 'ollama.dart'; diff --git a/packages/genai/lib/interface/model_providers/ollama.dart b/packages/genai/lib/interface/model_providers/ollama.dart new file mode 100644 index 00000000..78c87cf3 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/ollama.dart @@ -0,0 +1,13 @@ +import '../../models/models.dart'; +import '../consts.dart'; +import 'openai.dart'; + +class OllamaModel extends OpenAIModel { + static final instance = OllamaModel(); + + @override + ModelRequestData get defaultRequestData => kDefaultModelRequestData.copyWith( + url: kOllamaUrl, + modelConfigs: [kDefaultModelConfigTemperature, kDefaultModelConfigTopP], + ); +} diff --git a/packages/genai/lib/interface/model_providers/openai.dart b/packages/genai/lib/interface/model_providers/openai.dart new file mode 100644 index 00000000..f42b6c31 --- /dev/null +++ b/packages/genai/lib/interface/model_providers/openai.dart @@ -0,0 +1,49 @@ +import 'package:better_networking/better_networking.dart'; +import '../../models/models.dart'; +import '../consts.dart'; + +class OpenAIModel extends ModelProvider { + static final instance = OpenAIModel(); + + @override + ModelRequestData get defaultRequestData => + kDefaultModelRequestData.copyWith(url: kOpenAIUrl); + + @override + HttpRequestModel? createRequest(ModelRequestData? requestData) { + if (requestData == null) { + return null; + } + return HttpRequestModel( + method: HTTPVerb.post, + url: requestData.url, + authModel: AuthModel( + type: APIAuthType.bearer, + bearer: AuthBearerModel(token: requestData.apiKey), + ), + body: kJsonEncoder.convert({ + "model": requestData.model, + "messages": [ + {"role": "system", "content": requestData.systemPrompt}, + if (requestData.userPrompt.isNotEmpty) ...{ + {"role": "user", "content": requestData.userPrompt}, + } else ...{ + {"role": "user", "content": "Generate"}, + }, + ], + ...requestData.getModelConfigMap(), + if (requestData.stream ?? false) ...{'stream': true}, + }), + ); + } + + @override + String? outputFormatter(Map x) { + return x["choices"]?[0]["message"]?["content"]?.trim(); + } + + @override + String? streamOutputFormatter(Map x) { + return x["choices"]?[0]["delta"]?["content"]; + } +} diff --git a/packages/genai/lib/llm_config.dart b/packages/genai/lib/llm_config.dart deleted file mode 100644 index 19aec010..00000000 --- a/packages/genai/lib/llm_config.dart +++ /dev/null @@ -1,199 +0,0 @@ -import 'dart:convert'; - -typedef LLMOutputFormatter = String? Function(Map); - -class LLMModelConfiguration { - final String configId; - final String configName; - final String configDescription; - final LLMModelConfigurationType configType; - final LLMModelConfigValue configValue; - - LLMModelConfiguration updateValue(LLMModelConfigValue value) { - return LLMModelConfiguration( - configId: configId, - configName: configName, - configDescription: configDescription, - configType: configType, - configValue: value, - ); - } - - LLMModelConfiguration({ - required this.configId, - required this.configName, - required this.configDescription, - required this.configType, - required this.configValue, - }) { - // Assert that the configuration type and value matches - switch (configType) { - case LLMModelConfigurationType.boolean: - assert(configValue is LLMConfigBooleanValue); - case LLMModelConfigurationType.slider: - assert(configValue is LLMConfigSliderValue); - case LLMModelConfigurationType.numeric: - assert(configValue is LLMConfigNumericValue); - case LLMModelConfigurationType.text: - assert(configValue is LLMConfigTextValue); - } - } - - factory LLMModelConfiguration.fromJson(Map x) { - LLMModelConfigurationType cT; - LLMModelConfigValue cV; - switch (x['configType']) { - case 'boolean': - cT = LLMModelConfigurationType.boolean; - cV = LLMConfigBooleanValue.deserialize(x['configValue']); - break; - case 'slider': - cT = LLMModelConfigurationType.slider; - cV = LLMConfigSliderValue.deserialize(x['configValue']); - break; - case 'numeric': - cT = LLMModelConfigurationType.numeric; - cV = LLMConfigNumericValue.deserialize(x['configValue']); - break; - case 'text': - cT = LLMModelConfigurationType.text; - cV = LLMConfigTextValue.deserialize(x['configValue']); - break; - default: - cT = LLMModelConfigurationType.text; - cV = LLMConfigTextValue.deserialize(x['configValue']); - } - return LLMModelConfiguration( - configId: x['configId'], - configName: x['configName'], - configDescription: x['configDescription'], - configType: cT, - configValue: cV, - ); - } - - Map toJson() { - return { - 'configId': configId, - 'configName': configName, - 'configDescription': configDescription, - 'configType': configType.name.toString(), - 'configValue': configValue.serialize(), - }; - } - - LLMModelConfiguration clone() { - return LLMModelConfiguration.fromJson(toJson()); - } -} - -enum LLMModelConfigurationType { boolean, slider, numeric, text } - -//----------------LLMConfigValues ------------ - -abstract class LLMModelConfigValue { - dynamic _value; - - // ignore: unnecessary_getters_setters - dynamic get value => _value; - - set value(dynamic newValue) => _value = newValue; - - String serialize(); - - LLMModelConfigValue(this._value); -} - -class LLMConfigBooleanValue extends LLMModelConfigValue { - LLMConfigBooleanValue({required bool value}) : super(value); - - @override - String serialize() { - return value.toString(); - } - - static LLMConfigBooleanValue deserialize(String x) { - return LLMConfigBooleanValue(value: x == 'true'); - } -} - -class LLMConfigNumericValue extends LLMModelConfigValue { - LLMConfigNumericValue({required num value}) : super(value); - - @override - String serialize() { - return value.toString(); - } - - static LLMConfigNumericValue deserialize(String x) { - return LLMConfigNumericValue(value: num.parse(x)); - } -} - -class LLMConfigSliderValue extends LLMModelConfigValue { - LLMConfigSliderValue({required (double, double, double) value}) - : super(value); - - @override - String serialize() { - final v = value as (double, double, double); - return jsonEncode([v.$1, v.$2, v.$3]); - } - - static LLMConfigSliderValue deserialize(String x) { - final z = jsonDecode(x) as List; - final val = ( - double.parse(z[0].toString()), - double.parse(z[1].toString()), - double.parse(z[2].toString()), - ); - return LLMConfigSliderValue(value: val); - } -} - -class LLMConfigTextValue extends LLMModelConfigValue { - LLMConfigTextValue({required String value}) : super(value); - - @override - String serialize() { - return value.toString(); - } - - static LLMConfigTextValue deserialize(String x) { - return LLMConfigTextValue(value: x); - } -} - -enum LLMConfigName { temperature, top_p, max_tokens, endpoint, stream } - -Map defaultLLMConfigurations = { - LLMConfigName.temperature: LLMModelConfiguration( - configId: 'temperature', - configName: 'Temperature', - configDescription: 'The Temperature of the Model', - configType: LLMModelConfigurationType.slider, - configValue: LLMConfigSliderValue(value: (0, 0.5, 1)), - ), - LLMConfigName.top_p: LLMModelConfiguration( - configId: 'top_p', - configName: 'Top P', - configDescription: 'The Top P of the Model', - configType: LLMModelConfigurationType.slider, - configValue: LLMConfigSliderValue(value: (0, 0.95, 1)), - ), - LLMConfigName.max_tokens: LLMModelConfiguration( - configId: 'max_tokens', - configName: 'Maximum Tokens', - configDescription: 'The maximum number of tokens allowed in the output', - configType: LLMModelConfigurationType.numeric, - configValue: LLMConfigNumericValue(value: -1), - ), - LLMConfigName.stream: LLMModelConfiguration( - configId: 'stream', - configName: 'Enable Streaming Mode', - configDescription: - 'The LLM output will be sent in a stream instead of all at once', - configType: LLMModelConfigurationType.boolean, - configValue: LLMConfigBooleanValue(value: false), - ), -}; diff --git a/packages/genai/lib/llm_input_payload.dart b/packages/genai/lib/llm_input_payload.dart deleted file mode 100644 index 79198911..00000000 --- a/packages/genai/lib/llm_input_payload.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'llm_config.dart'; - -class LLMInputPayload { - String endpoint; - String credential; - String systemPrompt; - String userPrompt; - Map configMap; - - LLMInputPayload({ - required this.endpoint, - required this.credential, - required this.systemPrompt, - required this.userPrompt, - required this.configMap, - }); - - LLMInputPayload clone() { - Map cmap = {}; - for (final k in configMap.keys) { - cmap[k] = configMap[k]!.clone(); - } - return LLMInputPayload( - endpoint: endpoint, - credential: credential, - systemPrompt: systemPrompt, - userPrompt: userPrompt, - configMap: cmap, - ); - } - - static Map toJSON(LLMInputPayload payload) { - Map cmap = {}; - for (final e in payload.configMap.entries) { - cmap[e.key] = e.value.toJson(); - } - return { - 'endpoint': payload.endpoint, - 'credential': payload.credential, - 'system_prompt': payload.systemPrompt, - 'user_prompt': payload.userPrompt, - 'config_map': cmap, - }; - } - - static LLMInputPayload fromJSON(Map json) { - Map cmap = {}; - for (final k in json['config_map'].keys) { - cmap[k] = LLMModelConfiguration.fromJson(json['config_map'][k]); - } - return LLMInputPayload( - endpoint: json['endpoint'], - credential: json['credential'], - systemPrompt: json['system_prompt'], - userPrompt: json['user_prompt'], - configMap: cmap, - ); - } -} diff --git a/packages/genai/lib/llm_manager.dart b/packages/genai/lib/llm_manager.dart deleted file mode 100644 index 5feb7701..00000000 --- a/packages/genai/lib/llm_manager.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:convert'; -import 'package:better_networking/better_networking.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class LLMManager { - static Map avaiableModels = { - "gemini": [ - ["gemini-2.0-flash", "Gemini 2.0 Flash"], - ], - }; - - static get models => avaiableModels; - - static const String modelRemoteURL = - 'https://raw.githubusercontent.com/synapsecode/apidash/package/genai/packages/genai/models.json'; - static const String baseOllamaURL = 'http://localhost:11434'; - - static addLLM(String providerID, String modelID, String modelName) async { - avaiableModels[providerID] = [ - ...avaiableModels[providerID], - [modelID, modelName], - ]; - await saveAvailableLLMs(avaiableModels); - } - - static removeLLM(String providerID, String modelID, String modelName) async { - List z = avaiableModels[providerID] as List; - z = z.where((x) => x[0] != modelID && x[1] != modelName).toList(); - avaiableModels[providerID] = z; - await saveAvailableLLMs(avaiableModels); - } - - static fetchAvailableLLMs([String? remoteURL, String? ollamaURL]) async { - //get LLMs from remove - final (resp, _, __) = await sendHttpRequest( - 'FETCH_MODELS', - APIType.rest, - HttpRequestModel(url: remoteURL ?? modelRemoteURL, method: HTTPVerb.get), - ); - if (resp == null) { - throw Exception('UNABLE TO FETCH MODELS'); - } - Map remoteModels = jsonDecode(resp.body); - final oM = await fetchInstalledOllamaModels(ollamaURL); - remoteModels['ollama'] = oM; - saveAvailableLLMs(remoteModels); - loadAvailableLLMs(); - } - - static saveAvailableLLMs(Map updatedLLMs) async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('genai_available_llms', jsonEncode(updatedLLMs)); - } - - static loadAvailableLLMs() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - final avl = prefs.getString('genai_available_llms'); - if (avl != null) { - avaiableModels = (jsonDecode(avl)); - } - } - - static clearAvailableLLMs() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('genai_available_llms'); - } - - static Future fetchInstalledOllamaModels([String? ollamaURL]) async { - final url = "${ollamaURL ?? baseOllamaURL}/api/tags"; - final (resp, _, __) = await sendHttpRequest( - 'OLLAMA_FETCH', - APIType.rest, - HttpRequestModel(url: url, method: HTTPVerb.get), - noSSL: true, - ); - if (resp == null) return []; - final output = jsonDecode(resp.body); - final models = output['models']; - if (models == null) return []; - List ollamaModels = []; - for (final m in models) { - ollamaModels.add([m['model'], m['name']]); - } - return ollamaModels; - } -} diff --git a/packages/genai/lib/llm_model.dart b/packages/genai/lib/llm_model.dart deleted file mode 100644 index 30458c60..00000000 --- a/packages/genai/lib/llm_model.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'llm_input_payload.dart'; -import 'llm_provider.dart'; -import 'llm_request.dart'; - -class LLMModel { - const LLMModel(this.identifier, this.modelName, this.provider); - final String identifier; - final String modelName; - final LLMProvider provider; - - static Map toJson(LLMModel m) { - return {'identifier': m.identifier, 'provider': m.provider.name}; - } - - static LLMModel fromJson(Map json) { - return LLMProvider.fromName( - json['provider'], - ).getLLMByIdentifier(json['identifier']); - } -} - -abstract class ModelController { - LLMInputPayload get inputPayload => throw UnimplementedError(); - - LLMRequestDetails createRequest( - LLMModel model, - LLMInputPayload inputPayload, { - bool stream = false, - }) { - throw UnimplementedError(); - } - - String? outputFormatter(Map x) { - throw UnimplementedError(); - } - - String? streamOutputFormatter(Map x) { - throw UnimplementedError(); - } -} diff --git a/packages/genai/lib/llm_provider.dart b/packages/genai/lib/llm_provider.dart deleted file mode 100644 index 1e652910..00000000 --- a/packages/genai/lib/llm_provider.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'providers/providers.dart'; -import '../llm_manager.dart'; -import 'llm_model.dart'; - -enum LLMProvider { - gemini('Gemini'), - openai('OpenAI'), - anthropic('Anthropic'), - ollama('Ollama'), - azureopenai('Azure OpenAI'); - - const LLMProvider(this.displayName); - - final String displayName; - - List get models { - final avl = LLMManager.models[this.name.toLowerCase()]; - if (avl == null) return []; - List models = []; - for (final x in avl) { - models.add(LLMModel(x[0], x[1], this)); - } - return models; - } - - ModelController get modelController { - switch (this) { - case LLMProvider.ollama: - return OllamaModelController.instance; - case LLMProvider.gemini: - return GeminiModelController.instance; - case LLMProvider.azureopenai: - return AzureOpenAIModelController.instance; - case LLMProvider.openai: - return OpenAIModelController.instance; - case LLMProvider.anthropic: - return AnthropicModelController.instance; - } - } - - static LLMProvider fromJSON(Map json) { - return LLMProvider.fromName(json['llm_provider']); - } - - static Map toJSON(LLMProvider p) { - return {'llm_provider': p.name}; - } - - static LLMProvider? fromJSONNullable(Map? json) { - if (json == null) return null; - return LLMProvider.fromName(json['llm_provider']); - } - - static Map? toJSONNullable(LLMProvider? p) { - if (p == null) return null; - return {'llm_provider': p.name}; - } - - LLMModel getLLMByIdentifier(String identifier) { - final m = this.models.where((e) => e.identifier == identifier).firstOrNull; - if (m == null) { - throw Exception('MODEL DOES NOT EXIST $identifier'); - } - return m; - } - - static LLMProvider fromName(String name) { - return LLMProvider.values.firstWhere( - (model) => model.name == name, - orElse: () => throw ArgumentError('INVALID LLM PROVIDER: $name'), - ); - } -} diff --git a/packages/genai/lib/llm_request.dart b/packages/genai/lib/llm_request.dart deleted file mode 100644 index 041fc381..00000000 --- a/packages/genai/lib/llm_request.dart +++ /dev/null @@ -1,13 +0,0 @@ -class LLMRequestDetails { - String endpoint; - Map headers; - String method; - Map body; - - LLMRequestDetails({ - required this.endpoint, - required this.headers, - required this.method, - required this.body, - }); -} diff --git a/packages/genai/lib/llm_saveobject.dart b/packages/genai/lib/llm_saveobject.dart deleted file mode 100644 index 7d315789..00000000 --- a/packages/genai/lib/llm_saveobject.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'llm_config.dart'; -import 'llm_model.dart'; -import 'llm_provider.dart'; - -class LLMSaveObject { - String endpoint; - String credential; - LLMProvider provider; - LLMModel selectedLLM; - Map configMap; - - LLMSaveObject({ - required this.endpoint, - required this.credential, - required this.configMap, - required this.selectedLLM, - required this.provider, - }); - - Map toJSON() { - Map cmap = {}; - for (final e in configMap.entries) { - cmap[e.key] = e.value.toJson(); - } - return { - 'endpoint': endpoint, - 'credential': credential, - 'config_map': cmap, - 'selected_llm': selectedLLM.identifier, - 'provider': provider.name, - }; - } - - static LLMSaveObject fromJSON(Map json) { - Map cmap = {}; - for (final k in json['config_map'].keys) { - cmap[k] = LLMModelConfiguration.fromJson(json['config_map'][k]); - } - final provider = LLMProvider.fromName(json['provider']); - return LLMSaveObject( - endpoint: json['endpoint'], - credential: json['credential'], - configMap: cmap, - selectedLLM: provider.getLLMByIdentifier(json['selected_llm']), - provider: provider, - ); - } -} diff --git a/packages/genai/lib/models/ai_request_model.dart b/packages/genai/lib/models/ai_request_model.dart index 07d66392..7ed9e4fc 100644 --- a/packages/genai/lib/models/ai_request_model.dart +++ b/packages/genai/lib/models/ai_request_model.dart @@ -1,15 +1,7 @@ -import 'dart:convert'; - import 'package:better_networking/better_networking.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:genai/llm_config.dart'; -import '../llm_model.dart'; -import '../llm_provider.dart'; -import '../llm_saveobject.dart'; -import '../llm_input_payload.dart'; -import '../llm_request.dart'; -import '../providers/gemini.dart'; -import '../providers/providers.dart'; +import '../interface/interface.dart'; +import 'model_request_data.dart'; part 'ai_request_model.freezed.dart'; part 'ai_request_model.g.dart'; @@ -18,67 +10,20 @@ class AIRequestModel with _$AIRequestModel { const AIRequestModel._(); @JsonSerializable(explicitToJson: true, anyMap: true) - factory AIRequestModel({ - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - required LLMInputPayload payload, - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - required LLMModel model, - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - required LLMProvider provider, + const factory AIRequestModel({ + ModelAPIProvider? modelProvider, + ModelRequestData? modelRequestData, }) = _AIRequestModel; factory AIRequestModel.fromJson(Map json) => _$AIRequestModelFromJson(json); - AIRequestModel updatePayload(LLMInputPayload p) { - return AIRequestModel(payload: p, model: model, provider: provider); - } + HttpRequestModel? get httpRequestModel => + kModelProvidersMap[modelProvider]?.createRequest(modelRequestData); - LLMRequestDetails createRequest({bool stream = false}) { - final controller = model.provider.modelController; - return controller.createRequest(model, payload, stream: stream); - } + String? getFormattedOutput(Map x) => + kModelProvidersMap[modelProvider]?.outputFormatter(x); - factory AIRequestModel.fromDefaultSaveObject(LLMSaveObject? defaultLLMSO) { - final gmC = GeminiModelController.instance; - return AIRequestModel( - model: - defaultLLMSO?.selectedLLM ?? - LLMProvider.gemini.getLLMByIdentifier('gemini-2.0-flash'), - provider: defaultLLMSO?.provider ?? LLMProvider.gemini, - payload: LLMInputPayload( - endpoint: defaultLLMSO?.endpoint ?? gmC.inputPayload.endpoint, - credential: defaultLLMSO?.credential ?? '', - systemPrompt: '', - userPrompt: '', - configMap: defaultLLMSO?.configMap ?? gmC.inputPayload.configMap, - ), - ); - } - - AIRequestModel clone() { - return AIRequestModel( - model: model, - payload: payload.clone(), - provider: provider, - ); - } - - HttpRequestModel convertToHTTPRequest() { - final streamingMode = - payload.configMap[LLMConfigName.stream.name]?.configValue.value ?? - false; - final genAIRequest = createRequest(stream: streamingMode); - return HttpRequestModel( - method: HTTPVerb.post, - headers: [ - ...genAIRequest.headers.entries.map( - (x) => NameValueModel(name: x.key, value: x.value), - ), - ], - url: genAIRequest.endpoint, - bodyContentType: ContentType.json, - body: jsonEncode(genAIRequest.body), - ); - } + String? getFormattedStreamOutput(Map x) => + kModelProvidersMap[modelProvider]?.streamOutputFormatter(x); } diff --git a/packages/genai/lib/models/ai_request_model.freezed.dart b/packages/genai/lib/models/ai_request_model.freezed.dart index 84890f0a..7d9b3220 100644 --- a/packages/genai/lib/models/ai_request_model.freezed.dart +++ b/packages/genai/lib/models/ai_request_model.freezed.dart @@ -21,12 +21,8 @@ AIRequestModel _$AIRequestModelFromJson(Map json) { /// @nodoc mixin _$AIRequestModel { - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - LLMInputPayload get payload => throw _privateConstructorUsedError; - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - LLMModel get model => throw _privateConstructorUsedError; - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - LLMProvider get provider => throw _privateConstructorUsedError; + ModelAPIProvider? get modelProvider => throw _privateConstructorUsedError; + ModelRequestData? get modelRequestData => throw _privateConstructorUsedError; /// Serializes this AIRequestModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -46,13 +42,11 @@ abstract class $AIRequestModelCopyWith<$Res> { ) = _$AIRequestModelCopyWithImpl<$Res, AIRequestModel>; @useResult $Res call({ - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - LLMInputPayload payload, - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - LLMModel model, - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - LLMProvider provider, + ModelAPIProvider? modelProvider, + ModelRequestData? modelRequestData, }); + + $ModelRequestDataCopyWith<$Res>? get modelRequestData; } /// @nodoc @@ -70,28 +64,37 @@ class _$AIRequestModelCopyWithImpl<$Res, $Val extends AIRequestModel> @pragma('vm:prefer-inline') @override $Res call({ - Object? payload = null, - Object? model = null, - Object? provider = null, + Object? modelProvider = freezed, + Object? modelRequestData = freezed, }) { return _then( _value.copyWith( - payload: null == payload - ? _value.payload - : payload // ignore: cast_nullable_to_non_nullable - as LLMInputPayload, - model: null == model - ? _value.model - : model // ignore: cast_nullable_to_non_nullable - as LLMModel, - provider: null == provider - ? _value.provider - : provider // ignore: cast_nullable_to_non_nullable - as LLMProvider, + modelProvider: freezed == modelProvider + ? _value.modelProvider + : modelProvider // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + modelRequestData: freezed == modelRequestData + ? _value.modelRequestData + : modelRequestData // ignore: cast_nullable_to_non_nullable + as ModelRequestData?, ) as $Val, ); } + + /// Create a copy of AIRequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ModelRequestDataCopyWith<$Res>? get modelRequestData { + if (_value.modelRequestData == null) { + return null; + } + + return $ModelRequestDataCopyWith<$Res>(_value.modelRequestData!, (value) { + return _then(_value.copyWith(modelRequestData: value) as $Val); + }); + } } /// @nodoc @@ -104,13 +107,12 @@ abstract class _$$AIRequestModelImplCopyWith<$Res> @override @useResult $Res call({ - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - LLMInputPayload payload, - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - LLMModel model, - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - LLMProvider provider, + ModelAPIProvider? modelProvider, + ModelRequestData? modelRequestData, }); + + @override + $ModelRequestDataCopyWith<$Res>? get modelRequestData; } /// @nodoc @@ -127,24 +129,19 @@ class __$$AIRequestModelImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? payload = null, - Object? model = null, - Object? provider = null, + Object? modelProvider = freezed, + Object? modelRequestData = freezed, }) { return _then( _$AIRequestModelImpl( - payload: null == payload - ? _value.payload - : payload // ignore: cast_nullable_to_non_nullable - as LLMInputPayload, - model: null == model - ? _value.model - : model // ignore: cast_nullable_to_non_nullable - as LLMModel, - provider: null == provider - ? _value.provider - : provider // ignore: cast_nullable_to_non_nullable - as LLMProvider, + modelProvider: freezed == modelProvider + ? _value.modelProvider + : modelProvider // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + modelRequestData: freezed == modelRequestData + ? _value.modelRequestData + : modelRequestData // ignore: cast_nullable_to_non_nullable + as ModelRequestData?, ), ); } @@ -154,31 +151,20 @@ class __$$AIRequestModelImplCopyWithImpl<$Res> @JsonSerializable(explicitToJson: true, anyMap: true) class _$AIRequestModelImpl extends _AIRequestModel { - _$AIRequestModelImpl({ - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - required this.payload, - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - required this.model, - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - required this.provider, - }) : super._(); + const _$AIRequestModelImpl({this.modelProvider, this.modelRequestData}) + : super._(); factory _$AIRequestModelImpl.fromJson(Map json) => _$$AIRequestModelImplFromJson(json); @override - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - final LLMInputPayload payload; + final ModelAPIProvider? modelProvider; @override - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - final LLMModel model; - @override - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - final LLMProvider provider; + final ModelRequestData? modelRequestData; @override String toString() { - return 'AIRequestModel(payload: $payload, model: $model, provider: $provider)'; + return 'AIRequestModel(modelProvider: $modelProvider, modelRequestData: $modelRequestData)'; } @override @@ -186,15 +172,15 @@ class _$AIRequestModelImpl extends _AIRequestModel { return identical(this, other) || (other.runtimeType == runtimeType && other is _$AIRequestModelImpl && - (identical(other.payload, payload) || other.payload == payload) && - (identical(other.model, model) || other.model == model) && - (identical(other.provider, provider) || - other.provider == provider)); + (identical(other.modelProvider, modelProvider) || + other.modelProvider == modelProvider) && + (identical(other.modelRequestData, modelRequestData) || + other.modelRequestData == modelRequestData)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, payload, model, provider); + int get hashCode => Object.hash(runtimeType, modelProvider, modelRequestData); /// Create a copy of AIRequestModel /// with the given fields replaced by the non-null parameter values. @@ -214,28 +200,19 @@ class _$AIRequestModelImpl extends _AIRequestModel { } abstract class _AIRequestModel extends AIRequestModel { - factory _AIRequestModel({ - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - required final LLMInputPayload payload, - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - required final LLMModel model, - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - required final LLMProvider provider, + const factory _AIRequestModel({ + final ModelAPIProvider? modelProvider, + final ModelRequestData? modelRequestData, }) = _$AIRequestModelImpl; - _AIRequestModel._() : super._(); + const _AIRequestModel._() : super._(); factory _AIRequestModel.fromJson(Map json) = _$AIRequestModelImpl.fromJson; @override - @JsonKey(fromJson: LLMInputPayload.fromJSON, toJson: LLMInputPayload.toJSON) - LLMInputPayload get payload; + ModelAPIProvider? get modelProvider; @override - @JsonKey(fromJson: LLMModel.fromJson, toJson: LLMModel.toJson) - LLMModel get model; - @override - @JsonKey(fromJson: LLMProvider.fromJSON, toJson: LLMProvider.toJSON) - LLMProvider get provider; + ModelRequestData? get modelRequestData; /// Create a copy of AIRequestModel /// with the given fields replaced by the non-null parameter values. diff --git a/packages/genai/lib/models/ai_request_model.g.dart b/packages/genai/lib/models/ai_request_model.g.dart index df256f43..182fe1cb 100644 --- a/packages/genai/lib/models/ai_request_model.g.dart +++ b/packages/genai/lib/models/ai_request_model.g.dart @@ -8,15 +8,28 @@ part of 'ai_request_model.dart'; _$AIRequestModelImpl _$$AIRequestModelImplFromJson(Map json) => _$AIRequestModelImpl( - payload: LLMInputPayload.fromJSON(json['payload'] as Map), - model: LLMModel.fromJson(json['model'] as Map), - provider: LLMProvider.fromJSON(json['provider'] as Map), + modelProvider: $enumDecodeNullable( + _$ModelAPIProviderEnumMap, + json['modelProvider'], + ), + modelRequestData: json['modelRequestData'] == null + ? null + : ModelRequestData.fromJson( + Map.from(json['modelRequestData'] as Map), + ), ); Map _$$AIRequestModelImplToJson( _$AIRequestModelImpl instance, ) => { - 'payload': LLMInputPayload.toJSON(instance.payload), - 'model': LLMModel.toJson(instance.model), - 'provider': LLMProvider.toJSON(instance.provider), + 'modelProvider': _$ModelAPIProviderEnumMap[instance.modelProvider], + 'modelRequestData': instance.modelRequestData?.toJson(), +}; + +const _$ModelAPIProviderEnumMap = { + ModelAPIProvider.openai: 'openai', + ModelAPIProvider.anthropic: 'anthropic', + ModelAPIProvider.gemini: 'gemini', + ModelAPIProvider.azureopenai: 'azureopenai', + ModelAPIProvider.ollama: 'ollama', }; diff --git a/packages/genai/lib/models/available_models.dart b/packages/genai/lib/models/available_models.dart new file mode 100644 index 00000000..ef9fc306 --- /dev/null +++ b/packages/genai/lib/models/available_models.dart @@ -0,0 +1,56 @@ +// To parse this JSON data, do +// +// final availableModels = availableModelsFromJson(jsonString); + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:convert'; +import '../interface/interface.dart'; +part 'available_models.freezed.dart'; +part 'available_models.g.dart'; + +AvailableModels availableModelsFromJson(String str) => + AvailableModels.fromJson(json.decode(str)); + +String availableModelsToJson(AvailableModels data) => + json.encode(data.toJson()); + +@freezed +class AvailableModels with _$AvailableModels { + const AvailableModels._(); + const factory AvailableModels({ + @JsonKey(name: "version") required double version, + @JsonKey(name: "model_providers") + required List modelProviders, + }) = _AvailableModels; + + factory AvailableModels.fromJson(Map json) => + _$AvailableModelsFromJson(json); + + Map get map => + modelProviders.asMap().map( + (i, d) => MapEntry(d.providerId!, d), + ); +} + +@freezed +class AIModelProvider with _$AIModelProvider { + const factory AIModelProvider({ + @JsonKey(name: "provider_id") ModelAPIProvider? providerId, + @JsonKey(name: "provider_name") String? providerName, + @JsonKey(name: "source_url") String? sourceUrl, + @JsonKey(name: "models") List? models, + }) = _AIModelProvider; + + factory AIModelProvider.fromJson(Map json) => + _$AIModelProviderFromJson(json); +} + +@freezed +class Model with _$Model { + const factory Model({ + @JsonKey(name: "id") String? id, + @JsonKey(name: "name") String? name, + }) = _Model; + + factory Model.fromJson(Map json) => _$ModelFromJson(json); +} diff --git a/packages/genai/lib/models/available_models.freezed.dart b/packages/genai/lib/models/available_models.freezed.dart new file mode 100644 index 00000000..725ef027 --- /dev/null +++ b/packages/genai/lib/models/available_models.freezed.dart @@ -0,0 +1,651 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'available_models.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AvailableModels _$AvailableModelsFromJson(Map json) { + return _AvailableModels.fromJson(json); +} + +/// @nodoc +mixin _$AvailableModels { + @JsonKey(name: "version") + double get version => throw _privateConstructorUsedError; + @JsonKey(name: "model_providers") + List get modelProviders => + throw _privateConstructorUsedError; + + /// Serializes this AvailableModels to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AvailableModelsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AvailableModelsCopyWith<$Res> { + factory $AvailableModelsCopyWith( + AvailableModels value, + $Res Function(AvailableModels) then, + ) = _$AvailableModelsCopyWithImpl<$Res, AvailableModels>; + @useResult + $Res call({ + @JsonKey(name: "version") double version, + @JsonKey(name: "model_providers") List modelProviders, + }); +} + +/// @nodoc +class _$AvailableModelsCopyWithImpl<$Res, $Val extends AvailableModels> + implements $AvailableModelsCopyWith<$Res> { + _$AvailableModelsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? version = null, Object? modelProviders = null}) { + return _then( + _value.copyWith( + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as double, + modelProviders: null == modelProviders + ? _value.modelProviders + : modelProviders // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AvailableModelsImplCopyWith<$Res> + implements $AvailableModelsCopyWith<$Res> { + factory _$$AvailableModelsImplCopyWith( + _$AvailableModelsImpl value, + $Res Function(_$AvailableModelsImpl) then, + ) = __$$AvailableModelsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: "version") double version, + @JsonKey(name: "model_providers") List modelProviders, + }); +} + +/// @nodoc +class __$$AvailableModelsImplCopyWithImpl<$Res> + extends _$AvailableModelsCopyWithImpl<$Res, _$AvailableModelsImpl> + implements _$$AvailableModelsImplCopyWith<$Res> { + __$$AvailableModelsImplCopyWithImpl( + _$AvailableModelsImpl _value, + $Res Function(_$AvailableModelsImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? version = null, Object? modelProviders = null}) { + return _then( + _$AvailableModelsImpl( + version: null == version + ? _value.version + : version // ignore: cast_nullable_to_non_nullable + as double, + modelProviders: null == modelProviders + ? _value._modelProviders + : modelProviders // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AvailableModelsImpl extends _AvailableModels { + const _$AvailableModelsImpl({ + @JsonKey(name: "version") required this.version, + @JsonKey(name: "model_providers") + required final List modelProviders, + }) : _modelProviders = modelProviders, + super._(); + + factory _$AvailableModelsImpl.fromJson(Map json) => + _$$AvailableModelsImplFromJson(json); + + @override + @JsonKey(name: "version") + final double version; + final List _modelProviders; + @override + @JsonKey(name: "model_providers") + List get modelProviders { + if (_modelProviders is EqualUnmodifiableListView) return _modelProviders; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_modelProviders); + } + + @override + String toString() { + return 'AvailableModels(version: $version, modelProviders: $modelProviders)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AvailableModelsImpl && + (identical(other.version, version) || other.version == version) && + const DeepCollectionEquality().equals( + other._modelProviders, + _modelProviders, + )); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + version, + const DeepCollectionEquality().hash(_modelProviders), + ); + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AvailableModelsImplCopyWith<_$AvailableModelsImpl> get copyWith => + __$$AvailableModelsImplCopyWithImpl<_$AvailableModelsImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AvailableModelsImplToJson(this); + } +} + +abstract class _AvailableModels extends AvailableModels { + const factory _AvailableModels({ + @JsonKey(name: "version") required final double version, + @JsonKey(name: "model_providers") + required final List modelProviders, + }) = _$AvailableModelsImpl; + const _AvailableModels._() : super._(); + + factory _AvailableModels.fromJson(Map json) = + _$AvailableModelsImpl.fromJson; + + @override + @JsonKey(name: "version") + double get version; + @override + @JsonKey(name: "model_providers") + List get modelProviders; + + /// Create a copy of AvailableModels + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AvailableModelsImplCopyWith<_$AvailableModelsImpl> get copyWith => + throw _privateConstructorUsedError; +} + +AIModelProvider _$AIModelProviderFromJson(Map json) { + return _AIModelProvider.fromJson(json); +} + +/// @nodoc +mixin _$AIModelProvider { + @JsonKey(name: "provider_id") + ModelAPIProvider? get providerId => throw _privateConstructorUsedError; + @JsonKey(name: "provider_name") + String? get providerName => throw _privateConstructorUsedError; + @JsonKey(name: "source_url") + String? get sourceUrl => throw _privateConstructorUsedError; + @JsonKey(name: "models") + List? get models => throw _privateConstructorUsedError; + + /// Serializes this AIModelProvider to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AIModelProviderCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AIModelProviderCopyWith<$Res> { + factory $AIModelProviderCopyWith( + AIModelProvider value, + $Res Function(AIModelProvider) then, + ) = _$AIModelProviderCopyWithImpl<$Res, AIModelProvider>; + @useResult + $Res call({ + @JsonKey(name: "provider_id") ModelAPIProvider? providerId, + @JsonKey(name: "provider_name") String? providerName, + @JsonKey(name: "source_url") String? sourceUrl, + @JsonKey(name: "models") List? models, + }); +} + +/// @nodoc +class _$AIModelProviderCopyWithImpl<$Res, $Val extends AIModelProvider> + implements $AIModelProviderCopyWith<$Res> { + _$AIModelProviderCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? providerId = freezed, + Object? providerName = freezed, + Object? sourceUrl = freezed, + Object? models = freezed, + }) { + return _then( + _value.copyWith( + providerId: freezed == providerId + ? _value.providerId + : providerId // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + providerName: freezed == providerName + ? _value.providerName + : providerName // ignore: cast_nullable_to_non_nullable + as String?, + sourceUrl: freezed == sourceUrl + ? _value.sourceUrl + : sourceUrl // ignore: cast_nullable_to_non_nullable + as String?, + models: freezed == models + ? _value.models + : models // ignore: cast_nullable_to_non_nullable + as List?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AIModelProviderImplCopyWith<$Res> + implements $AIModelProviderCopyWith<$Res> { + factory _$$AIModelProviderImplCopyWith( + _$AIModelProviderImpl value, + $Res Function(_$AIModelProviderImpl) then, + ) = __$$AIModelProviderImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: "provider_id") ModelAPIProvider? providerId, + @JsonKey(name: "provider_name") String? providerName, + @JsonKey(name: "source_url") String? sourceUrl, + @JsonKey(name: "models") List? models, + }); +} + +/// @nodoc +class __$$AIModelProviderImplCopyWithImpl<$Res> + extends _$AIModelProviderCopyWithImpl<$Res, _$AIModelProviderImpl> + implements _$$AIModelProviderImplCopyWith<$Res> { + __$$AIModelProviderImplCopyWithImpl( + _$AIModelProviderImpl _value, + $Res Function(_$AIModelProviderImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? providerId = freezed, + Object? providerName = freezed, + Object? sourceUrl = freezed, + Object? models = freezed, + }) { + return _then( + _$AIModelProviderImpl( + providerId: freezed == providerId + ? _value.providerId + : providerId // ignore: cast_nullable_to_non_nullable + as ModelAPIProvider?, + providerName: freezed == providerName + ? _value.providerName + : providerName // ignore: cast_nullable_to_non_nullable + as String?, + sourceUrl: freezed == sourceUrl + ? _value.sourceUrl + : sourceUrl // ignore: cast_nullable_to_non_nullable + as String?, + models: freezed == models + ? _value._models + : models // ignore: cast_nullable_to_non_nullable + as List?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AIModelProviderImpl implements _AIModelProvider { + const _$AIModelProviderImpl({ + @JsonKey(name: "provider_id") this.providerId, + @JsonKey(name: "provider_name") this.providerName, + @JsonKey(name: "source_url") this.sourceUrl, + @JsonKey(name: "models") final List? models, + }) : _models = models; + + factory _$AIModelProviderImpl.fromJson(Map json) => + _$$AIModelProviderImplFromJson(json); + + @override + @JsonKey(name: "provider_id") + final ModelAPIProvider? providerId; + @override + @JsonKey(name: "provider_name") + final String? providerName; + @override + @JsonKey(name: "source_url") + final String? sourceUrl; + final List? _models; + @override + @JsonKey(name: "models") + List? get models { + final value = _models; + if (value == null) return null; + if (_models is EqualUnmodifiableListView) return _models; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'AIModelProvider(providerId: $providerId, providerName: $providerName, sourceUrl: $sourceUrl, models: $models)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AIModelProviderImpl && + (identical(other.providerId, providerId) || + other.providerId == providerId) && + (identical(other.providerName, providerName) || + other.providerName == providerName) && + (identical(other.sourceUrl, sourceUrl) || + other.sourceUrl == sourceUrl) && + const DeepCollectionEquality().equals(other._models, _models)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + providerId, + providerName, + sourceUrl, + const DeepCollectionEquality().hash(_models), + ); + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AIModelProviderImplCopyWith<_$AIModelProviderImpl> get copyWith => + __$$AIModelProviderImplCopyWithImpl<_$AIModelProviderImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AIModelProviderImplToJson(this); + } +} + +abstract class _AIModelProvider implements AIModelProvider { + const factory _AIModelProvider({ + @JsonKey(name: "provider_id") final ModelAPIProvider? providerId, + @JsonKey(name: "provider_name") final String? providerName, + @JsonKey(name: "source_url") final String? sourceUrl, + @JsonKey(name: "models") final List? models, + }) = _$AIModelProviderImpl; + + factory _AIModelProvider.fromJson(Map json) = + _$AIModelProviderImpl.fromJson; + + @override + @JsonKey(name: "provider_id") + ModelAPIProvider? get providerId; + @override + @JsonKey(name: "provider_name") + String? get providerName; + @override + @JsonKey(name: "source_url") + String? get sourceUrl; + @override + @JsonKey(name: "models") + List? get models; + + /// Create a copy of AIModelProvider + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AIModelProviderImplCopyWith<_$AIModelProviderImpl> get copyWith => + throw _privateConstructorUsedError; +} + +Model _$ModelFromJson(Map json) { + return _Model.fromJson(json); +} + +/// @nodoc +mixin _$Model { + @JsonKey(name: "id") + String? get id => throw _privateConstructorUsedError; + @JsonKey(name: "name") + String? get name => throw _privateConstructorUsedError; + + /// Serializes this Model to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ModelCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ModelCopyWith<$Res> { + factory $ModelCopyWith(Model value, $Res Function(Model) then) = + _$ModelCopyWithImpl<$Res, Model>; + @useResult + $Res call({ + @JsonKey(name: "id") String? id, + @JsonKey(name: "name") String? name, + }); +} + +/// @nodoc +class _$ModelCopyWithImpl<$Res, $Val extends Model> + implements $ModelCopyWith<$Res> { + _$ModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? id = freezed, Object? name = freezed}) { + return _then( + _value.copyWith( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ModelImplCopyWith<$Res> implements $ModelCopyWith<$Res> { + factory _$$ModelImplCopyWith( + _$ModelImpl value, + $Res Function(_$ModelImpl) then, + ) = __$$ModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: "id") String? id, + @JsonKey(name: "name") String? name, + }); +} + +/// @nodoc +class __$$ModelImplCopyWithImpl<$Res> + extends _$ModelCopyWithImpl<$Res, _$ModelImpl> + implements _$$ModelImplCopyWith<$Res> { + __$$ModelImplCopyWithImpl( + _$ModelImpl _value, + $Res Function(_$ModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? id = freezed, Object? name = freezed}) { + return _then( + _$ModelImpl( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ModelImpl implements _Model { + const _$ModelImpl({ + @JsonKey(name: "id") this.id, + @JsonKey(name: "name") this.name, + }); + + factory _$ModelImpl.fromJson(Map json) => + _$$ModelImplFromJson(json); + + @override + @JsonKey(name: "id") + final String? id; + @override + @JsonKey(name: "name") + final String? name; + + @override + String toString() { + return 'Model(id: $id, name: $name)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, name); + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ModelImplCopyWith<_$ModelImpl> get copyWith => + __$$ModelImplCopyWithImpl<_$ModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ModelImplToJson(this); + } +} + +abstract class _Model implements Model { + const factory _Model({ + @JsonKey(name: "id") final String? id, + @JsonKey(name: "name") final String? name, + }) = _$ModelImpl; + + factory _Model.fromJson(Map json) = _$ModelImpl.fromJson; + + @override + @JsonKey(name: "id") + String? get id; + @override + @JsonKey(name: "name") + String? get name; + + /// Create a copy of Model + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ModelImplCopyWith<_$ModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/genai/lib/models/available_models.g.dart b/packages/genai/lib/models/available_models.g.dart new file mode 100644 index 00000000..a8bb7816 --- /dev/null +++ b/packages/genai/lib/models/available_models.g.dart @@ -0,0 +1,60 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'available_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AvailableModelsImpl _$$AvailableModelsImplFromJson( + Map json, +) => _$AvailableModelsImpl( + version: (json['version'] as num).toDouble(), + modelProviders: (json['model_providers'] as List) + .map((e) => AIModelProvider.fromJson(e as Map)) + .toList(), +); + +Map _$$AvailableModelsImplToJson( + _$AvailableModelsImpl instance, +) => { + 'version': instance.version, + 'model_providers': instance.modelProviders, +}; + +_$AIModelProviderImpl _$$AIModelProviderImplFromJson( + Map json, +) => _$AIModelProviderImpl( + providerId: $enumDecodeNullable( + _$ModelAPIProviderEnumMap, + json['provider_id'], + ), + providerName: json['provider_name'] as String?, + sourceUrl: json['source_url'] as String?, + models: (json['models'] as List?) + ?.map((e) => Model.fromJson(e as Map)) + .toList(), +); + +Map _$$AIModelProviderImplToJson( + _$AIModelProviderImpl instance, +) => { + 'provider_id': _$ModelAPIProviderEnumMap[instance.providerId], + 'provider_name': instance.providerName, + 'source_url': instance.sourceUrl, + 'models': instance.models, +}; + +const _$ModelAPIProviderEnumMap = { + ModelAPIProvider.openai: 'openai', + ModelAPIProvider.anthropic: 'anthropic', + ModelAPIProvider.gemini: 'gemini', + ModelAPIProvider.azureopenai: 'azureopenai', + ModelAPIProvider.ollama: 'ollama', +}; + +_$ModelImpl _$$ModelImplFromJson(Map json) => + _$ModelImpl(id: json['id'] as String?, name: json['name'] as String?); + +Map _$$ModelImplToJson(_$ModelImpl instance) => + {'id': instance.id, 'name': instance.name}; diff --git a/packages/genai/lib/models/model_config.dart b/packages/genai/lib/models/model_config.dart new file mode 100644 index 00000000..15868d0c --- /dev/null +++ b/packages/genai/lib/models/model_config.dart @@ -0,0 +1,74 @@ +import 'model_config_value.dart'; + +class ModelConfig { + final String id; + final String name; + final String description; + final ConfigType type; + final ConfigValue value; + + ModelConfig({ + required this.id, + required this.name, + required this.description, + required this.type, + required this.value, + }) { + assert(checkTypeValue(type, value)); + } + + ModelConfig updateValue(ConfigValue value) { + return ModelConfig( + id: id, + name: name, + description: description, + type: type, + value: value, + ); + } + + factory ModelConfig.fromJson(Map x) { + final id = x['id'] as String?; + final name = x['name'] as String?; + final description = x['description'] as String?; + final type = x['type'] as String?; + final value = x['value'] as String?; + + final cT = getConfigTypeEnum(type); + final cV = deserilizeValue(cT, value); + + return ModelConfig( + id: id ?? "", + name: name ?? "", + description: description ?? "", + type: cT, + value: cV, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'description': description, + 'type': type.name.toString(), + 'value': value.serialize(), + }; + } + + ModelConfig copyWith({ + String? id, + String? name, + String? description, + ConfigType? type, + ConfigValue? value, + }) { + return ModelConfig( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + type: type ?? this.type, + value: value ?? this.value, + ); + } +} diff --git a/packages/genai/lib/models/model_config_value.dart b/packages/genai/lib/models/model_config_value.dart new file mode 100644 index 00000000..1aa27552 --- /dev/null +++ b/packages/genai/lib/models/model_config_value.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; + +enum ConfigType { boolean, slider, numeric, text } + +ConfigType getConfigTypeEnum(String? t) { + try { + final val = ConfigType.values.byName(t ?? ""); + return val; + } catch (e) { + debugPrint("ConfigType <$t> not found."); + return ConfigType.text; + } +} + +bool checkTypeValue(ConfigType t, dynamic v) { + return switch (t) { + ConfigType.boolean => v is ConfigBooleanValue, + ConfigType.slider => v is ConfigSliderValue, + ConfigType.numeric => v is ConfigNumericValue, + ConfigType.text => v is ConfigTextValue, + }; +} + +dynamic deserilizeValue(ConfigType t, String? v) { + return switch (t) { + ConfigType.boolean => ConfigBooleanValue.deserialize(v ?? ""), + ConfigType.slider => ConfigSliderValue.deserialize(v ?? ""), + ConfigType.numeric => ConfigNumericValue.deserialize(v ?? ""), + ConfigType.text => ConfigTextValue.deserialize(v ?? ""), + }; +} + +abstract class ConfigValue { + ConfigValue(this.value); + + dynamic value; + + String serialize(); + + dynamic getPayloadValue() { + return value; + } +} + +class ConfigBooleanValue extends ConfigValue { + ConfigBooleanValue({required bool value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static ConfigBooleanValue deserialize(String x) { + return ConfigBooleanValue(value: x == 'true'); + } +} + +class ConfigNumericValue extends ConfigValue { + ConfigNumericValue({required num? value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static ConfigNumericValue deserialize(String x) { + return ConfigNumericValue(value: num.tryParse(x)); + } +} + +class ConfigSliderValue extends ConfigValue { + ConfigSliderValue({required (double, double, double) value}) : super(value); + + @override + String serialize() { + final v = value as (double, double, double); + return jsonEncode([v.$1, v.$2, v.$3]); + } + + @override + dynamic getPayloadValue() { + final v = value as (double, double, double); + return v.$2; + } + + static ConfigSliderValue deserialize(String x) { + final z = jsonDecode(x) as List; + final val = ( + double.parse(z[0].toString()), + double.parse(z[1].toString()), + double.parse(z[2].toString()), + ); + return ConfigSliderValue(value: val); + } +} + +class ConfigTextValue extends ConfigValue { + ConfigTextValue({required String value}) : super(value); + + @override + String serialize() { + return value.toString(); + } + + static ConfigTextValue deserialize(String x) { + return ConfigTextValue(value: x); + } +} diff --git a/packages/genai/lib/models/model_provider.dart b/packages/genai/lib/models/model_provider.dart new file mode 100644 index 00000000..70b019fc --- /dev/null +++ b/packages/genai/lib/models/model_provider.dart @@ -0,0 +1,18 @@ +import 'package:better_networking/better_networking.dart'; +import '../models/models.dart'; + +abstract class ModelProvider { + ModelRequestData get defaultRequestData => throw UnimplementedError(); + + HttpRequestModel? createRequest(ModelRequestData? requestData) { + throw UnimplementedError(); + } + + String? outputFormatter(Map x) { + throw UnimplementedError(); + } + + String? streamOutputFormatter(Map x) { + throw UnimplementedError(); + } +} diff --git a/packages/genai/lib/models/model_request_data.dart b/packages/genai/lib/models/model_request_data.dart new file mode 100644 index 00000000..7fa04e33 --- /dev/null +++ b/packages/genai/lib/models/model_request_data.dart @@ -0,0 +1,33 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'model_config.dart'; +part 'model_request_data.freezed.dart'; +part 'model_request_data.g.dart'; + +@freezed +class ModelRequestData with _$ModelRequestData { + const ModelRequestData._(); + + @JsonSerializable(explicitToJson: true, anyMap: true) + const factory ModelRequestData({ + @Default("") String url, + @Default("") String model, + @Default("") String apiKey, + @JsonKey(name: "system_prompt") @Default("") String systemPrompt, + @JsonKey(name: "user_prompt") @Default("") String userPrompt, + @JsonKey(name: "model_configs") + @Default([]) + List modelConfigs, + @Default(null) bool? stream, + }) = _ModelRequestData; + + factory ModelRequestData.fromJson(Map json) => + _$ModelRequestDataFromJson(json); + + Map getModelConfigMap() { + Map m = {}; + for (var config in modelConfigs) { + m[config.id] = config.value.getPayloadValue(); + } + return m; + } +} diff --git a/packages/genai/lib/models/model_request_data.freezed.dart b/packages/genai/lib/models/model_request_data.freezed.dart new file mode 100644 index 00000000..35c0a7a5 --- /dev/null +++ b/packages/genai/lib/models/model_request_data.freezed.dart @@ -0,0 +1,339 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'model_request_data.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +ModelRequestData _$ModelRequestDataFromJson(Map json) { + return _ModelRequestData.fromJson(json); +} + +/// @nodoc +mixin _$ModelRequestData { + String get url => throw _privateConstructorUsedError; + String get model => throw _privateConstructorUsedError; + String get apiKey => throw _privateConstructorUsedError; + @JsonKey(name: "system_prompt") + String get systemPrompt => throw _privateConstructorUsedError; + @JsonKey(name: "user_prompt") + String get userPrompt => throw _privateConstructorUsedError; + @JsonKey(name: "model_configs") + List get modelConfigs => throw _privateConstructorUsedError; + bool? get stream => throw _privateConstructorUsedError; + + /// Serializes this ModelRequestData to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ModelRequestData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ModelRequestDataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ModelRequestDataCopyWith<$Res> { + factory $ModelRequestDataCopyWith( + ModelRequestData value, + $Res Function(ModelRequestData) then, + ) = _$ModelRequestDataCopyWithImpl<$Res, ModelRequestData>; + @useResult + $Res call({ + String url, + String model, + String apiKey, + @JsonKey(name: "system_prompt") String systemPrompt, + @JsonKey(name: "user_prompt") String userPrompt, + @JsonKey(name: "model_configs") List modelConfigs, + bool? stream, + }); +} + +/// @nodoc +class _$ModelRequestDataCopyWithImpl<$Res, $Val extends ModelRequestData> + implements $ModelRequestDataCopyWith<$Res> { + _$ModelRequestDataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ModelRequestData + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? url = null, + Object? model = null, + Object? apiKey = null, + Object? systemPrompt = null, + Object? userPrompt = null, + Object? modelConfigs = null, + Object? stream = freezed, + }) { + return _then( + _value.copyWith( + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as String, + apiKey: null == apiKey + ? _value.apiKey + : apiKey // ignore: cast_nullable_to_non_nullable + as String, + systemPrompt: null == systemPrompt + ? _value.systemPrompt + : systemPrompt // ignore: cast_nullable_to_non_nullable + as String, + userPrompt: null == userPrompt + ? _value.userPrompt + : userPrompt // ignore: cast_nullable_to_non_nullable + as String, + modelConfigs: null == modelConfigs + ? _value.modelConfigs + : modelConfigs // ignore: cast_nullable_to_non_nullable + as List, + stream: freezed == stream + ? _value.stream + : stream // ignore: cast_nullable_to_non_nullable + as bool?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ModelRequestDataImplCopyWith<$Res> + implements $ModelRequestDataCopyWith<$Res> { + factory _$$ModelRequestDataImplCopyWith( + _$ModelRequestDataImpl value, + $Res Function(_$ModelRequestDataImpl) then, + ) = __$$ModelRequestDataImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String url, + String model, + String apiKey, + @JsonKey(name: "system_prompt") String systemPrompt, + @JsonKey(name: "user_prompt") String userPrompt, + @JsonKey(name: "model_configs") List modelConfigs, + bool? stream, + }); +} + +/// @nodoc +class __$$ModelRequestDataImplCopyWithImpl<$Res> + extends _$ModelRequestDataCopyWithImpl<$Res, _$ModelRequestDataImpl> + implements _$$ModelRequestDataImplCopyWith<$Res> { + __$$ModelRequestDataImplCopyWithImpl( + _$ModelRequestDataImpl _value, + $Res Function(_$ModelRequestDataImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ModelRequestData + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? url = null, + Object? model = null, + Object? apiKey = null, + Object? systemPrompt = null, + Object? userPrompt = null, + Object? modelConfigs = null, + Object? stream = freezed, + }) { + return _then( + _$ModelRequestDataImpl( + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + model: null == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as String, + apiKey: null == apiKey + ? _value.apiKey + : apiKey // ignore: cast_nullable_to_non_nullable + as String, + systemPrompt: null == systemPrompt + ? _value.systemPrompt + : systemPrompt // ignore: cast_nullable_to_non_nullable + as String, + userPrompt: null == userPrompt + ? _value.userPrompt + : userPrompt // ignore: cast_nullable_to_non_nullable + as String, + modelConfigs: null == modelConfigs + ? _value._modelConfigs + : modelConfigs // ignore: cast_nullable_to_non_nullable + as List, + stream: freezed == stream + ? _value.stream + : stream // ignore: cast_nullable_to_non_nullable + as bool?, + ), + ); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, anyMap: true) +class _$ModelRequestDataImpl extends _ModelRequestData { + const _$ModelRequestDataImpl({ + this.url = "", + this.model = "", + this.apiKey = "", + @JsonKey(name: "system_prompt") this.systemPrompt = "", + @JsonKey(name: "user_prompt") this.userPrompt = "", + @JsonKey(name: "model_configs") + final List modelConfigs = const [], + this.stream = null, + }) : _modelConfigs = modelConfigs, + super._(); + + factory _$ModelRequestDataImpl.fromJson(Map json) => + _$$ModelRequestDataImplFromJson(json); + + @override + @JsonKey() + final String url; + @override + @JsonKey() + final String model; + @override + @JsonKey() + final String apiKey; + @override + @JsonKey(name: "system_prompt") + final String systemPrompt; + @override + @JsonKey(name: "user_prompt") + final String userPrompt; + final List _modelConfigs; + @override + @JsonKey(name: "model_configs") + List get modelConfigs { + if (_modelConfigs is EqualUnmodifiableListView) return _modelConfigs; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_modelConfigs); + } + + @override + @JsonKey() + final bool? stream; + + @override + String toString() { + return 'ModelRequestData(url: $url, model: $model, apiKey: $apiKey, systemPrompt: $systemPrompt, userPrompt: $userPrompt, modelConfigs: $modelConfigs, stream: $stream)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ModelRequestDataImpl && + (identical(other.url, url) || other.url == url) && + (identical(other.model, model) || other.model == model) && + (identical(other.apiKey, apiKey) || other.apiKey == apiKey) && + (identical(other.systemPrompt, systemPrompt) || + other.systemPrompt == systemPrompt) && + (identical(other.userPrompt, userPrompt) || + other.userPrompt == userPrompt) && + const DeepCollectionEquality().equals( + other._modelConfigs, + _modelConfigs, + ) && + (identical(other.stream, stream) || other.stream == stream)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + url, + model, + apiKey, + systemPrompt, + userPrompt, + const DeepCollectionEquality().hash(_modelConfigs), + stream, + ); + + /// Create a copy of ModelRequestData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ModelRequestDataImplCopyWith<_$ModelRequestDataImpl> get copyWith => + __$$ModelRequestDataImplCopyWithImpl<_$ModelRequestDataImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ModelRequestDataImplToJson(this); + } +} + +abstract class _ModelRequestData extends ModelRequestData { + const factory _ModelRequestData({ + final String url, + final String model, + final String apiKey, + @JsonKey(name: "system_prompt") final String systemPrompt, + @JsonKey(name: "user_prompt") final String userPrompt, + @JsonKey(name: "model_configs") final List modelConfigs, + final bool? stream, + }) = _$ModelRequestDataImpl; + const _ModelRequestData._() : super._(); + + factory _ModelRequestData.fromJson(Map json) = + _$ModelRequestDataImpl.fromJson; + + @override + String get url; + @override + String get model; + @override + String get apiKey; + @override + @JsonKey(name: "system_prompt") + String get systemPrompt; + @override + @JsonKey(name: "user_prompt") + String get userPrompt; + @override + @JsonKey(name: "model_configs") + List get modelConfigs; + @override + bool? get stream; + + /// Create a copy of ModelRequestData + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ModelRequestDataImplCopyWith<_$ModelRequestDataImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/genai/lib/models/model_request_data.g.dart b/packages/genai/lib/models/model_request_data.g.dart new file mode 100644 index 00000000..8522874a --- /dev/null +++ b/packages/genai/lib/models/model_request_data.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'model_request_data.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ModelRequestDataImpl _$$ModelRequestDataImplFromJson(Map json) => + _$ModelRequestDataImpl( + url: json['url'] as String? ?? "", + model: json['model'] as String? ?? "", + apiKey: json['apiKey'] as String? ?? "", + systemPrompt: json['system_prompt'] as String? ?? "", + userPrompt: json['user_prompt'] as String? ?? "", + modelConfigs: + (json['model_configs'] as List?) + ?.map((e) => ModelConfig.fromJson(e as Map)) + .toList() ?? + const [], + stream: json['stream'] as bool? ?? null, + ); + +Map _$$ModelRequestDataImplToJson( + _$ModelRequestDataImpl instance, +) => { + 'url': instance.url, + 'model': instance.model, + 'apiKey': instance.apiKey, + 'system_prompt': instance.systemPrompt, + 'user_prompt': instance.userPrompt, + 'model_configs': instance.modelConfigs.map((e) => e.toJson()).toList(), + 'stream': instance.stream, +}; diff --git a/packages/genai/lib/models/models.dart b/packages/genai/lib/models/models.dart new file mode 100644 index 00000000..69ecfe07 --- /dev/null +++ b/packages/genai/lib/models/models.dart @@ -0,0 +1,7 @@ +export 'ai_request_model.dart'; +export 'available_models.dart'; +export 'model_config_value.dart'; +export 'model_config.dart'; +export 'model_provider.dart'; +export 'model_request_data.dart'; +export 'models_data.g.dart'; diff --git a/packages/genai/lib/models/models_data.g.dart b/packages/genai/lib/models/models_data.g.dart new file mode 100644 index 00000000..df125b5f --- /dev/null +++ b/packages/genai/lib/models/models_data.g.dart @@ -0,0 +1,2 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +const kModelsData = {"version":1.0,"model_providers":[{"provider_id":"ollama","provider_name":"Ollama","source_url":null,"models":[{"id":"","name":"Custom"}]},{"provider_id":"openai","provider_name":"OpenAI","source_url":"https://platform.openai.com/docs/models","models":[{"id":"gpt-5","name":"GPT-5"},{"id":"gpt-5-mini","name":"GPT-5 mini"},{"id":"gpt-5-nano","name":"GPT-5 Nano"},{"id":"gpt-4.1","name":"GPT-4.1"},{"id":"gpt-oss-120b","name":"gpt-oss-120b"},{"id":"gpt-oss-20b","name":"gpt-oss-20b"},{"id":"o3-pro","name":"o3-pro"},{"id":"o3","name":"o3"},{"id":"o4-mini","name":"o4-mini"},{"id":"gpt-4o","name":"GPT-4o"},{"id":"gpt-4","name":"GPT-4"},{"id":"gpt-4o-mini","name":"GPT-4o Mini"},{"id":"","name":"Other"}]},{"provider_id":"anthropic","provider_name":"Anthropic","source_url":"https://docs.anthropic.com/en/docs/about-claude/models/overview","models":[{"id":"claude-opus-4-1","name":"Claude Opus 4.1"},{"id":"claude-opus-4-0","name":"Claude Opus 4"},{"id":"claude-sonnet-4-0","name":"Claude Sonnet 4"},{"id":"claude-3-7-sonnet-latest","name":"Claude Sonnet 3.7"},{"id":"claude-3-5-sonnet-latest","name":"Claude Sonnet 3.5"},{"id":"claude-3-5-haiku-latest","name":"Claude Haiku 3.5"},{"id":"","name":"Other"}]},{"provider_id":"gemini","provider_name":"Gemini","source_url":"https://ai.google.dev/gemini-api/docs/models","models":[{"id":"gemini-2.5-pro","name":"Gemini 2.5 Pro"},{"id":"gemini-2.5-flash","name":"Gemini 2.5 Flash"},{"id":"gemini-2.5-flash-lite","name":"Gemini 2.5 Flash-Lite"},{"id":"gemini-2.0-flash","name":"Gemini 2.0 Flash"},{"id":"gemini-2.0-flash-lite","name":"Gemini 2.0 Flash-Lite"},{"id":"","name":"Other"}]},{"provider_id":"azureopenai","provider_name":"Azure OpenAI","source_url":null,"models":[{"id":"","name":"Custom"}]}]}; diff --git a/packages/genai/lib/providers/anthropic.dart b/packages/genai/lib/providers/anthropic.dart deleted file mode 100644 index c91fc4cc..00000000 --- a/packages/genai/lib/providers/anthropic.dart +++ /dev/null @@ -1,76 +0,0 @@ -import '../llm_config.dart'; -import '../llm_input_payload.dart'; -import '../llm_model.dart'; -import '../llm_request.dart'; - -class AnthropicModelController extends ModelController { - static final instance = AnthropicModelController(); - @override - LLMInputPayload get inputPayload => LLMInputPayload( - endpoint: 'https://api.anthropic.com/v1/messages', - credential: '', - systemPrompt: '', - userPrompt: '', - configMap: { - LLMConfigName.temperature.name: - defaultLLMConfigurations[LLMConfigName.temperature]!, - LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, - LLMConfigName.stream.name: - defaultLLMConfigurations[LLMConfigName.stream]!, - }, - ).clone(); - - @override - LLMRequestDetails createRequest( - LLMModel model, - LLMInputPayload inputPayload, { - bool stream = false, - }) { - return LLMRequestDetails( - endpoint: inputPayload.endpoint, - headers: { - 'anthropic-version': '2023-06-01', - 'Authorization': 'Bearer ${inputPayload.credential}', - }, - method: 'POST', - body: { - "model": model.identifier, - if (stream) ...{'stream': true}, - "messages": [ - {"role": "system", "content": inputPayload.systemPrompt}, - {"role": "user", "content": inputPayload.userPrompt}, - ], - "temperature": - inputPayload - .configMap[LLMConfigName.temperature.name] - ?.configValue - .value - ?.$2 ?? - 0.5, - "top_p": - inputPayload - .configMap[LLMConfigName.top_p.name] - ?.configValue - .value - ?.$2 ?? - 0.95, - if (inputPayload.configMap[LLMConfigName.max_tokens.name] != null) ...{ - "max_tokens": inputPayload - .configMap[LLMConfigName.max_tokens.name]! - .configValue - .value, - }, - }, - ); - } - - @override - String? outputFormatter(Map x) { - return x['content']?[0]['text']; - } - - @override - String? streamOutputFormatter(Map x) { - return x['text']; - } -} diff --git a/packages/genai/lib/providers/azureopenai.dart b/packages/genai/lib/providers/azureopenai.dart deleted file mode 100644 index ff2656a4..00000000 --- a/packages/genai/lib/providers/azureopenai.dart +++ /dev/null @@ -1,79 +0,0 @@ -import '../llm_config.dart'; -import '../llm_input_payload.dart'; -import '../llm_model.dart'; -import '../llm_request.dart'; - -class AzureOpenAIModelController extends ModelController { - static final instance = AzureOpenAIModelController(); - @override - LLMInputPayload get inputPayload => LLMInputPayload( - endpoint: '', //TO BE FILLED BY USER - credential: '', - systemPrompt: '', - userPrompt: '', - configMap: { - LLMConfigName.temperature.name: - defaultLLMConfigurations[LLMConfigName.temperature]!, - LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, - LLMConfigName.stream.name: - defaultLLMConfigurations[LLMConfigName.stream]!, - }, - ).clone(); - - @override - LLMRequestDetails createRequest( - LLMModel model, - LLMInputPayload inputPayload, { - bool stream = false, - }) { - if (inputPayload.endpoint.isEmpty) { - throw Exception('MODEL ENDPOINT IS EMPTY'); - } - return LLMRequestDetails( - endpoint: inputPayload.endpoint, - headers: {'api-key': inputPayload.credential}, - method: 'POST', - body: { - if (stream) ...{'stream': true}, - "messages": [ - {"role": "system", "content": inputPayload.systemPrompt}, - if (inputPayload.userPrompt.isNotEmpty) ...{ - {"role": "user", "content": inputPayload.userPrompt}, - } else ...{ - {"role": "user", "content": "Generate"}, - }, - ], - "temperature": - inputPayload - .configMap[LLMConfigName.temperature.name] - ?.configValue - .value - ?.$2 ?? - 0.5, - "top_p": - inputPayload - .configMap[LLMConfigName.top_p.name] - ?.configValue - .value - ?.$2 ?? - 0.95, - if (inputPayload.configMap[LLMConfigName.max_tokens.name] != null) ...{ - "max_tokens": inputPayload - .configMap[LLMConfigName.max_tokens.name]! - .configValue - .value, - }, - }, - ); - } - - @override - String? outputFormatter(Map x) { - return x["choices"]?[0]["message"]?["content"]?.trim(); - } - - @override - String? streamOutputFormatter(Map x) { - return x["choices"]?[0]["delta"]?["content"]; - } -} diff --git a/packages/genai/lib/providers/gemini.dart b/packages/genai/lib/providers/gemini.dart deleted file mode 100644 index 65e2843d..00000000 --- a/packages/genai/lib/providers/gemini.dart +++ /dev/null @@ -1,95 +0,0 @@ -import '../llm_config.dart'; -import '../llm_input_payload.dart'; -import '../llm_model.dart'; -import '../llm_request.dart'; - -class GeminiModelController extends ModelController { - static final instance = GeminiModelController(); - @override - LLMInputPayload get inputPayload => LLMInputPayload( - endpoint: 'https://generativelanguage.googleapis.com/v1beta/models', - credential: '', - systemPrompt: '', - userPrompt: '', - configMap: { - //TODO: CHANGES TO THESE DO NOT APPLY TO OLDER REQUESTS!!!!!! - LLMConfigName.temperature.name: - defaultLLMConfigurations[LLMConfigName.temperature]!, - LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, - LLMConfigName.stream.name: - defaultLLMConfigurations[LLMConfigName.stream]!, - }, - ).clone(); - - @override - LLMRequestDetails createRequest( - LLMModel model, - LLMInputPayload inputPayload, { - bool stream = false, - }) { - String endpoint = inputPayload.endpoint; - endpoint = - "$endpoint/${model.identifier}:generateContent?key=${inputPayload.credential}"; - if (stream) { - endpoint = endpoint.replaceAll( - 'generateContent?', - 'streamGenerateContent?alt=sse&', - ); - } - return LLMRequestDetails( - endpoint: endpoint, - headers: {}, - method: 'POST', - body: { - "model": model.identifier, - "contents": [ - { - "role": "user", - "parts": [ - {"text": inputPayload.userPrompt}, - ], - }, - ], - "systemInstruction": { - "role": "system", - "parts": [ - {"text": inputPayload.systemPrompt}, - ], - }, - "generationConfig": { - "temperature": - inputPayload - .configMap[LLMConfigName.temperature.name] - ?.configValue - .value - ?.$2 ?? - 0.5, - "topP": - inputPayload - .configMap[LLMConfigName.top_p.name] - ?.configValue - .value - ?.$2 ?? - 0.95, - if (inputPayload.configMap[LLMConfigName.max_tokens.name] != - null) ...{ - "maxOutputTokens": inputPayload - .configMap[LLMConfigName.max_tokens.name]! - .configValue - .value, - }, - }, - }, - ); - } - - @override - String? outputFormatter(Map x) { - return x['candidates']?[0]?['content']?['parts']?[0]?['text']; - } - - @override - String? streamOutputFormatter(Map x) { - return x['candidates']?[0]?['content']?['parts']?[0]?['text']; - } -} diff --git a/packages/genai/lib/providers/ollama.dart b/packages/genai/lib/providers/ollama.dart deleted file mode 100644 index 44201acd..00000000 --- a/packages/genai/lib/providers/ollama.dart +++ /dev/null @@ -1,74 +0,0 @@ -import '../llm_config.dart'; -import '../llm_input_payload.dart'; -import '../llm_model.dart'; -import '../llm_request.dart'; - -class OllamaModelController extends ModelController { - static final instance = OllamaModelController(); - - @override - LLMInputPayload get inputPayload => LLMInputPayload( - endpoint: 'http://localhost:11434/v1/chat/completions', - credential: '', - systemPrompt: '', - userPrompt: '', - configMap: { - LLMConfigName.temperature.name: - defaultLLMConfigurations[LLMConfigName.temperature]!, - LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, - LLMConfigName.stream.name: - defaultLLMConfigurations[LLMConfigName.stream]!, - }, - ).clone(); - - @override - LLMRequestDetails createRequest( - LLMModel model, - LLMInputPayload inputPayload, { - bool stream = false, - }) { - return LLMRequestDetails( - endpoint: inputPayload.endpoint, - headers: {}, - method: 'POST', - body: { - "model": model.identifier, - if (stream) ...{'stream': true}, - "messages": [ - {"role": "system", "content": inputPayload.systemPrompt}, - {"role": "user", "content": inputPayload.userPrompt}, - ], - "temperature": - inputPayload - .configMap[LLMConfigName.temperature.name] - ?.configValue - .value - ?.$2 ?? - 0.5, - "top_p": - inputPayload - .configMap[LLMConfigName.top_p.name] - ?.configValue - .value - ?.$2 ?? - 0.95, - if (inputPayload.configMap[LLMConfigName.max_tokens.name] != null) ...{ - "max_tokens": inputPayload - .configMap[LLMConfigName.max_tokens.name]! - .configValue - .value, - }, - }, - ); - } - - @override - String? outputFormatter(Map x) { - return x['choices']?[0]['message']?['content']; - } - - @override - String? streamOutputFormatter(Map x) { - return x['choices']?[0]['delta']?['content']; - } -} diff --git a/packages/genai/lib/providers/openai.dart b/packages/genai/lib/providers/openai.dart deleted file mode 100644 index 8ad3765e..00000000 --- a/packages/genai/lib/providers/openai.dart +++ /dev/null @@ -1,78 +0,0 @@ -import '../llm_config.dart'; -import '../llm_input_payload.dart'; -import '../llm_model.dart'; -import '../llm_request.dart'; - -class OpenAIModelController extends ModelController { - static final instance = OpenAIModelController(); - - @override - LLMInputPayload get inputPayload => LLMInputPayload( - endpoint: 'https://api.openai.com/v1/chat/completions', - credential: '', - systemPrompt: '', - userPrompt: '', - configMap: { - LLMConfigName.temperature.name: - defaultLLMConfigurations[LLMConfigName.temperature]!, - LLMConfigName.top_p.name: defaultLLMConfigurations[LLMConfigName.top_p]!, - LLMConfigName.stream.name: - defaultLLMConfigurations[LLMConfigName.stream]!, - }, - ).clone(); - - @override - LLMRequestDetails createRequest( - LLMModel model, - LLMInputPayload inputPayload, { - bool stream = false, - }) { - return LLMRequestDetails( - endpoint: inputPayload.endpoint, - headers: {'Authorization': "Bearer ${inputPayload.credential}"}, - method: 'POST', - body: { - 'model': model.identifier, - if (stream) ...{'stream': true}, - "messages": [ - {"role": "system", "content": inputPayload.systemPrompt}, - if (inputPayload.userPrompt.isNotEmpty) ...{ - {"role": "user", "content": inputPayload.userPrompt}, - } else ...{ - {"role": "user", "content": "Generate"}, - }, - ], - "temperature": - inputPayload - .configMap[LLMConfigName.temperature.name] - ?.configValue - .value - ?.$2 ?? - 0.5, - "top_p": - inputPayload - .configMap[LLMConfigName.top_p.name] - ?.configValue - .value - ?.$2 ?? - 0.95, - if (inputPayload.configMap[LLMConfigName.max_tokens.name] != null) ...{ - "max_tokens": inputPayload - .configMap[LLMConfigName.max_tokens.name]! - .configValue - .value, - }, - }, - ); - } - - @override - String? outputFormatter(Map x) { - return x["choices"]?[0]["message"]?["content"]?.trim(); - } - - @override - String? streamOutputFormatter(Map x) { - return x["choices"]?[0]["delta"]?["content"]; - } -} diff --git a/packages/genai/lib/utils/ai_request_utils.dart b/packages/genai/lib/utils/ai_request_utils.dart new file mode 100644 index 00000000..33b27bd3 --- /dev/null +++ b/packages/genai/lib/utils/ai_request_utils.dart @@ -0,0 +1,149 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:better_networking/better_networking.dart'; +import 'package:flutter/foundation.dart'; +import 'package:nanoid/nanoid.dart'; +import '../models/models.dart'; + +Future executeGenAIRequest(AIRequestModel? aiRequestModel) async { + final httpRequestModel = aiRequestModel?.httpRequestModel; + if (httpRequestModel == null) { + debugPrint("executeGenAIRequest -> httpRequestModel is null"); + return null; + } + final (response, _, _) = await sendHttpRequest( + nanoid(), + APIType.rest, + httpRequestModel, + ); + if (response == null) return null; + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return aiRequestModel?.getFormattedOutput(data); + } else { + debugPrint('LLM_EXCEPTION: ${response.statusCode}\n${response.body}'); + return null; + } +} + +Future> streamGenAIRequest( + AIRequestModel? aiRequestModel, +) async { + final httpRequestModel = aiRequestModel?.httpRequestModel; + final streamController = StreamController(); + if (httpRequestModel == null) { + debugPrint("streamGenAIRequest -> httpRequestModel is null"); + } else { + final httpStream = await streamHttpRequest( + nanoid(), + APIType.rest, + httpRequestModel, + ); + + final subscription = httpStream.listen( + (dat) { + if (dat == null) { + streamController.addError('STREAMING ERROR: NULL DATA'); + return; + } + + final chunk = dat.$2; + final error = dat.$4; + + if (chunk == null) { + streamController.addError(error ?? 'NULL ERROR'); + return; + } + + final ans = chunk.body; + + final lines = ans.split('\n'); + for (final line in lines) { + if (!line.startsWith('data: ') || line.contains('[DONE]')) continue; + final jsonStr = line.substring(6).trim(); + try { + final jsonData = jsonDecode(jsonStr); + final formattedOutput = aiRequestModel?.getFormattedStreamOutput( + jsonData, + ); + streamController.sink.add(formattedOutput); + } catch (e) { + debugPrint( + '⚠️ JSON decode error in SSE: $e\nSending as Regular Text', + ); + streamController.sink.add(jsonStr); + } + } + }, + onError: (error) { + streamController.addError('STREAM ERROR: $error'); + streamController.close(); + }, + onDone: () { + streamController.close(); + }, + cancelOnError: true, + ); + streamController.onCancel = () async { + await subscription.cancel(); + }; + } + return streamController.stream; +} + +Future callGenerativeModel( + AIRequestModel? aiRequestModel, { + required Function(String?) onAnswer, + required Function(dynamic) onError, +}) async { + final modelRequestData = aiRequestModel?.modelRequestData; + if (modelRequestData != null) { + try { + if (modelRequestData.stream ?? false) { + final answerStream = await streamGenAIRequest(aiRequestModel); + processGenAIStreamOutput(answerStream, (w) { + onAnswer('$w '); + }, onError); + } else { + final answer = await executeGenAIRequest(aiRequestModel); + onAnswer(answer); + } + } catch (e) { + onError(e); + } + } +} + +void processGenAIStreamOutput( + Stream stream, + Function(String) onWord, + Function(dynamic) onError, +) { + String buffer = ''; + stream.listen( + (chunk) { + if (chunk == null || chunk.isEmpty) return; + buffer += chunk; + // Split on spaces but preserve last partial word + final parts = buffer.split(RegExp(r'\s+')); + if (parts.length > 1) { + // Keep the last part in buffer (it may be incomplete) + buffer = parts.removeLast(); + for (final word in parts) { + if (word.trim().isNotEmpty) { + onWord(word); + } + } + } + }, + onDone: () { + // Print any remaining word when stream is finished + if (buffer.trim().isNotEmpty) { + onWord(buffer); + } + }, + onError: (e) { + onError(e); + }, + ); +} diff --git a/packages/genai/lib/utils/model_manager.dart b/packages/genai/lib/utils/model_manager.dart new file mode 100644 index 00000000..a11518c2 --- /dev/null +++ b/packages/genai/lib/utils/model_manager.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; +import 'package:better_networking/better_networking.dart'; +import 'package:flutter/foundation.dart'; +import '../consts.dart'; +import '../interface/interface.dart'; +import '../models/models.dart'; + +class ModelManager { + static Future fetchModelsFromRemote({ + String? remoteURL, + }) async { + try { + final (resp, _, _) = await sendHttpRequest( + 'FETCH_MODELS', + APIType.rest, + HttpRequestModel( + url: remoteURL ?? kModelRemoteUrl, + method: HTTPVerb.get, + ), + ); + if (resp == null) { + debugPrint('fetchModelsFromRemote -> resp == null'); + } else { + var remoteModels = availableModelsFromJson(resp.body); + return remoteModels; + } + } catch (e) { + debugPrint('fetchModelsFromRemote -> ${e.toString()}'); + } + return null; + } + + static Future fetchAvailableModels({ + String? ollamaUrl, + }) async { + try { + final oM = await fetchInstalledOllamaModels(ollamaUrl: ollamaUrl); + if (oM != null) { + List l = []; + for (var prov in kAvailableModels.modelProviders) { + if (prov.providerId == ModelAPIProvider.ollama) { + l.add( + prov.copyWith( + providerId: prov.providerId, + providerName: prov.providerName, + sourceUrl: prov.sourceUrl, + models: oM, + ), + ); + } else { + l.add(prov); + } + } + return kAvailableModels.copyWith( + version: kAvailableModels.version, + modelProviders: l, + ); + } + } catch (e) { + debugPrint('fetchAvailableModels -> ${e.toString()}'); + } + return kAvailableModels; + } + + static Future?> fetchInstalledOllamaModels({ + String? ollamaUrl, + }) async { + // All available models + // final url = "${ollamaUrl ?? kBaseOllamaUrl}/api/tags"; + // All loaded models + final url = "${ollamaUrl ?? kBaseOllamaUrl}/api/ps"; + + try { + final (resp, _, msg) = await sendHttpRequest( + 'OLLAMA_FETCH', + APIType.rest, + HttpRequestModel(url: url, method: HTTPVerb.get), + noSSL: true, + ); + // debugPrint("fetchInstalledOllamaModels -> $url -> ${resp?.body} -> $msg"); + if (resp == null) { + return null; + } + final output = jsonDecode(resp.body); + final models = output['models']; + if (models == null) return []; + List ollamaModels = []; + for (final m in models) { + ollamaModels.add(Model(id: m['model'], name: m['name'])); + } + return ollamaModels; + } catch (e) { + debugPrint('fetchInstalledOllamaModels -> ${e.toString()}'); + return null; + } + } +} diff --git a/packages/genai/lib/utils/utils.dart b/packages/genai/lib/utils/utils.dart new file mode 100644 index 00000000..e2ea0399 --- /dev/null +++ b/packages/genai/lib/utils/utils.dart @@ -0,0 +1,2 @@ +export 'ai_request_utils.dart'; +export 'model_manager.dart'; diff --git a/packages/genai/lib/widgets/ai_config_bool.dart b/packages/genai/lib/widgets/ai_config_bool.dart new file mode 100644 index 00000000..6d9f8d18 --- /dev/null +++ b/packages/genai/lib/widgets/ai_config_bool.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import '../models/models.dart'; + +class AIConfigBool extends StatelessWidget { + final ModelConfig configuration; + final Function(ModelConfig) onConfigUpdated; + final bool readonly; + const AIConfigBool({ + super.key, + required this.configuration, + required this.onConfigUpdated, + this.readonly = false, + }); + + @override + Widget build(BuildContext context) { + return Switch( + value: configuration.value.value as bool, + onChanged: (x) { + if (readonly) return; + configuration.value.value = x; + onConfigUpdated(configuration); + }, + ); + } +} diff --git a/packages/genai/lib/widgets/ai_config_field.dart b/packages/genai/lib/widgets/ai_config_field.dart new file mode 100644 index 00000000..ebae9384 --- /dev/null +++ b/packages/genai/lib/widgets/ai_config_field.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import '../models/models.dart'; + +class AIConfigField extends StatelessWidget { + final bool numeric; + final ModelConfig configuration; + final Function(ModelConfig) onConfigUpdated; + final bool readonly; + const AIConfigField({ + super.key, + this.numeric = false, + required this.configuration, + required this.onConfigUpdated, + this.readonly = false, + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + initialValue: configuration.value.value.toString(), + onChanged: (x) { + if (readonly) return; + if (numeric) { + if (x.isEmpty) x = '0'; + if (num.tryParse(x) == null) return; + configuration.value.value = num.parse(x); + } else { + configuration.value.value = x; + } + onConfigUpdated(configuration); + }, + ); + } +} diff --git a/packages/genai/lib/widgets/ai_config_slider.dart b/packages/genai/lib/widgets/ai_config_slider.dart new file mode 100644 index 00000000..36b48774 --- /dev/null +++ b/packages/genai/lib/widgets/ai_config_slider.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import '../models/models.dart'; + +class AIConfigSlider extends StatelessWidget { + final ModelConfig configuration; + final Function(ModelConfig) onSliderUpdated; + final bool readonly; + const AIConfigSlider({ + super.key, + required this.configuration, + required this.onSliderUpdated, + this.readonly = false, + }); + + @override + Widget build(BuildContext context) { + final val = configuration.value.value as (double, double, double); + return Row( + children: [ + Expanded( + child: Slider( + min: val.$1, + value: val.$2, + max: val.$3, + onChanged: (x) { + if (readonly) return; + configuration.value.value = (val.$1, x, val.$3); + onSliderUpdated(configuration); + }, + ), + ), + Text(val.$2.toStringAsFixed(2)), + ], + ); + } +} diff --git a/packages/genai/lib/widgets/ai_config_widgets.dart b/packages/genai/lib/widgets/ai_config_widgets.dart deleted file mode 100644 index 957c5c1f..00000000 --- a/packages/genai/lib/widgets/ai_config_widgets.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:genai/llm_config.dart'; - -class SliderAIConfig extends StatelessWidget { - final LLMModelConfiguration configuration; - final Function(LLMModelConfiguration) onSliderUpdated; - final bool readonly; - const SliderAIConfig({ - super.key, - required this.configuration, - required this.onSliderUpdated, - this.readonly = false, - }); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: Slider( - min: (configuration.configValue.value as (double, double, double)) - .$1, - value: (configuration.configValue.value as (double, double, double)) - .$2, - max: (configuration.configValue.value as (double, double, double)) - .$3, - onChanged: (x) { - if (readonly) return; - final z = - configuration.configValue.value as (double, double, double); - configuration.configValue.value = (z.$1, x, z.$3); - onSliderUpdated(configuration); - }, - ), - ), - Text( - (configuration.configValue.value as (double, double, double)).$2 - .toStringAsFixed(2), - ), - ], - ); - } -} - -class WritableAIConfig extends StatelessWidget { - final bool numeric; - final LLMModelConfiguration configuration; - final Function(LLMModelConfiguration) onConfigUpdated; - final bool readonly; - const WritableAIConfig({ - super.key, - this.numeric = false, - required this.configuration, - required this.onConfigUpdated, - this.readonly = false, - }); - - @override - Widget build(BuildContext context) { - return TextFormField( - initialValue: configuration.configValue.value.toString(), - onChanged: (x) { - if (readonly) return; - if (numeric) { - if (x.isEmpty) x = '0'; - if (num.tryParse(x) == null) return; - configuration.configValue.value = num.parse(x); - } else { - configuration.configValue.value = x; - } - onConfigUpdated(configuration); - }, - ); - } -} - -class BooleanAIConfig extends StatelessWidget { - final LLMModelConfiguration configuration; - final Function(LLMModelConfiguration) onConfigUpdated; - final bool readonly; - const BooleanAIConfig({ - super.key, - required this.configuration, - required this.onConfigUpdated, - this.readonly = false, - }); - - @override - Widget build(BuildContext context) { - return Switch( - value: configuration.configValue.value as bool, - onChanged: (x) { - if (readonly) return; - configuration.configValue.value = x; - onConfigUpdated(configuration); - }, - ); - } -} diff --git a/packages/genai/lib/widgets/widgets.dart b/packages/genai/lib/widgets/widgets.dart new file mode 100644 index 00000000..15f9a10c --- /dev/null +++ b/packages/genai/lib/widgets/widgets.dart @@ -0,0 +1,3 @@ +export 'ai_config_bool.dart'; +export 'ai_config_field.dart'; +export 'ai_config_slider.dart'; diff --git a/packages/genai/models.json b/packages/genai/models.json index b8156b72..5fcb9ad0 100644 --- a/packages/genai/models.json +++ b/packages/genai/models.json @@ -1,98 +1,152 @@ { - "openai": [ - [ - "gpt-4o", - "GPT-4o" - ], - [ - "gpt-4", - "GPT-4" - ], - [ - "gpt-4o-mini", - "GPT-4o Mini" - ], - [ - "gpt-4-turbo", - "GPT-4 Turbo" - ], - [ - "gpt-4.1", - "GPT-4.1" - ], - [ - "gpt-4.1-mini", - "GPT-4.1 Mini" - ], - [ - "gpt-4.1-nano", - "GPT-4.1 Nano" - ], - [ - "o1", - "o1" - ], - [ - "o3", - "o3" - ], - [ - "o3-mini", - "o3 Mini" - ], - [ - "gpt-3.5-turbo", - "GPT-3.5 Turbo" - ] - ], - "anthropic": [ - [ - "claude-3-opus-latest", - "Claude 3 Opus" - ], - [ - "claude-3-sonnet-latest", - "Claude 3 Sonnet" - ], - [ - "claude-3-haiku-latest", - "Claude 3 Haiku" - ], - [ - "claude-3-5-haiku-latest", - "Claude 3.5 Haiku" - ], - [ - "claude-3-5-sonnet-latest", - "Claude 3.5 Sonnet" - ] - ], - "gemini": [ - [ - "gemini-1.5-pro", - "Gemini 1.5 Pro" - ], - [ - "gemini-1.5-flash-8b", - "Gemini 1.5 Flash 8B" - ], - [ - "gemini-2.0-flash", - "Gemini 2.0 Flash" - ], - [ - "gemini-2.0-flash-lite", - "Gemini 2.0 Flash Lite" - ], - [ - "gemini-2.5-flash-preview-0520", - "Gemini 2.5 Flash Preview 0520" - ] - ], - "azureopenai": [ - [ - "custom", - "Custom" - ] - ] -} \ No newline at end of file + "version": 1.0, + "model_providers": [ + { + "provider_id": "ollama", + "provider_name": "Ollama", + "source_url": null, + "models": [ + { + "id": "", + "name": "Custom" + } + ] + }, + { + "provider_id": "openai", + "provider_name": "OpenAI", + "source_url": "https://platform.openai.com/docs/models", + "models": [ + { + "id": "gpt-5", + "name": "GPT-5" + }, + { + "id": "gpt-5-mini", + "name": "GPT-5 mini" + }, + { + "id": "gpt-5-nano", + "name": "GPT-5 Nano" + }, + { + "id": "gpt-4.1", + "name": "GPT-4.1" + }, + { + "id": "gpt-oss-120b", + "name": "gpt-oss-120b" + }, + { + "id": "gpt-oss-20b", + "name": "gpt-oss-20b" + }, + { + "id": "o3-pro", + "name": "o3-pro" + }, + { + "id": "o3", + "name": "o3" + }, + { + "id": "o4-mini", + "name": "o4-mini" + }, + { + "id": "gpt-4o", + "name": "GPT-4o" + }, + { + "id": "gpt-4", + "name": "GPT-4" + }, + { + "id": "gpt-4o-mini", + "name": "GPT-4o Mini" + }, + { + "id": "", + "name": "Other" + } + ] + }, + { + "provider_id": "anthropic", + "provider_name": "Anthropic", + "source_url": "https://docs.anthropic.com/en/docs/about-claude/models/overview", + "models": [ + { + "id": "claude-opus-4-1", + "name": "Claude Opus 4.1" + }, + { + "id": "claude-opus-4-0", + "name": "Claude Opus 4" + }, + { + "id": "claude-sonnet-4-0", + "name": "Claude Sonnet 4" + }, + { + "id": "claude-3-7-sonnet-latest", + "name": "Claude Sonnet 3.7" + }, + { + "id": "claude-3-5-sonnet-latest", + "name": "Claude Sonnet 3.5" + }, + { + "id": "claude-3-5-haiku-latest", + "name": "Claude Haiku 3.5" + }, + { + "id": "", + "name": "Other" + } + ] + }, + { + "provider_id": "gemini", + "provider_name": "Gemini", + "source_url": "https://ai.google.dev/gemini-api/docs/models", + "models": [ + { + "id": "gemini-2.5-pro", + "name": "Gemini 2.5 Pro" + }, + { + "id": "gemini-2.5-flash", + "name": "Gemini 2.5 Flash" + }, + { + "id": "gemini-2.5-flash-lite", + "name": "Gemini 2.5 Flash-Lite" + }, + { + "id": "gemini-2.0-flash", + "name": "Gemini 2.0 Flash" + }, + { + "id": "gemini-2.0-flash-lite", + "name": "Gemini 2.0 Flash-Lite" + }, + { + "id": "", + "name": "Other" + } + ] + }, + { + "provider_id": "azureopenai", + "provider_name": "Azure OpenAI", + "source_url": null, + "models": [ + { + "id": "", + "name": "Custom" + } + ] + } + ] +} diff --git a/packages/genai/pubspec.yaml b/packages/genai/pubspec.yaml index b5b6f696..13237382 100644 --- a/packages/genai/pubspec.yaml +++ b/packages/genai/pubspec.yaml @@ -11,9 +11,11 @@ environment: dependencies: flutter: sdk: flutter - shared_preferences: ^2.5.2 better_networking: path: ../better_networking + freezed_annotation: ^2.4.1 + json_annotation: ^4.9.0 + nanoid: ^1.0.0 dev_dependencies: flutter_test: @@ -22,5 +24,4 @@ dev_dependencies: flutter_lints: ^4.0.0 freezed: ^2.5.7 json_serializable: ^6.7.1 - -flutter: \ No newline at end of file + test: ^1.25.2 diff --git a/packages/genai/pubspec_overrides.yaml b/packages/genai/pubspec_overrides.yaml new file mode 100644 index 00000000..02df146a --- /dev/null +++ b/packages/genai/pubspec_overrides.yaml @@ -0,0 +1,6 @@ +# melos_managed_dependency_overrides: better_networking,seed +dependency_overrides: + better_networking: + path: ../better_networking + seed: + path: ../seed diff --git a/packages/genai/tool/json_to_dart.dart b/packages/genai/tool/json_to_dart.dart new file mode 100644 index 00000000..796252b4 --- /dev/null +++ b/packages/genai/tool/json_to_dart.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; +import 'dart:io'; + +void main() { + final inputFile = File('models.json'); + final outputFile = File('lib/models/models_data.g.dart'); + + final jsonData = jsonDecode(inputFile.readAsStringSync()); + final dartCode = + ''' +// GENERATED CODE - DO NOT MODIFY BY HAND +const kModelsData = ${jsonEncode(jsonData)}; +'''; + + outputFile.writeAsStringSync(dartCode); + print('✅ Generated data.g.dart from data.json'); +} diff --git a/packages/genai/tool/pre_publish.sh b/packages/genai/tool/pre_publish.sh new file mode 100644 index 00000000..e51fd07a --- /dev/null +++ b/packages/genai/tool/pre_publish.sh @@ -0,0 +1,4 @@ +#!/bin/bash +echo "🔄 Running pre-publish steps..." +dart run tool/json_to_dart.dart +echo "✅ Pre-publish steps completed."