-
Notifications
You must be signed in to change notification settings - Fork 352
[Flutter SDK] Add configuration validation for LLM, STT, and TTS (#450) #456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 9 commits
e98124d
15138c5
7154800
1f85521
40829dd
01d9b3c
616c961
418f015
115e330
8804c3d
875a9f7
86851fe
1fc42e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import 'package:runanywhere/core/protocols/component/component_configuration.dart'; | ||
| import 'package:runanywhere/core/types/model_types.dart'; | ||
| import 'package:runanywhere/foundation/error_types/sdk_error.dart'; | ||
|
|
||
| /// Configuration for the LLM component. | ||
| /// | ||
| /// Mirrors the validation contract used by the Swift and Kotlin SDKs so | ||
| /// invalid parameters fail in Dart before crossing the FFI boundary. | ||
| class LLMConfiguration implements ComponentConfiguration { | ||
| final String? modelId; | ||
| final InferenceFramework? preferredFramework; | ||
| final int contextLength; | ||
| final double temperature; | ||
| final int maxTokens; | ||
| final String? systemPrompt; | ||
| final bool streamingEnabled; | ||
|
|
||
| const LLMConfiguration({ | ||
| this.modelId, | ||
| this.preferredFramework, | ||
| this.contextLength = 2048, | ||
| this.temperature = 0.7, | ||
| this.maxTokens = 100, | ||
| this.systemPrompt, | ||
| this.streamingEnabled = true, | ||
| }); | ||
|
|
||
| @override | ||
| void validate() { | ||
| if (contextLength <= 0 || contextLength > 32768) { | ||
| throw SDKError.validationFailed( | ||
| 'Context length must be between 1 and 32768', | ||
| ); | ||
| } | ||
|
|
||
| if (!temperature.isFinite || temperature < 0 || temperature > 2.0) { | ||
| throw SDKError.validationFailed( | ||
| 'Temperature must be between 0 and 2.0', | ||
| ); | ||
| } | ||
|
|
||
| if (maxTokens <= 0 || maxTokens > contextLength) { | ||
| throw SDKError.validationFailed( | ||
| 'Max tokens must be between 1 and context length', | ||
| ); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import 'package:runanywhere/core/protocols/component/component_configuration.dart'; | ||
| import 'package:runanywhere/core/types/model_types.dart'; | ||
| import 'package:runanywhere/foundation/error_types/sdk_error.dart'; | ||
|
|
||
| /// Configuration for the STT component. | ||
| /// | ||
| /// Mirrors the validation contract used by the Swift and Kotlin SDKs so | ||
| /// invalid parameters fail in Dart before crossing the FFI boundary. | ||
| class STTConfiguration implements ComponentConfiguration { | ||
| final String? modelId; | ||
| final InferenceFramework? preferredFramework; | ||
| final String language; | ||
| final int sampleRate; | ||
| final bool enablePunctuation; | ||
| final bool enableDiarization; | ||
| final List<String> vocabularyList; | ||
| final int maxAlternatives; | ||
| final bool enableTimestamps; | ||
|
|
||
| const STTConfiguration({ | ||
| this.modelId, | ||
| this.preferredFramework, | ||
| this.language = 'en-US', | ||
| this.sampleRate = 16000, | ||
| this.enablePunctuation = true, | ||
| this.enableDiarization = false, | ||
| this.vocabularyList = const <String>[], | ||
| this.maxAlternatives = 1, | ||
| this.enableTimestamps = true, | ||
| }); | ||
|
|
||
| @override | ||
| void validate() { | ||
| if (sampleRate <= 0 || sampleRate > 48000) { | ||
| throw SDKError.validationFailed( | ||
| 'Sample rate must be between 1 and 48000 Hz', | ||
| ); | ||
| } | ||
|
|
||
| if (maxAlternatives <= 0 || maxAlternatives > 10) { | ||
| throw SDKError.validationFailed( | ||
| 'Max alternatives must be between 1 and 10', | ||
| ); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,7 @@ import 'dart:ffi'; | |
| import 'dart:isolate'; // Keep for non-streaming generation | ||
|
|
||
| import 'package:ffi/ffi.dart'; | ||
|
|
||
| import 'package:runanywhere/features/llm/llm_configuration.dart'; | ||
| import 'package:runanywhere/foundation/logging/sdk_logger.dart'; | ||
| import 'package:runanywhere/native/ffi_types.dart'; | ||
| import 'package:runanywhere/native/platform_loader.dart'; | ||
|
|
@@ -241,6 +241,12 @@ class DartBridgeLLM { | |
| double temperature = 0.7, | ||
| String? systemPrompt, | ||
| }) async { | ||
| _validateGenerationParameters( | ||
| maxTokens: maxTokens, | ||
| temperature: temperature, | ||
| systemPrompt: systemPrompt, | ||
| ); | ||
|
|
||
| final handle = getHandle(); | ||
|
|
||
| if (!isLoaded) { | ||
|
|
@@ -284,6 +290,13 @@ class DartBridgeLLM { | |
| double temperature = 0.7, | ||
| String? systemPrompt, | ||
| }) { | ||
| _validateGenerationParameters( | ||
| maxTokens: maxTokens, | ||
| temperature: temperature, | ||
| systemPrompt: systemPrompt, | ||
| streamingEnabled: true, | ||
| ); | ||
|
|
||
| final handle = getHandle(); | ||
|
|
||
| if (!isLoaded) { | ||
|
|
@@ -367,6 +380,21 @@ class DartBridgeLLM { | |
| } | ||
| } | ||
|
|
||
| void _validateGenerationParameters({ | ||
| required int maxTokens, | ||
| required double temperature, | ||
| String? systemPrompt, | ||
| bool streamingEnabled = false, | ||
| }) { | ||
| LLMConfiguration( | ||
| contextLength: 32768, | ||
| maxTokens: maxTokens, | ||
| temperature: temperature, | ||
| systemPrompt: systemPrompt, | ||
| streamingEnabled: streamingEnabled, | ||
| ).validate(); | ||
| } | ||
|
Comment on lines
+401
to
+415
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded The Consider passing the actual model's context length here. Since Prompt To Fix With AIThis is a comment left during a code review.
Path: sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_llm.dart
Line: 383-396
Comment:
**Hardcoded `contextLength` defeats `maxTokens` validation**
The `contextLength` is hardcoded to `32768` (the maximum allowed), which means the `maxTokens <= contextLength` check in `LLMConfiguration.validate()` will never reject any value under 32768. A user could pass `maxTokens: 32768` even though the loaded model may have a much smaller context window (e.g. 2048 or 4096), sending an invalid value across the FFI boundary — exactly what this PR is trying to prevent.
Consider passing the actual model's context length here. Since `DartBridgeLLM` manages the C++ lifecycle, the real context length may be queryable from the native layer, or you could store it when the model is loaded and pass it through to validation.
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| // MARK: - Cleanup | ||
|
|
||
| /// Destroy the component and release resources. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1844,6 +1844,8 @@ class RunAnywhere { | |
| tokensPerSecond: tokensPerSecond, | ||
| structuredData: structuredData, | ||
| ); | ||
| } on SDKError { | ||
| rethrow; | ||
|
Comment on lines
+1856
to
+1857
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing parallel The More importantly, Prompt To Fix With AIThis is a comment left during a code review.
Path: sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart
Line: 1847-1848
Comment:
**Missing parallel `SDKError` rethrow in `generateStream`, `transcribe`, and `synthesize`**
The `on SDKError { rethrow; }` clause was added here for `generate`, but the sibling `generateStream` method (line ~1930) calls `DartBridge.llm.generateStream()` outside any try-catch. While this works because validation errors propagate naturally (they're thrown synchronously before the stream is returned), it creates an inconsistency in error handling patterns.
More importantly, `transcribe` (line 684) and `synthesize` (line 947) both have a generic `catch (e) { ... rethrow; }` which *does* preserve the `SDKError` via `rethrow`. This means the `generate` method was the only place actively swallowing `SDKError` (via `throw SDKError.generationFailed('$e')`). This fix is correct — just calling it out for completeness that only `generate` had this masking issue.
How can I resolve this? If you propose a fix, please make it concise. |
||
| } catch (e) { | ||
| // Track generation failure | ||
| TelemetryService.shared.trackError( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.