basics of AI provider system

This commit is contained in:
vabene1111
2025-09-05 21:36:43 +02:00
parent 5572833f64
commit 41f0060c43
24 changed files with 1346 additions and 31 deletions

View File

@@ -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

View File

@@ -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):
"""

View File

@@ -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)

View File

@@ -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)

View File

@@ -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')

View File

@@ -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,

View File

@@ -126,6 +126,7 @@
<v-card-text>
Convert the recipe using AI
<model-select model="AiProvider" v-model="selectedAiProvider"></model-select>
</v-card-text>
</v-card>
</template>
@@ -191,7 +192,7 @@
<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {ApiApi, Recipe} from "@/openapi"
import {AiProvider, ApiApi, Recipe} from "@/openapi"
import NumberScalerDialog from "@/components/inputs/NumberScalerDialog.vue"
import StepsOverview from "@/components/display/StepsOverview.vue";
import RecipeActivity from "@/components/display/RecipeActivity.vue";
@@ -207,6 +208,7 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import {useFileApi} from "@/composables/useFileApi.ts";
import PrivateRecipeBadge from "@/components/display/PrivateRecipeBadge.vue";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
const {request, release} = useWakeLock()
const {doAiImport, fileApiLoading} = useFileApi()
@@ -217,6 +219,8 @@ const recipe = defineModel<Recipe>({required: true})
const servings = ref(1)
const showFullRecipeName = ref(false)
const selectedAiProvider = ref<undefined | AiProvider>(undefined)
/**
* factor for multiplying ingredient amounts based on recipe base servings and user selected servings
*/
@@ -249,7 +253,7 @@ onBeforeUnmount(() => {
function aiConvertRecipe() {
let api = new ApiApi()
doAiImport(null, '', recipe.value.id!).then(r => {
doAiImport(selectedAiProvider.value.id!,null, '', recipe.value.id!).then(r => {
if (r.recipe) {
recipe.value.internal = true
recipe.value.steps = r.recipe.steps

View File

@@ -0,0 +1,99 @@
<template>
<model-editor-base
:loading="loading"
:dialog="dialog"
@save="saveObject"
@delete="deleteObject"
@close="emit('close'); editingObjChanged = false"
:is-update="isUpdate()"
:is-changed="editingObjChanged"
:model-class="modelClass"
:object-name="editingObjName()">
<v-card-text>
<v-form :disabled="loading">
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
<v-textarea :label="$t('Description')" v-model="editingObj.description"></v-textarea>
<!-- TODO localize -->
<v-checkbox :label="$t('Global')" v-model="globalProvider" v-if="useUserPreferenceStore().userSettings.user.isSuperuser"></v-checkbox>
<v-text-field :label="$t('APIKey')" v-model="editingObj.apiKey"></v-text-field>
<v-combobox :label="$t('Model')" :items="aiModels" v-model="editingObj.modelName" hide-details>
</v-combobox>
<p class="mt-2 mb-2">You can use any model that <a href="https://docs.litellm.ai/docs/providers" target="_blank">LiteLLM supports</a>, the list just contains some
of the most
commonly used models.</p>
<v-text-field :label="$t('Url')" v-model="editingObj.url"></v-text-field>
</v-form>
</v-card-text>
</model-editor-base>
</template>
<script setup lang="ts">
import {onMounted, PropType, ref, watch} from "vue";
import {AiProvider} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const props = defineProps({
item: {type: {} as PropType<AiProvider>, required: false, default: null},
itemId: {type: [Number, String], required: false, default: undefined},
itemDefaults: {type: {} as PropType<AiProvider>, required: false, default: {} as AiProvider},
dialog: {type: Boolean, default: false}
})
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<AiProvider>('AiProvider', emit)
/**
* watch prop changes and re-initialize editor
* required to embed editor directly into pages and be able to change item from the outside
*/
watch([() => props.item, () => props.itemId], () => {
initializeEditor()
})
// object specific data (for selects/display)
const aiModels = ref(['gemini/gemini-2.5-pro', 'gemini/gemini-2.5-flash', 'gemini/gemini-2.5-flash-lite', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano'])
const globalProvider = ref(false)
watch(() => globalProvider, () => {
if(globalProvider.value){
editingObj.value.space = undefined
} else {
editingObj.value.space = useUserPreferenceStore().activeSpace.id!
}
})
onMounted(() => {
initializeEditor()
})
/**
* component specific state setup logic
*/
function initializeEditor() {
setupState(props.item, props.itemId, {
itemDefaults: props.itemDefaults
}).then(() => {
globalProvider.value = editingObj.value.space == undefined
})
}
</script>
<style scoped>
</style>

View File

@@ -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/`), {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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<AiProvider, 'createdAt'|'updatedAt'>;
}
export interface ApiAiProviderDestroyRequest {
id: number;
}
export interface ApiAiProviderListRequest {
page?: number;
pageSize?: number;
}
export interface ApiAiProviderPartialUpdateRequest {
id: number;
patchedAiProvider?: Omit<PatchedAiProvider, 'createdAt'|'updatedAt'>;
}
export interface ApiAiProviderRetrieveRequest {
id: number;
}
export interface ApiAiProviderUpdateRequest {
id: number;
aiProvider: Omit<AiProvider, 'createdAt'|'updatedAt'>;
}
export interface ApiAutoPlanCreateRequest {
autoMealPlan: AutoMealPlan;
}
@@ -1732,7 +1783,7 @@ export interface ApiSpaceListRequest {
export interface ApiSpacePartialUpdateRequest {
id: number;
patchedSpace?: Omit<PatchedSpace, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'>;
patchedSpace?: Omit<PatchedSpace, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiCreditsMonthly'|'aiCreditsBalance'|'aiMonthlyCreditsUsed'>;
}
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<runtime.ApiResponse<PaginatedAiLogList>> {
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<PaginatedAiLogList> {
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<runtime.ApiResponse<AiLog>> {
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<AiLog> {
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<runtime.ApiResponse<AiProvider>> {
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<AiProvider> {
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<runtime.ApiResponse<void>> {
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<void> {
await this.apiAiProviderDestroyRaw(requestParameters, initOverrides);
}
/**
* logs request counts to redis cache total/per user/
*/
async apiAiProviderListRaw(requestParameters: ApiAiProviderListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedAiProviderList>> {
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<PaginatedAiProviderList> {
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<runtime.ApiResponse<AiProvider>> {
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<AiProvider> {
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<runtime.ApiResponse<AiProvider>> {
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<AiProvider> {
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<runtime.ApiResponse<AiProvider>> {
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<AiProvider> {
const response = await this.apiAiProviderUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/

View File

@@ -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<AiLog, 'aiProvider'|'createdAt'|'updatedAt'> | 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'],
};
}

View File

@@ -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<AiProvider, 'createdAt'|'updatedAt'> | 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'],
};
}

View File

@@ -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<AiLog>}
* @memberof PaginatedAiLogList
*/
results: Array<AiLog>;
/**
*
* @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<any>).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<any>).map(AiLogToJSON)),
'timestamp': value['timestamp'] == null ? undefined : ((value['timestamp']).toISOString()),
};
}

View File

@@ -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<AiProvider>}
* @memberof PaginatedAiProviderList
*/
results: Array<AiProvider>;
/**
*
* @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<any>).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<any>).map(AiProviderToJSON)),
'timestamp': value['timestamp'] == null ? undefined : ((value['timestamp']).toISOString()),
};
}

View File

@@ -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<PatchedAiProvider, 'createdAt'|'updatedAt'> | 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'],
};
}

View File

@@ -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<PatchedSpace, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'> | null): any {
export function PatchedSpaceToJSON(value?: Omit<PatchedSpace, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiCreditsMonthly'|'aiCreditsBalance'|'aiMonthlyCreditsUsed'> | null): any {
if (value == null) {
return value;
}

View File

@@ -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<Space, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'> | null): any {
export function SpaceToJSON(value?: Omit<Space, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiCreditsMonthly'|'aiCreditsBalance'|'aiMonthlyCreditsUsed'> | null): any {
if (value == null) {
return value;
}

View File

@@ -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';

View File

@@ -35,6 +35,16 @@
<database-model-col model="MealType"></database-model-col>
</v-row>
<v-row>
<v-col>
<h2>{{ $t('Ai') }}</h2>
</v-col>
</v-row>
<v-row dense>
<database-model-col model="AiProvider"></database-model-col>
<database-model-col model="AiLog"></database-model-col>
</v-row>
<template v-for="p in TANDOOR_PLUGINS" :key="p.name">
<component :is="p.databasePageComponent" v-if="p.databasePageComponent"></component>
</template>

View File

@@ -140,10 +140,18 @@
@keydown.enter="loadRecipeFromUrl({url: importUrl})"></v-text-field>
<div v-if="importType == 'ai'">
<v-btn-toggle v-model="aiMode">
<v-btn value="file">{{ $t('File') }}</v-btn>
<v-btn value="text">{{ $t('Text') }}</v-btn>
</v-btn-toggle>
<v-row>
<v-col md="6">
<ModelSelect model="AiProvider" v-model="selectedAiProvider"></ModelSelect>
</v-col>
<v-col md="6">
<v-btn-toggle class="mb-2" border divided v-model="aiMode">
<v-btn value="file">{{ $t('File') }}</v-btn>
<v-btn value="text">{{ $t('Text') }}</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-file-upload v-model="image" v-if="aiMode == 'file'" :loading="loading" clearable>
<template #icon>
@@ -540,6 +548,7 @@ import {useI18n} from "vue-i18n";
import {computed, onMounted, ref} from "vue";
import {
AccessToken,
AiProvider,
ApiApi,
ImportLog,
Recipe,
@@ -648,6 +657,7 @@ const appImportDuplicates = ref(false)
const appImportLog = ref<null | ImportLog>(null)
const image = ref<null | File>(null)
const aiMode = ref<'file' | 'text'>('file')
const selectedAiProvider = ref<undefined | AiProvider>(undefined)
const editAfterImport = ref(false)
const bookmarkletToken = ref("")
@@ -724,12 +734,15 @@ function loadRecipeFromUrl(recipeFromSourceRequest: RecipeFromSource) {
*/
function loadRecipeFromAiImport() {
let request = null
if(selectedAiProvider.value == undefined) {
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, "No AI Provider selected")
}
if (image.value != null && aiMode.value == 'file') {
console.log('file import')
request = doAiImport(image.value)
request = doAiImport(selectedAiProvider.value.id!,image.value)
} else if (sourceImportText.value != '' && aiMode.value == 'text') {
console.log('text import')
request = doAiImport(null, sourceImportText.value)
request = doAiImport(selectedAiProvider.value.id!, null, sourceImportText.value)
}
if (request != null) {
@@ -811,13 +824,13 @@ function deleteStep(step: SourceImportStep) {
}
function handleMergeAllSteps(): void {
if (importResponse.value.recipe && importResponse.value.recipe.steps){
if (importResponse.value.recipe && importResponse.value.recipe.steps) {
mergeAllSteps(importResponse.value.recipe.steps)
}
}
function handleSplitAllSteps(): void {
if (importResponse.value.recipe && importResponse.value.recipe.steps){
if (importResponse.value.recipe && importResponse.value.recipe.steps) {
splitAllSteps(importResponse.value.recipe.steps, '\n')
}
}

View File

@@ -1,5 +1,5 @@
import {
AccessToken,
AccessToken, AiLog, AiProvider,
ApiApi, ApiKeywordMoveUpdateRequest, Automation, type AutomationTypeEnum, ConnectorConfig, CookLog, CustomFilter,
Food,
Ingredient,
@@ -146,6 +146,8 @@ export type EditorSupportedModels =
| 'ViewLog'
| 'ConnectorConfig'
| 'SearchFields'
| 'AiProvider'
| 'AiLog'
// used to type methods/parameters in conjunction with configuration type
export type EditorSupportedTypes =
@@ -180,6 +182,8 @@ export type EditorSupportedTypes =
| ViewLog
| ConnectorConfig
| SearchFields
| AiProvider
| AiLog
export const TFood = {
name: 'Food',
@@ -788,6 +792,53 @@ export const TConnectorConfig = {
} as Model
registerModel(TConnectorConfig)
export const TAiProvider = {
name: 'AiProvider',
localizationKey: 'AiProvider',
localizationKeyDescription: 'AiProviderHelp',
icon: 'fa-solid fa-wand-magic-sparkles',
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/AiProviderEditor.vue`)),
disableListView: false,
toStringKeys: ['name'],
isPaginated: true,
disableCreate: false,
disableDelete: false,
disableUpdate: false,
tableHeaders: [
{title: 'Name', key: 'name'},
{title: 'Actions', key: 'action', align: 'end'},
]
} as Model
registerModel(TAiProvider)
export const TAiLog = {
name: 'AiLog',
localizationKey: 'AiLog',
localizationKeyDescription: 'AiLogHelp',
icon: 'fa-solid fa-wand-magic-sparkles',
disableListView: false,
toStringKeys: ['aiProvider.name', 'function', 'created_at'],
isPaginated: true,
disableCreate: true,
disableDelete: true,
disableUpdate: true,
tableHeaders: [
{title: 'Name', key: 'function'},
{title: 'AiProvider', key: 'aiProvider.name',},
{title: 'CreatedAt', key: 'createdAt'},
{title: 'Actions', key: 'action', align: 'end'},
]
} as Model
registerModel(TAiLog)
export const TFoodInheritField = {
name: 'FoodInheritField',
localizationKey: 'FoodInherit',