diff --git a/cookbook/helper/ai_helper.py b/cookbook/helper/ai_helper.py new file mode 100644 index 000000000..5ac52cb5c --- /dev/null +++ b/cookbook/helper/ai_helper.py @@ -0,0 +1,18 @@ +from django.utils import timezone +from django.db.models import Sum + +from cookbook.models import AiLog + + +def get_monthly_token_usage(space): + """ + returns the number of credits the space has used in the current month + """ + return AiLog.objects.filter(space=space, credits_from_balance=False, created_at__month=timezone.now().month).aggregate(Sum('credit_cost'))['credit_cost__sum'] + + +def has_monthly_token(space): + """ + checks if the monthly credit limit has been exceeded + """ + return get_monthly_token_usage(space) < space.ai_credits_monthly diff --git a/cookbook/helper/permission_helper.py b/cookbook/helper/permission_helper.py index dbd7a15f9..4ddcb5a60 100644 --- a/cookbook/helper/permission_helper.py +++ b/cookbook/helper/permission_helper.py @@ -330,6 +330,21 @@ class CustomRecipePermission(permissions.BasePermission): return ((has_group_permission(request.user, ['guest']) and request.method in SAFE_METHODS) or has_group_permission(request.user, ['user'])) and obj.space == request.space +class CustomAiProviderPermission(permissions.BasePermission): + """ + Custom permission class for the AiProvider api endpoint + """ + message = _('You do not have the required permissions to view this page!') + + def has_permission(self, request, view): # user is either at least a user and the request is safe + return (has_group_permission(request.user, ['user']) and request.method in SAFE_METHODS) or (has_group_permission(request.user, ['admin']) or request.user.is_superuser) + + # editing of global providers allowed for superusers, space providers by admins and users can read only access + def has_object_permission(self, request, view, obj): + return ((obj.space is None and request.user.is_superuser) + or (obj.space == request.space and has_group_permission(request.user, ['admin'])) + or (obj.space == request.space and has_group_permission(request.user, ['user']) and request.method in SAFE_METHODS)) + class CustomUserPermission(permissions.BasePermission): """ diff --git a/cookbook/models.py b/cookbook/models.py index ae06a1234..0edd82fab 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -411,6 +411,8 @@ class AiProvider(models.Model): class AiLog(models.Model, PermissionModelMixin): + F_FILE_IMPORT = 'FILE_IMPORT' + ai_provider = models.ForeignKey(AiProvider, on_delete=models.SET_NULL, null=True) function = models.CharField(max_length=64) credit_cost = models.DecimalField(max_digits=16, decimal_places=4) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index d3a9393a8..94c9852d4 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -24,6 +24,7 @@ from rest_framework.fields import IntegerField from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage from cookbook.helper.HelperFunctions import str2bool +from cookbook.helper.ai_helper import get_monthly_token_usage from cookbook.helper.image_processing import is_file_type_allowed from cookbook.helper.permission_helper import above_space_limit from cookbook.helper.property_helper import FoodPropertyHelper @@ -36,7 +37,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu ShareLink, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion, - UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig, SearchPreference, SearchFields, AiLog) + UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig, SearchPreference, SearchFields, AiLog, AiProvider) from cookbook.templatetags.custom_tags import markdown from recipes.settings import AWS_ENABLED, MEDIA_URL, EMAIL_HOST @@ -353,7 +354,7 @@ class SpaceSerializer(WritableNestedModelSerializer): @extend_schema_field(int) def get_ai_monthly_credits_used(self, obj): - return AiLog.objects.filter(space=obj, credits_from_balance=False).aggregate(Sum('credit_cost'))['credit_cost__sum'] + return get_monthly_token_usage(obj) @extend_schema_field(float) def get_file_size_mb(self, obj): @@ -402,6 +403,33 @@ class SpacedModelSerializer(serializers.ModelSerializer): return super().create(validated_data) +class AiProviderSerializer(serializers.ModelSerializer): + + def create(self, validated_data): + if not self.context['request'].user.is_superuser: + validated_data['space'] = self.context['request'].space + return super().create(validated_data) + + class Meta: + model = AiProvider + fields = ('id','name', 'description', 'api_key', 'model_name', 'url', 'space', 'created_at', 'updated_at') + read_only_fields = ('created_at', 'updated_at',) + + extra_kwargs = { + 'api_key': {'write_only': True}, + } + + +class AiLogSerializer(serializers.ModelSerializer): + ai_provider = AiProviderSerializer(read_only=True) + + class Meta: + model = AiLog + fields = ('id', 'ai_provider', 'function', 'credit_cost', 'credits_from_balance', 'input_tokens', 'output_tokens', 'start_time', 'end_time', 'created_by', 'created_at', + 'updated_at') + read_only_fields = ('__all__',) + + class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer): def create(self, validated_data): @@ -1794,6 +1822,7 @@ class RecipeFromSourceResponseSerializer(serializers.Serializer): class AiImportSerializer(serializers.Serializer): + ai_provider_id = serializers.IntegerField() file = serializers.FileField(allow_null=True) text = serializers.CharField(allow_null=True, allow_blank=True) recipe_id = serializers.CharField(allow_null=True, allow_blank=True) diff --git a/cookbook/urls.py b/cookbook/urls.py index cbc58bbe7..4501eb04f 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -61,6 +61,8 @@ router.register(r'search-preference', api.SearchPreferenceViewSet) router.register(r'user-space', api.UserSpaceViewSet) router.register(r'view-log', api.ViewLogViewSet) router.register(r'access-token', api.AccessTokenViewSet) +router.register(r'ai-provider', api.AiProviderViewSet) +router.register(r'ai-log', api.AiLogViewSet) router.register(r'localization', api.LocalizationViewSet, basename='localization') router.register(r'server-settings', api.ServerSettingsViewSet, basename='server-settings') diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 64eef47d4..1611811cc 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -74,7 +74,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsOwner, Cus CustomTokenHasScope, CustomUserPermission, IsReadOnlyDRF, above_space_limit, group_required, has_group_permission, is_space_owner, - switch_user_active_space + switch_user_active_space, CustomAiProviderPermission ) from cookbook.helper.recipe_search import RecipeSearch from cookbook.helper.recipe_url_import import clean_dict, get_from_youtube_scraper, get_images_from_soup @@ -85,7 +85,7 @@ from cookbook.models import (Automation, BookmarkletImport, ConnectorConfig, Coo RecipeBookEntry, ShareLink, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion, - UserFile, UserPreference, UserSpace, ViewLog, RecipeImport, SearchPreference, SearchFields + UserFile, UserPreference, UserSpace, ViewLog, RecipeImport, SearchPreference, SearchFields, AiLog, AiProvider ) from cookbook.provider.dropbox import Dropbox from cookbook.provider.local import Local @@ -110,7 +110,8 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, Au UserSerializer, UserSpaceSerializer, ViewLogSerializer, LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer, ShoppingListEntryBulkCreateSerializer, FdcQuerySerializer, AiImportSerializer, ImportOpenDataSerializer, ImportOpenDataMetaDataSerializer, ImportOpenDataResponseSerializer, ExportRequestSerializer, - RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer + RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer, + AiProviderSerializer, AiLogSerializer ) from cookbook.version_info import TANDOOR_VERSION from cookbook.views.import_export import get_integration @@ -617,6 +618,29 @@ class SearchPreferenceViewSet(LoggingMixin, viewsets.ModelViewSet): return self.queryset.filter(user=self.request.user) +class AiProviderViewSet(LoggingMixin, viewsets.ModelViewSet): + queryset = AiProvider.objects + serializer_class = AiProviderSerializer + permission_classes = [CustomAiProviderPermission & CustomTokenHasReadWriteScope] + pagination_class = DefaultPagination + + def get_queryset(self): + # read only access to all space and global AiProviders + with scopes_disabled(): + return self.queryset.filter(Q(space=self.request.space) | Q(space__isnull=True)) + + +class AiLogViewSet(LoggingMixin, viewsets.ModelViewSet): + queryset = AiLog.objects + serializer_class = AiLogSerializer + permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope] + http_method_names = ['get'] + pagination_class = DefaultPagination + + def get_queryset(self): + return self.queryset.filter(space=self.request.space) + + class StorageViewSet(LoggingMixin, viewsets.ModelViewSet): # TODO handle delete protect error and adjust test queryset = Storage.objects @@ -2000,14 +2024,28 @@ class AiImportView(APIView): if serializer.is_valid(): # TODO max file size check + if 'ai_provider_id' not in serializer.validated_data: + response = { + 'error': True, + 'msg': 'You must select an AI provider to perform your request', + } + return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST) + + ai_provider = AiProvider.objects.filter(pk=serializer.validated_data['ai_provider_id']).filter(Q(space=request.space) | Q(space__isnull=True)).first() + def log_ai_request(kwargs, completion_response, start_time, end_time): - print(completion_response['usage']['completion_tokens'], completion_response['usage']['prompt_tokens'], start_time, end_time) - try: - response_cost = kwargs.get("response_cost", 0) - print("response_cost", response_cost) - except: - print('could not get cost') - traceback.print_exc() + AiLog.objects.create( + created_by=request.user, + space=request.space, + ai_provider=ai_provider, + start_time=start_time, + end_time=end_time, + input_tokens=completion_response['usage']['prompt_tokens'], + output_tokens=completion_response['usage']['completion_tokens'], + function=AiLog.F_FILE_IMPORT, + credit_cost=kwargs.get("response_cost", 0) * 100, + credits_from_balance=False, # TODO implement + ) litellm.success_callback = [log_ai_request] @@ -2079,7 +2117,9 @@ class AiImportView(APIView): return Response(RecipeFromSourceResponseSerializer(context={'request': request}).to_representation(response), status=status.HTTP_400_BAD_REQUEST) try: - ai_response = completion(api_key=AI_API_KEY, model=AI_MODEL_NAME, response_format={"type": "json_object"}, messages=messages, ) + ai_response = completion(api_key=ai_provider.api_key, + model=ai_provider.model_name, + response_format={"type": "json_object"}, messages=messages, ) except BadRequestError as err: response = { 'error': True, diff --git a/vue3/src/components/display/RecipeView.vue b/vue3/src/components/display/RecipeView.vue index 10f382742..f5534fcdc 100644 --- a/vue3/src/components/display/RecipeView.vue +++ b/vue3/src/components/display/RecipeView.vue @@ -126,6 +126,7 @@ Convert the recipe using AI + @@ -191,7 +192,7 @@ + + \ No newline at end of file diff --git a/vue3/src/composables/useFileApi.ts b/vue3/src/composables/useFileApi.ts index e2dcd6cee..22ce3f10f 100644 --- a/vue3/src/composables/useFileApi.ts +++ b/vue3/src/composables/useFileApi.ts @@ -1,7 +1,7 @@ import {useDjangoUrls} from "@/composables/useDjangoUrls"; import {ref} from "vue"; import {getCookie} from "@/utils/cookie"; -import {RecipeFromSourceResponseFromJSON, RecipeImageFromJSON, ResponseError, UserFile, UserFileFromJSON} from "@/openapi"; +import {AiProvider, RecipeFromSourceResponseFromJSON, RecipeImageFromJSON, ResponseError, UserFile, UserFileFromJSON} from "@/openapi"; /** @@ -86,7 +86,7 @@ export function useFileApi() { * @param text text to import * @param recipeId id of a recipe to use as import base (for external recipes */ - function doAiImport(file: File | null, text: string = '', recipeId: string = '') { + function doAiImport(providerId: number, file: File | null, text: string = '', recipeId: string = '') { let formData = new FormData() if (file != null) { @@ -96,6 +96,7 @@ export function useFileApi() { } formData.append('text', text) formData.append('recipe_id', recipeId) + formData.append('ai_provider_id', providerId) fileApiLoading.value = true return fetch(getDjangoUrl(`api/ai-import/`), { diff --git a/vue3/src/locales/de.json b/vue3/src/locales/de.json index 51370e67c..3de82e150 100644 --- a/vue3/src/locales/de.json +++ b/vue3/src/locales/de.json @@ -2,6 +2,7 @@ "AI": "AI", "AIImportSubtitle": "Verwende AI um Fotos von Rezepten zu importieren.", "API": "API", + "APIKey": "API Schlüssel", "Summary": "Zusammenfassung", "Structured": "Strukturiert", "API_Browser": "API Browser", @@ -307,6 +308,7 @@ "Monday": "Montag", "Month": "Monat", "More": "Mehr", + "Model": "Modell", "Move": "Verschieben", "MoveCategory": "Verschieben nach: ", "MoveToStep": "Verschieben in Schritt", diff --git a/vue3/src/locales/en.json b/vue3/src/locales/en.json index e9e5f9f6a..6e97be614 100644 --- a/vue3/src/locales/en.json +++ b/vue3/src/locales/en.json @@ -2,6 +2,7 @@ "AI": "AI", "AIImportSubtitle": "Use AI to import images of recipes.", "API": "API", + "APIKey": "API key", "API_Browser": "API Browser", "API_Documentation": "API Docs", "AccessTokenHelp": "Access keys for the REST API.", @@ -305,6 +306,7 @@ "Monday": "Monday", "Month": "Month", "More": "More", + "Model": "Model", "Move": "Move", "MoveCategory": "Move To: ", "MoveToStep": "Move to Step", diff --git a/vue3/src/openapi/.openapi-generator/FILES b/vue3/src/openapi/.openapi-generator/FILES index 4fc553952..d415929b7 100644 --- a/vue3/src/openapi/.openapi-generator/FILES +++ b/vue3/src/openapi/.openapi-generator/FILES @@ -3,6 +3,8 @@ apis/ApiTokenAuthApi.ts apis/index.ts index.ts models/AccessToken.ts +models/AiLog.ts +models/AiProvider.ts models/AlignmentEnum.ts models/AuthToken.ts models/AutoMealPlan.ts @@ -57,6 +59,8 @@ models/OpenDataStoreCategory.ts models/OpenDataUnit.ts models/OpenDataUnitTypeEnum.ts models/OpenDataVersion.ts +models/PaginatedAiLogList.ts +models/PaginatedAiProviderList.ts models/PaginatedAutomationList.ts models/PaginatedBookmarkletImportListList.ts models/PaginatedConnectorConfigList.ts @@ -103,6 +107,7 @@ models/PaginatedUserSpaceList.ts models/PaginatedViewLogList.ts models/ParsedIngredient.ts models/PatchedAccessToken.ts +models/PatchedAiProvider.ts models/PatchedAutomation.ts models/PatchedBookmarkletImport.ts models/PatchedConnectorConfig.ts diff --git a/vue3/src/openapi/apis/ApiApi.ts b/vue3/src/openapi/apis/ApiApi.ts index b415a5745..d9e526ff1 100644 --- a/vue3/src/openapi/apis/ApiApi.ts +++ b/vue3/src/openapi/apis/ApiApi.ts @@ -16,6 +16,8 @@ import * as runtime from '../runtime'; import type { AccessToken, + AiLog, + AiProvider, AutoMealPlan, Automation, BookmarkletImport, @@ -49,6 +51,8 @@ import type { OpenDataStore, OpenDataUnit, OpenDataVersion, + PaginatedAiLogList, + PaginatedAiProviderList, PaginatedAutomationList, PaginatedBookmarkletImportListList, PaginatedConnectorConfigList, @@ -95,6 +99,7 @@ import type { PaginatedViewLogList, ParsedIngredient, PatchedAccessToken, + PatchedAiProvider, PatchedAutomation, PatchedBookmarkletImport, PatchedConnectorConfig, @@ -179,6 +184,10 @@ import type { import { AccessTokenFromJSON, AccessTokenToJSON, + AiLogFromJSON, + AiLogToJSON, + AiProviderFromJSON, + AiProviderToJSON, AutoMealPlanFromJSON, AutoMealPlanToJSON, AutomationFromJSON, @@ -245,6 +254,10 @@ import { OpenDataUnitToJSON, OpenDataVersionFromJSON, OpenDataVersionToJSON, + PaginatedAiLogListFromJSON, + PaginatedAiLogListToJSON, + PaginatedAiProviderListFromJSON, + PaginatedAiProviderListToJSON, PaginatedAutomationListFromJSON, PaginatedAutomationListToJSON, PaginatedBookmarkletImportListListFromJSON, @@ -337,6 +350,8 @@ import { ParsedIngredientToJSON, PatchedAccessTokenFromJSON, PatchedAccessTokenToJSON, + PatchedAiProviderFromJSON, + PatchedAiProviderToJSON, PatchedAutomationFromJSON, PatchedAutomationToJSON, PatchedBookmarkletImportFromJSON, @@ -527,6 +542,42 @@ export interface ApiAiImportCreateRequest { recipeId: string | null; } +export interface ApiAiLogListRequest { + page?: number; + pageSize?: number; +} + +export interface ApiAiLogRetrieveRequest { + id: number; +} + +export interface ApiAiProviderCreateRequest { + aiProvider: Omit; +} + +export interface ApiAiProviderDestroyRequest { + id: number; +} + +export interface ApiAiProviderListRequest { + page?: number; + pageSize?: number; +} + +export interface ApiAiProviderPartialUpdateRequest { + id: number; + patchedAiProvider?: Omit; +} + +export interface ApiAiProviderRetrieveRequest { + id: number; +} + +export interface ApiAiProviderUpdateRequest { + id: number; + aiProvider: Omit; +} + export interface ApiAutoPlanCreateRequest { autoMealPlan: AutoMealPlan; } @@ -1732,7 +1783,7 @@ export interface ApiSpaceListRequest { export interface ApiSpacePartialUpdateRequest { id: number; - patchedSpace?: Omit; + patchedSpace?: Omit; } export interface ApiSpaceRetrieveRequest { @@ -2443,6 +2494,319 @@ export class ApiApi extends runtime.BaseAPI { return await response.value(); } + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiLogListRaw(requestParameters: ApiAiLogListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + if (requestParameters['page'] != null) { + queryParameters['page'] = requestParameters['page']; + } + + if (requestParameters['pageSize'] != null) { + queryParameters['page_size'] = requestParameters['pageSize']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication + } + + const response = await this.request({ + path: `/api/ai-log/`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => PaginatedAiLogListFromJSON(jsonValue)); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiLogList(requestParameters: ApiAiLogListRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiAiLogListRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiLogRetrieveRaw(requestParameters: ApiAiLogRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiAiLogRetrieve().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication + } + + const response = await this.request({ + path: `/api/ai-log/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => AiLogFromJSON(jsonValue)); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiLogRetrieve(requestParameters: ApiAiLogRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiAiLogRetrieveRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiProviderCreateRaw(requestParameters: ApiAiProviderCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['aiProvider'] == null) { + throw new runtime.RequiredError( + 'aiProvider', + 'Required parameter "aiProvider" was null or undefined when calling apiAiProviderCreate().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication + } + + const response = await this.request({ + path: `/api/ai-provider/`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: AiProviderToJSON(requestParameters['aiProvider']), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => AiProviderFromJSON(jsonValue)); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiProviderCreate(requestParameters: ApiAiProviderCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiAiProviderCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiProviderDestroyRaw(requestParameters: ApiAiProviderDestroyRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiAiProviderDestroy().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication + } + + const response = await this.request({ + path: `/api/ai-provider/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'DELETE', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiProviderDestroy(requestParameters: ApiAiProviderDestroyRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.apiAiProviderDestroyRaw(requestParameters, initOverrides); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiProviderListRaw(requestParameters: ApiAiProviderListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + if (requestParameters['page'] != null) { + queryParameters['page'] = requestParameters['page']; + } + + if (requestParameters['pageSize'] != null) { + queryParameters['page_size'] = requestParameters['pageSize']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication + } + + const response = await this.request({ + path: `/api/ai-provider/`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => PaginatedAiProviderListFromJSON(jsonValue)); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiProviderList(requestParameters: ApiAiProviderListRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiAiProviderListRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiProviderPartialUpdateRaw(requestParameters: ApiAiProviderPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiAiProviderPartialUpdate().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication + } + + const response = await this.request({ + path: `/api/ai-provider/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'PATCH', + headers: headerParameters, + query: queryParameters, + body: PatchedAiProviderToJSON(requestParameters['patchedAiProvider']), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => AiProviderFromJSON(jsonValue)); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiProviderPartialUpdate(requestParameters: ApiAiProviderPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiAiProviderPartialUpdateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiProviderRetrieveRaw(requestParameters: ApiAiProviderRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiAiProviderRetrieve().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication + } + + const response = await this.request({ + path: `/api/ai-provider/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => AiProviderFromJSON(jsonValue)); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiProviderRetrieve(requestParameters: ApiAiProviderRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiAiProviderRetrieveRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiProviderUpdateRaw(requestParameters: ApiAiProviderUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiAiProviderUpdate().' + ); + } + + if (requestParameters['aiProvider'] == null) { + throw new runtime.RequiredError( + 'aiProvider', + 'Required parameter "aiProvider" was null or undefined when calling apiAiProviderUpdate().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.apiKey) { + headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication + } + + const response = await this.request({ + path: `/api/ai-provider/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'PUT', + headers: headerParameters, + query: queryParameters, + body: AiProviderToJSON(requestParameters['aiProvider']), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => AiProviderFromJSON(jsonValue)); + } + + /** + * logs request counts to redis cache total/per user/ + */ + async apiAiProviderUpdate(requestParameters: ApiAiProviderUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiAiProviderUpdateRaw(requestParameters, initOverrides); + return await response.value(); + } + /** * logs request counts to redis cache total/per user/ */ diff --git a/vue3/src/openapi/models/AiLog.ts b/vue3/src/openapi/models/AiLog.ts new file mode 100644 index 000000000..8d3b6b35e --- /dev/null +++ b/vue3/src/openapi/models/AiLog.ts @@ -0,0 +1,157 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Tandoor + * Tandoor API Docs + * + * The version of the OpenAPI document: 0.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { AiProvider } from './AiProvider'; +import { + AiProviderFromJSON, + AiProviderFromJSONTyped, + AiProviderToJSON, +} from './AiProvider'; + +/** + * + * @export + * @interface AiLog + */ +export interface AiLog { + /** + * + * @type {number} + * @memberof AiLog + */ + id?: number; + /** + * + * @type {AiProvider} + * @memberof AiLog + */ + readonly aiProvider: AiProvider; + /** + * + * @type {string} + * @memberof AiLog + */ + _function: string; + /** + * + * @type {number} + * @memberof AiLog + */ + creditCost: number; + /** + * + * @type {boolean} + * @memberof AiLog + */ + creditsFromBalance?: boolean; + /** + * + * @type {number} + * @memberof AiLog + */ + inputTokens?: number; + /** + * + * @type {number} + * @memberof AiLog + */ + outputTokens?: number; + /** + * + * @type {Date} + * @memberof AiLog + */ + startTime?: Date; + /** + * + * @type {Date} + * @memberof AiLog + */ + endTime?: Date; + /** + * + * @type {number} + * @memberof AiLog + */ + createdBy?: number; + /** + * + * @type {Date} + * @memberof AiLog + */ + readonly createdAt: Date; + /** + * + * @type {Date} + * @memberof AiLog + */ + readonly updatedAt: Date; +} + +/** + * Check if a given object implements the AiLog interface. + */ +export function instanceOfAiLog(value: object): value is AiLog { + if (!('aiProvider' in value) || value['aiProvider'] === undefined) return false; + if (!('_function' in value) || value['_function'] === undefined) return false; + if (!('creditCost' in value) || value['creditCost'] === undefined) return false; + if (!('createdAt' in value) || value['createdAt'] === undefined) return false; + if (!('updatedAt' in value) || value['updatedAt'] === undefined) return false; + return true; +} + +export function AiLogFromJSON(json: any): AiLog { + return AiLogFromJSONTyped(json, false); +} + +export function AiLogFromJSONTyped(json: any, ignoreDiscriminator: boolean): AiLog { + if (json == null) { + return json; + } + return { + + 'id': json['id'] == null ? undefined : json['id'], + 'aiProvider': AiProviderFromJSON(json['ai_provider']), + '_function': json['function'], + 'creditCost': json['credit_cost'], + 'creditsFromBalance': json['credits_from_balance'] == null ? undefined : json['credits_from_balance'], + 'inputTokens': json['input_tokens'] == null ? undefined : json['input_tokens'], + 'outputTokens': json['output_tokens'] == null ? undefined : json['output_tokens'], + 'startTime': json['start_time'] == null ? undefined : (new Date(json['start_time'])), + 'endTime': json['end_time'] == null ? undefined : (new Date(json['end_time'])), + 'createdBy': json['created_by'] == null ? undefined : json['created_by'], + 'createdAt': (new Date(json['created_at'])), + 'updatedAt': (new Date(json['updated_at'])), + }; +} + +export function AiLogToJSON(value?: Omit | null): any { + if (value == null) { + return value; + } + return { + + 'id': value['id'], + 'function': value['_function'], + 'credit_cost': value['creditCost'], + 'credits_from_balance': value['creditsFromBalance'], + 'input_tokens': value['inputTokens'], + 'output_tokens': value['outputTokens'], + 'start_time': value['startTime'] == null ? undefined : ((value['startTime'] as any).toISOString()), + 'end_time': value['endTime'] == null ? undefined : ((value['endTime'] as any).toISOString()), + 'created_by': value['createdBy'], + }; +} + diff --git a/vue3/src/openapi/models/AiProvider.ts b/vue3/src/openapi/models/AiProvider.ts new file mode 100644 index 000000000..bb4d01ecf --- /dev/null +++ b/vue3/src/openapi/models/AiProvider.ts @@ -0,0 +1,127 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Tandoor + * Tandoor API Docs + * + * The version of the OpenAPI document: 0.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface AiProvider + */ +export interface AiProvider { + /** + * + * @type {number} + * @memberof AiProvider + */ + id?: number; + /** + * + * @type {string} + * @memberof AiProvider + */ + name: string; + /** + * + * @type {string} + * @memberof AiProvider + */ + description?: string; + /** + * + * @type {string} + * @memberof AiProvider + */ + apiKey: string; + /** + * + * @type {string} + * @memberof AiProvider + */ + modelName: string; + /** + * + * @type {string} + * @memberof AiProvider + */ + url?: string; + /** + * + * @type {number} + * @memberof AiProvider + */ + space?: number; + /** + * + * @type {Date} + * @memberof AiProvider + */ + readonly createdAt: Date; + /** + * + * @type {Date} + * @memberof AiProvider + */ + readonly updatedAt: Date; +} + +/** + * Check if a given object implements the AiProvider interface. + */ +export function instanceOfAiProvider(value: object): value is AiProvider { + if (!('name' in value) || value['name'] === undefined) return false; + if (!('apiKey' in value) || value['apiKey'] === undefined) return false; + if (!('modelName' in value) || value['modelName'] === undefined) return false; + if (!('createdAt' in value) || value['createdAt'] === undefined) return false; + if (!('updatedAt' in value) || value['updatedAt'] === undefined) return false; + return true; +} + +export function AiProviderFromJSON(json: any): AiProvider { + return AiProviderFromJSONTyped(json, false); +} + +export function AiProviderFromJSONTyped(json: any, ignoreDiscriminator: boolean): AiProvider { + if (json == null) { + return json; + } + return { + + 'id': json['id'] == null ? undefined : json['id'], + 'name': json['name'], + 'description': json['description'] == null ? undefined : json['description'], + 'apiKey': json['api_key'], + 'modelName': json['model_name'], + 'url': json['url'] == null ? undefined : json['url'], + 'space': json['space'] == null ? undefined : json['space'], + 'createdAt': (new Date(json['created_at'])), + 'updatedAt': (new Date(json['updated_at'])), + }; +} + +export function AiProviderToJSON(value?: Omit | null): any { + if (value == null) { + return value; + } + return { + + 'id': value['id'], + 'name': value['name'], + 'description': value['description'], + 'api_key': value['apiKey'], + 'model_name': value['modelName'], + 'url': value['url'], + 'space': value['space'], + }; +} + diff --git a/vue3/src/openapi/models/PaginatedAiLogList.ts b/vue3/src/openapi/models/PaginatedAiLogList.ts new file mode 100644 index 000000000..a40a2c2da --- /dev/null +++ b/vue3/src/openapi/models/PaginatedAiLogList.ts @@ -0,0 +1,101 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Tandoor + * Tandoor API Docs + * + * The version of the OpenAPI document: 0.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { AiLog } from './AiLog'; +import { + AiLogFromJSON, + AiLogFromJSONTyped, + AiLogToJSON, +} from './AiLog'; + +/** + * + * @export + * @interface PaginatedAiLogList + */ +export interface PaginatedAiLogList { + /** + * + * @type {number} + * @memberof PaginatedAiLogList + */ + count: number; + /** + * + * @type {string} + * @memberof PaginatedAiLogList + */ + next?: string; + /** + * + * @type {string} + * @memberof PaginatedAiLogList + */ + previous?: string; + /** + * + * @type {Array} + * @memberof PaginatedAiLogList + */ + results: Array; + /** + * + * @type {Date} + * @memberof PaginatedAiLogList + */ + timestamp?: Date; +} + +/** + * Check if a given object implements the PaginatedAiLogList interface. + */ +export function instanceOfPaginatedAiLogList(value: object): value is PaginatedAiLogList { + if (!('count' in value) || value['count'] === undefined) return false; + if (!('results' in value) || value['results'] === undefined) return false; + return true; +} + +export function PaginatedAiLogListFromJSON(json: any): PaginatedAiLogList { + return PaginatedAiLogListFromJSONTyped(json, false); +} + +export function PaginatedAiLogListFromJSONTyped(json: any, ignoreDiscriminator: boolean): PaginatedAiLogList { + if (json == null) { + return json; + } + return { + + 'count': json['count'], + 'next': json['next'] == null ? undefined : json['next'], + 'previous': json['previous'] == null ? undefined : json['previous'], + 'results': ((json['results'] as Array).map(AiLogFromJSON)), + 'timestamp': json['timestamp'] == null ? undefined : (new Date(json['timestamp'])), + }; +} + +export function PaginatedAiLogListToJSON(value?: PaginatedAiLogList | null): any { + if (value == null) { + return value; + } + return { + + 'count': value['count'], + 'next': value['next'], + 'previous': value['previous'], + 'results': ((value['results'] as Array).map(AiLogToJSON)), + 'timestamp': value['timestamp'] == null ? undefined : ((value['timestamp']).toISOString()), + }; +} + diff --git a/vue3/src/openapi/models/PaginatedAiProviderList.ts b/vue3/src/openapi/models/PaginatedAiProviderList.ts new file mode 100644 index 000000000..ff879e592 --- /dev/null +++ b/vue3/src/openapi/models/PaginatedAiProviderList.ts @@ -0,0 +1,101 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Tandoor + * Tandoor API Docs + * + * The version of the OpenAPI document: 0.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { AiProvider } from './AiProvider'; +import { + AiProviderFromJSON, + AiProviderFromJSONTyped, + AiProviderToJSON, +} from './AiProvider'; + +/** + * + * @export + * @interface PaginatedAiProviderList + */ +export interface PaginatedAiProviderList { + /** + * + * @type {number} + * @memberof PaginatedAiProviderList + */ + count: number; + /** + * + * @type {string} + * @memberof PaginatedAiProviderList + */ + next?: string; + /** + * + * @type {string} + * @memberof PaginatedAiProviderList + */ + previous?: string; + /** + * + * @type {Array} + * @memberof PaginatedAiProviderList + */ + results: Array; + /** + * + * @type {Date} + * @memberof PaginatedAiProviderList + */ + timestamp?: Date; +} + +/** + * Check if a given object implements the PaginatedAiProviderList interface. + */ +export function instanceOfPaginatedAiProviderList(value: object): value is PaginatedAiProviderList { + if (!('count' in value) || value['count'] === undefined) return false; + if (!('results' in value) || value['results'] === undefined) return false; + return true; +} + +export function PaginatedAiProviderListFromJSON(json: any): PaginatedAiProviderList { + return PaginatedAiProviderListFromJSONTyped(json, false); +} + +export function PaginatedAiProviderListFromJSONTyped(json: any, ignoreDiscriminator: boolean): PaginatedAiProviderList { + if (json == null) { + return json; + } + return { + + 'count': json['count'], + 'next': json['next'] == null ? undefined : json['next'], + 'previous': json['previous'] == null ? undefined : json['previous'], + 'results': ((json['results'] as Array).map(AiProviderFromJSON)), + 'timestamp': json['timestamp'] == null ? undefined : (new Date(json['timestamp'])), + }; +} + +export function PaginatedAiProviderListToJSON(value?: PaginatedAiProviderList | null): any { + if (value == null) { + return value; + } + return { + + 'count': value['count'], + 'next': value['next'], + 'previous': value['previous'], + 'results': ((value['results'] as Array).map(AiProviderToJSON)), + 'timestamp': value['timestamp'] == null ? undefined : ((value['timestamp']).toISOString()), + }; +} + diff --git a/vue3/src/openapi/models/PatchedAiProvider.ts b/vue3/src/openapi/models/PatchedAiProvider.ts new file mode 100644 index 000000000..833898bd0 --- /dev/null +++ b/vue3/src/openapi/models/PatchedAiProvider.ts @@ -0,0 +1,122 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Tandoor + * Tandoor API Docs + * + * The version of the OpenAPI document: 0.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface PatchedAiProvider + */ +export interface PatchedAiProvider { + /** + * + * @type {number} + * @memberof PatchedAiProvider + */ + id?: number; + /** + * + * @type {string} + * @memberof PatchedAiProvider + */ + name?: string; + /** + * + * @type {string} + * @memberof PatchedAiProvider + */ + description?: string; + /** + * + * @type {string} + * @memberof PatchedAiProvider + */ + apiKey?: string; + /** + * + * @type {string} + * @memberof PatchedAiProvider + */ + modelName?: string; + /** + * + * @type {string} + * @memberof PatchedAiProvider + */ + url?: string; + /** + * + * @type {number} + * @memberof PatchedAiProvider + */ + space?: number; + /** + * + * @type {Date} + * @memberof PatchedAiProvider + */ + readonly createdAt?: Date; + /** + * + * @type {Date} + * @memberof PatchedAiProvider + */ + readonly updatedAt?: Date; +} + +/** + * Check if a given object implements the PatchedAiProvider interface. + */ +export function instanceOfPatchedAiProvider(value: object): value is PatchedAiProvider { + return true; +} + +export function PatchedAiProviderFromJSON(json: any): PatchedAiProvider { + return PatchedAiProviderFromJSONTyped(json, false); +} + +export function PatchedAiProviderFromJSONTyped(json: any, ignoreDiscriminator: boolean): PatchedAiProvider { + if (json == null) { + return json; + } + return { + + 'id': json['id'] == null ? undefined : json['id'], + 'name': json['name'] == null ? undefined : json['name'], + 'description': json['description'] == null ? undefined : json['description'], + 'apiKey': json['api_key'] == null ? undefined : json['api_key'], + 'modelName': json['model_name'] == null ? undefined : json['model_name'], + 'url': json['url'] == null ? undefined : json['url'], + 'space': json['space'] == null ? undefined : json['space'], + 'createdAt': json['created_at'] == null ? undefined : (new Date(json['created_at'])), + 'updatedAt': json['updated_at'] == null ? undefined : (new Date(json['updated_at'])), + }; +} + +export function PatchedAiProviderToJSON(value?: Omit | null): any { + if (value == null) { + return value; + } + return { + + 'id': value['id'], + 'name': value['name'], + 'description': value['description'], + 'api_key': value['apiKey'], + 'model_name': value['modelName'], + 'url': value['url'], + 'space': value['space'], + }; +} + diff --git a/vue3/src/openapi/models/PatchedSpace.ts b/vue3/src/openapi/models/PatchedSpace.ts index 36bb0952d..d4fc0bac3 100644 --- a/vue3/src/openapi/models/PatchedSpace.ts +++ b/vue3/src/openapi/models/PatchedSpace.ts @@ -212,6 +212,24 @@ export interface PatchedSpace { * @memberof PatchedSpace */ logoColorSvg?: UserFileView; + /** + * + * @type {number} + * @memberof PatchedSpace + */ + readonly aiCreditsMonthly?: number; + /** + * + * @type {number} + * @memberof PatchedSpace + */ + readonly aiCreditsBalance?: number; + /** + * + * @type {number} + * @memberof PatchedSpace + */ + readonly aiMonthlyCreditsUsed?: number; } /** @@ -258,10 +276,13 @@ export function PatchedSpaceFromJSONTyped(json: any, ignoreDiscriminator: boolea 'logoColor192': json['logo_color_192'] == null ? undefined : UserFileViewFromJSON(json['logo_color_192']), 'logoColor512': json['logo_color_512'] == null ? undefined : UserFileViewFromJSON(json['logo_color_512']), 'logoColorSvg': json['logo_color_svg'] == null ? undefined : UserFileViewFromJSON(json['logo_color_svg']), + 'aiCreditsMonthly': json['ai_credits_monthly'] == null ? undefined : json['ai_credits_monthly'], + 'aiCreditsBalance': json['ai_credits_balance'] == null ? undefined : json['ai_credits_balance'], + 'aiMonthlyCreditsUsed': json['ai_monthly_credits_used'] == null ? undefined : json['ai_monthly_credits_used'], }; } -export function PatchedSpaceToJSON(value?: Omit | null): any { +export function PatchedSpaceToJSON(value?: Omit | null): any { if (value == null) { return value; } diff --git a/vue3/src/openapi/models/Space.ts b/vue3/src/openapi/models/Space.ts index fe7a259b5..f9febb9fb 100644 --- a/vue3/src/openapi/models/Space.ts +++ b/vue3/src/openapi/models/Space.ts @@ -212,6 +212,24 @@ export interface Space { * @memberof Space */ logoColorSvg?: UserFileView; + /** + * + * @type {number} + * @memberof Space + */ + readonly aiCreditsMonthly: number; + /** + * + * @type {number} + * @memberof Space + */ + readonly aiCreditsBalance: number; + /** + * + * @type {number} + * @memberof Space + */ + readonly aiMonthlyCreditsUsed: number; } /** @@ -229,6 +247,9 @@ export function instanceOfSpace(value: object): value is Space { if (!('userCount' in value) || value['userCount'] === undefined) return false; if (!('recipeCount' in value) || value['recipeCount'] === undefined) return false; if (!('fileSizeMb' in value) || value['fileSizeMb'] === undefined) return false; + if (!('aiCreditsMonthly' in value) || value['aiCreditsMonthly'] === undefined) return false; + if (!('aiCreditsBalance' in value) || value['aiCreditsBalance'] === undefined) return false; + if (!('aiMonthlyCreditsUsed' in value) || value['aiMonthlyCreditsUsed'] === undefined) return false; return true; } @@ -269,10 +290,13 @@ export function SpaceFromJSONTyped(json: any, ignoreDiscriminator: boolean): Spa 'logoColor192': json['logo_color_192'] == null ? undefined : UserFileViewFromJSON(json['logo_color_192']), 'logoColor512': json['logo_color_512'] == null ? undefined : UserFileViewFromJSON(json['logo_color_512']), 'logoColorSvg': json['logo_color_svg'] == null ? undefined : UserFileViewFromJSON(json['logo_color_svg']), + 'aiCreditsMonthly': json['ai_credits_monthly'], + 'aiCreditsBalance': json['ai_credits_balance'], + 'aiMonthlyCreditsUsed': json['ai_monthly_credits_used'], }; } -export function SpaceToJSON(value?: Omit | null): any { +export function SpaceToJSON(value?: Omit | null): any { if (value == null) { return value; } diff --git a/vue3/src/openapi/models/index.ts b/vue3/src/openapi/models/index.ts index 7cedeefa9..44221fb71 100644 --- a/vue3/src/openapi/models/index.ts +++ b/vue3/src/openapi/models/index.ts @@ -1,6 +1,8 @@ /* tslint:disable */ /* eslint-disable */ export * from './AccessToken'; +export * from './AiLog'; +export * from './AiProvider'; export * from './AlignmentEnum'; export * from './AuthToken'; export * from './AutoMealPlan'; @@ -55,6 +57,8 @@ export * from './OpenDataStoreCategory'; export * from './OpenDataUnit'; export * from './OpenDataUnitTypeEnum'; export * from './OpenDataVersion'; +export * from './PaginatedAiLogList'; +export * from './PaginatedAiProviderList'; export * from './PaginatedAutomationList'; export * from './PaginatedBookmarkletImportListList'; export * from './PaginatedConnectorConfigList'; @@ -101,6 +105,7 @@ export * from './PaginatedUserSpaceList'; export * from './PaginatedViewLogList'; export * from './ParsedIngredient'; export * from './PatchedAccessToken'; +export * from './PatchedAiProvider'; export * from './PatchedAutomation'; export * from './PatchedBookmarkletImport'; export * from './PatchedConnectorConfig'; diff --git a/vue3/src/pages/DatabasePage.vue b/vue3/src/pages/DatabasePage.vue index e887f2249..0b447c264 100644 --- a/vue3/src/pages/DatabasePage.vue +++ b/vue3/src/pages/DatabasePage.vue @@ -35,6 +35,16 @@ + + +

{{ $t('Ai') }}

+
+
+ + + + + diff --git a/vue3/src/pages/RecipeImportPage.vue b/vue3/src/pages/RecipeImportPage.vue index 6aaae7a6e..025c81e27 100644 --- a/vue3/src/pages/RecipeImportPage.vue +++ b/vue3/src/pages/RecipeImportPage.vue @@ -140,10 +140,18 @@ @keydown.enter="loadRecipeFromUrl({url: importUrl})">
- - {{ $t('File') }} - {{ $t('Text') }} - + + + + + + + {{ $t('File') }} + {{ $t('Text') }} + + + +