feat(model-avatar): Update model icon resolution for OpenWebUI
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
import '../models/model.dart';
|
import '../models/model.dart';
|
||||||
import '../services/api_service.dart';
|
import '../services/api_service.dart';
|
||||||
|
|
||||||
|
/// Extracts the profile image URL from a model's metadata.
|
||||||
|
///
|
||||||
|
/// Note: After OpenWebUI updates, the profile_image_url field is stripped from
|
||||||
|
/// the /api/models response. This function still checks for legacy data but
|
||||||
|
/// clients should use [buildModelAvatarUrl] to construct the proper endpoint URL.
|
||||||
String? deriveModelIcon(Model? model) {
|
String? deriveModelIcon(Model? model) {
|
||||||
if (model == null) return null;
|
if (model == null) return null;
|
||||||
|
|
||||||
@@ -47,6 +52,46 @@ String? deriveModelIcon(Model? model) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds the model avatar URL using the new OpenWebUI endpoint.
|
||||||
|
///
|
||||||
|
/// OpenWebUI now serves model avatars through a dedicated endpoint:
|
||||||
|
/// `/api/v1/models/model/profile/image?id={modelId}`
|
||||||
|
///
|
||||||
|
/// This endpoint:
|
||||||
|
/// - Requires authentication
|
||||||
|
/// - Handles external URLs (returns 302 redirect)
|
||||||
|
/// - Decodes base64 data URIs
|
||||||
|
/// - Provides a fallback favicon.png
|
||||||
|
String? buildModelAvatarUrl(ApiService? api, String? modelId) {
|
||||||
|
if (api == null || modelId == null || modelId.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final baseUrl = api.baseUrl.trim();
|
||||||
|
if (baseUrl.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final baseUri = Uri.parse(baseUrl);
|
||||||
|
final path = '/api/v1/models/model/profile/image';
|
||||||
|
final queryParams = {'id': modelId};
|
||||||
|
|
||||||
|
final avatarUri = baseUri.replace(
|
||||||
|
path: path,
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
return avatarUri.toString();
|
||||||
|
} catch (_) {
|
||||||
|
// Fallback to manual URL construction
|
||||||
|
final normalizedBase = baseUrl.endsWith('/')
|
||||||
|
? baseUrl.substring(0, baseUrl.length - 1)
|
||||||
|
: baseUrl;
|
||||||
|
return '$normalizedBase/api/v1/models/model/profile/image?id=${Uri.encodeComponent(modelId)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String? resolveModelIconUrl(ApiService? api, String? rawUrl) {
|
String? resolveModelIconUrl(ApiService? api, String? rawUrl) {
|
||||||
final value = rawUrl?.trim();
|
final value = rawUrl?.trim();
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
@@ -94,7 +139,30 @@ String? resolveModelIconUrl(ApiService? api, String? rawUrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves the final model icon URL for a given model.
|
||||||
|
///
|
||||||
|
/// This function first checks for a legacy profile_image_url in the model's
|
||||||
|
/// metadata (for backwards compatibility with older OpenWebUI versions).
|
||||||
|
/// If found and it's an external URL or data URI, it uses that directly.
|
||||||
|
///
|
||||||
|
/// Otherwise, it constructs the URL using the new OpenWebUI endpoint:
|
||||||
|
/// `/api/v1/models/model/profile/image?id={modelId}`
|
||||||
String? resolveModelIconUrlForModel(ApiService? api, Model? model) {
|
String? resolveModelIconUrlForModel(ApiService? api, Model? model) {
|
||||||
final raw = deriveModelIcon(model);
|
if (model == null) return null;
|
||||||
return resolveModelIconUrl(api, raw);
|
|
||||||
|
// Check for legacy profile_image_url in metadata
|
||||||
|
final legacyUrl = deriveModelIcon(model);
|
||||||
|
|
||||||
|
// If we have a legacy URL that's external or a data URI, use it directly
|
||||||
|
if (legacyUrl != null && legacyUrl.isNotEmpty) {
|
||||||
|
final trimmed = legacyUrl.trim();
|
||||||
|
if (trimmed.startsWith('data:image') ||
|
||||||
|
trimmed.startsWith('http://') ||
|
||||||
|
trimmed.startsWith('https://')) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the new dedicated endpoint for model avatars
|
||||||
|
return buildModelAvatarUrl(api, model.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,31 @@ import 'package:flutter/material.dart';
|
|||||||
import '../theme/theme_extensions.dart';
|
import '../theme/theme_extensions.dart';
|
||||||
import 'user_avatar.dart';
|
import 'user_avatar.dart';
|
||||||
|
|
||||||
|
/// Displays a model's avatar image with automatic caching and fallback UI.
|
||||||
|
///
|
||||||
|
/// The avatar can display:
|
||||||
|
/// - Network images from the OpenWebUI model avatar endpoint
|
||||||
|
/// - Data URIs (base64-encoded images)
|
||||||
|
/// - A fallback UI showing the first letter of the model name or a brain icon
|
||||||
|
///
|
||||||
|
/// Images are automatically cached using [CachedNetworkImage] with proper
|
||||||
|
/// authentication headers. The cache respects self-signed certificates if
|
||||||
|
/// configured.
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```dart
|
||||||
|
/// final avatarUrl = resolveModelIconUrlForModel(apiService, model);
|
||||||
|
/// ModelAvatar(size: 40, imageUrl: avatarUrl, label: model.name)
|
||||||
|
/// ```
|
||||||
class ModelAvatar extends StatelessWidget {
|
class ModelAvatar extends StatelessWidget {
|
||||||
|
/// The size (width and height) of the avatar in logical pixels.
|
||||||
final double size;
|
final double size;
|
||||||
|
|
||||||
|
/// The URL of the avatar image. Should be obtained via
|
||||||
|
/// [resolveModelIconUrlForModel] to use the correct OpenWebUI endpoint.
|
||||||
final String? imageUrl;
|
final String? imageUrl;
|
||||||
|
|
||||||
|
/// The model name, used for the fallback UI (shows first letter).
|
||||||
final String? label;
|
final String? label;
|
||||||
|
|
||||||
const ModelAvatar({super.key, required this.size, this.imageUrl, this.label});
|
const ModelAvatar({super.key, required this.size, this.imageUrl, this.label});
|
||||||
|
|||||||
Reference in New Issue
Block a user