diff --git a/lib/core/utils/model_icon_utils.dart b/lib/core/utils/model_icon_utils.dart index a49eee4..a0deb7d 100644 --- a/lib/core/utils/model_icon_utils.dart +++ b/lib/core/utils/model_icon_utils.dart @@ -1,6 +1,11 @@ import '../models/model.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) { if (model == null) return null; @@ -47,6 +52,46 @@ String? deriveModelIcon(Model? model) { 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) { final value = rawUrl?.trim(); 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) { - final raw = deriveModelIcon(model); - return resolveModelIconUrl(api, raw); + if (model == null) return null; + + // 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); } diff --git a/lib/shared/widgets/model_avatar.dart b/lib/shared/widgets/model_avatar.dart index 7e62c37..7c16d0c 100644 --- a/lib/shared/widgets/model_avatar.dart +++ b/lib/shared/widgets/model_avatar.dart @@ -3,9 +3,31 @@ import 'package:flutter/material.dart'; import '../theme/theme_extensions.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 { + /// The size (width and height) of the avatar in logical pixels. final double size; + + /// The URL of the avatar image. Should be obtained via + /// [resolveModelIconUrlForModel] to use the correct OpenWebUI endpoint. final String? imageUrl; + + /// The model name, used for the fallback UI (shows first letter). final String? label; const ModelAvatar({super.key, required this.size, this.imageUrl, this.label});