274 lines
7.8 KiB
Dart
274 lines
7.8 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:ollama_dart/ollama_dart.dart' as ollama;
|
|
|
|
import '../main.dart';
|
|
import 'clients.dart' as clients;
|
|
import 'preferences.dart';
|
|
|
|
/// Container class for model capabilities.
|
|
///
|
|
/// See more:
|
|
/// - https://github.com/ollama/ollama/blob/main/types/model/capability.go
|
|
class ModelCapability {
|
|
final String name;
|
|
|
|
ModelCapability(this.name);
|
|
|
|
static ModelCapability completion = ModelCapability("completion");
|
|
static ModelCapability tools = ModelCapability("tools");
|
|
static ModelCapability insertion = ModelCapability("insert");
|
|
static ModelCapability vision = ModelCapability("vision");
|
|
static ModelCapability embedding = ModelCapability("embedding");
|
|
static ModelCapability thinking = ModelCapability("thinking");
|
|
static ModelCapability image = ModelCapability("image");
|
|
static ModelCapability audio = ModelCapability("audio");
|
|
|
|
@override
|
|
operator ==(Object other) => other is ModelCapability && other.name == name;
|
|
@override
|
|
int get hashCode => name.hashCode;
|
|
|
|
IconData? get icon => switch (name) {
|
|
"completion" => Icons.text_fields,
|
|
"tools" => Icons.build,
|
|
"insert" => Icons.read_more,
|
|
"vision" => Icons.visibility_outlined,
|
|
"embedding" => Icons.link,
|
|
"thinking" => Icons.emoji_objects_outlined,
|
|
"image" => Icons.image_outlined,
|
|
"audio" => Icons.audiotrack_outlined,
|
|
_ => Icons.inventory_2_outlined,
|
|
};
|
|
}
|
|
|
|
class Model extends ChangeNotifier {
|
|
final String name;
|
|
|
|
List<TextSpan> nameColored(BuildContext context) =>
|
|
nameColoredStatic(name: name, context: context)!;
|
|
static List<TextSpan>? nameColoredStatic({
|
|
required String? name,
|
|
required BuildContext context,
|
|
}) {
|
|
if (name == null) return null;
|
|
final colorScheme = ColorScheme.of(context);
|
|
|
|
final parts = name.split(":");
|
|
assert(
|
|
parts.isNotEmpty && parts.length <= 2,
|
|
"Model name should be in format family:version, but got $name",
|
|
);
|
|
|
|
return [
|
|
TextSpan(text: parts.first),
|
|
if (parts.length == 2)
|
|
TextSpan(
|
|
text: ":${parts.last}",
|
|
style: TextStyle(color: colorScheme.outline),
|
|
),
|
|
];
|
|
}
|
|
|
|
DateTime _modifiedAt;
|
|
DateTime get modifiedAt => _modifiedAt;
|
|
|
|
int _size;
|
|
int get size => _size;
|
|
|
|
String _family;
|
|
String get family => _family;
|
|
|
|
Set<String> _families;
|
|
Set<String> get families => Set.unmodifiable(_families);
|
|
|
|
String _parameterSize;
|
|
String get parameterSize => _parameterSize;
|
|
|
|
Set<ModelCapability> _capabilities;
|
|
Set<ModelCapability> get capabilities => Set.unmodifiable(_capabilities);
|
|
|
|
Model._(
|
|
this.name, {
|
|
required this._modifiedAt,
|
|
required this._size,
|
|
required this._family,
|
|
this._families = const {},
|
|
required this._parameterSize,
|
|
}) : _capabilities = const {};
|
|
|
|
factory Model.fromApi({required ollama.ModelSummary model}) {
|
|
return Model._(
|
|
model.model!,
|
|
modifiedAt:
|
|
DateTime.tryParse(model.modifiedAt!)?.toLocal() ??
|
|
DateTime.fromMillisecondsSinceEpoch(0),
|
|
size: model.size ?? 0,
|
|
family: model.details!.family!,
|
|
families: model.details!.families?.toSet() ?? {},
|
|
parameterSize: model.details!.parameterSize!.replaceAll(".0", ""),
|
|
);
|
|
}
|
|
|
|
bool _didCallUpdateData = false;
|
|
bool get didCallUpdateData => _didCallUpdateData;
|
|
|
|
Future<void> updateData() async {
|
|
final data = await clients.ollamaClient.models.show(
|
|
request: ollama.ShowRequest(model: name),
|
|
);
|
|
|
|
// just in case, but should always be present
|
|
if (data.details != null) {
|
|
_family = data.details?["family"] ?? _family;
|
|
_families = data.details?["families"] != null
|
|
? (data.details!["families"] as List).map((e) => e.toString()).toSet()
|
|
: _families;
|
|
_parameterSize = data.details?["parameterSize"] ?? _parameterSize;
|
|
}
|
|
_capabilities = data.capabilities!.map(ModelCapability.new).toSet();
|
|
|
|
_didCallUpdateData = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> loadIntoMemory() async {
|
|
// unable to use [ollamaClient] here, because the library does not support
|
|
// sending a [GenerateChatCompletionRequest] without [messages], which is
|
|
// required for loading, otherwise a message will be generated
|
|
|
|
final headers = <String, String>{
|
|
"Content-Type": "application/json",
|
|
...Preferences.instance.hostHeaders,
|
|
};
|
|
final body = {"model": name, "keep_alive": Preferences.instance.keepAlive};
|
|
|
|
await clients.httpClient
|
|
.post(
|
|
Uri.parse("${clients.ollamaClient.config.baseUrl}/api/generate"),
|
|
headers: headers,
|
|
body: jsonEncode(body),
|
|
)
|
|
.timeout(TimeoutMultiplier.medium);
|
|
}
|
|
|
|
@override
|
|
operator ==(Object other) => other is Model && other.name == name;
|
|
@override
|
|
int get hashCode => name.hashCode;
|
|
}
|
|
|
|
class ModelManager extends ChangeNotifier {
|
|
static final ModelManager _instance = ModelManager._();
|
|
static ModelManager get instance => _instance;
|
|
|
|
bool _initialized = false;
|
|
bool get initialized => _initialized;
|
|
|
|
String? _currentModelName;
|
|
String? get currentModelName => _currentModelName;
|
|
set currentModelName(String? name) {
|
|
assert(!useModel, "Model if fixed to $fixedModel and cannot be changed.");
|
|
if (useModel) return;
|
|
|
|
if (name == _currentModelName) return;
|
|
_currentModelName = name;
|
|
Preferences.instance.model = name;
|
|
notifyListeners();
|
|
}
|
|
|
|
Model? get currentModel => _currentModelName == null || _models.isEmpty
|
|
? null
|
|
: _models.singleWhere((e) => e.name == _currentModelName);
|
|
set currentModel(Model? model) => currentModelName = model?.name;
|
|
|
|
final Set<Model> _models = {};
|
|
Set<Model> get models => Set.unmodifiable(_models);
|
|
|
|
ModelManager._()
|
|
: _currentModelName = useModel ? fixedModel : Preferences.instance.model;
|
|
|
|
Future<void> loadModels({bool fetchCapabilitiesInBackground = true}) async {
|
|
final data = await clients.ollamaClient.models.list().timeout(
|
|
TimeoutMultiplier.medium,
|
|
);
|
|
loadModelsRaw(
|
|
data: data,
|
|
fetchCapabilitiesInBackground: fetchCapabilitiesInBackground,
|
|
);
|
|
}
|
|
|
|
void loadModelsRaw({
|
|
required ollama.ListResponse data,
|
|
bool fetchCapabilitiesInBackground = true,
|
|
}) {
|
|
for (var model in _models) {
|
|
model.dispose();
|
|
}
|
|
_models.clear();
|
|
|
|
for (var model in data.models!) {
|
|
_models.add(Model.fromApi(model: model)..addListener(notifyListeners));
|
|
}
|
|
if (_currentModelName != null &&
|
|
!_models.any((e) => e.name == _currentModelName)) {
|
|
_currentModelName = null;
|
|
}
|
|
|
|
_initialized = true;
|
|
notifyListeners();
|
|
|
|
if (fetchCapabilitiesInBackground) {
|
|
compute((_) async {
|
|
for (var model in _instance.models) {
|
|
await model.updateData().catchError((_) {});
|
|
notifyListeners();
|
|
}
|
|
}, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
String bytesToHumanReadable(int bytes, {String? locale}) {
|
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
var value = bytes.toDouble();
|
|
var unitIndex = 0;
|
|
|
|
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
value /= 1024;
|
|
unitIndex++;
|
|
}
|
|
final nf = NumberFormat.decimalPattern(locale)
|
|
..maximumFractionDigits = 2
|
|
..minimumFractionDigits = 0;
|
|
|
|
final separator =
|
|
RegExp(r"(\s)")
|
|
.firstMatch(
|
|
NumberFormat.simpleCurrency(locale: locale, name: "USD").format(0),
|
|
)
|
|
?.group(1) ??
|
|
"";
|
|
|
|
return "${nf.format(value)}$separator${units[unitIndex]}";
|
|
}
|
|
|
|
extension FilterSupportedCapabilities on Iterable<ModelCapability> {
|
|
Iterable<ModelCapability> filterSupportedCapabilities() => where(
|
|
(capability) => switch (capability.name) {
|
|
"completion" => false,
|
|
"tools" => false,
|
|
"insert" => false,
|
|
"vision" => true,
|
|
"embedding" => false,
|
|
"thinking" => true,
|
|
"image" => false,
|
|
"audio" => false,
|
|
_ => false,
|
|
},
|
|
);
|
|
}
|