diff --git a/plane-src/apps/api/plane/app/serializers/voice_tasker.py b/plane-src/apps/api/plane/app/serializers/voice_tasker.py index 461fb1f..832ec5d 100644 --- a/plane-src/apps/api/plane/app/serializers/voice_tasker.py +++ b/plane-src/apps/api/plane/app/serializers/voice_tasker.py @@ -33,6 +33,9 @@ class WorkspaceAISettingsSerializer(BaseSerializer): "max_audio_duration_seconds", "per_user_hourly_limit", "workspace_hourly_limit", + "per_user_daily_limit", + "workspace_daily_limit", + "project_daily_limit", "credential", "openai_api_key", "created_at", @@ -105,6 +108,21 @@ class WorkspaceAISettingsSerializer(BaseSerializer): raise serializers.ValidationError("Workspace hourly limit must be between 1 and 10000.") return value + def validate_per_user_daily_limit(self, value): + if value < 1 or value > 10000: + raise serializers.ValidationError("Per-user daily limit must be between 1 and 10000.") + return value + + def validate_workspace_daily_limit(self, value): + if value < 1 or value > 100000: + raise serializers.ValidationError("Workspace daily limit must be between 1 and 100000.") + return value + + def validate_project_daily_limit(self, value): + if value < 1 or value > 50000: + raise serializers.ValidationError("Project daily limit must be between 1 and 50000.") + return value + def update(self, instance, validated_data): api_key = validated_data.pop("openai_api_key", None) default_project_id = validated_data.pop("default_project_id", serializers.empty) diff --git a/plane-src/apps/api/plane/app/views/voice_tasker.py b/plane-src/apps/api/plane/app/views/voice_tasker.py index 1f99d6f..4042b2e 100644 --- a/plane-src/apps/api/plane/app/views/voice_tasker.py +++ b/plane-src/apps/api/plane/app/views/voice_tasker.py @@ -61,10 +61,14 @@ VOICE_TASK_INTENTS = {"create_task", "update_task", "delete_task", "unknown"} VOICE_TASK_PRIORITIES = {"none", "low", "medium", "high", "urgent"} VOICE_TASK_MEMORY_LIMIT = 5 VOICE_TASK_CONTEXT_LIMIT = 100 -VOICE_TASK_RATE_LIMIT_WINDOW_SECONDS = 60 * 60 +VOICE_TASK_RATE_LIMIT_HOURLY_WINDOW_SECONDS = 60 * 60 +VOICE_TASK_RATE_LIMIT_DAILY_WINDOW_SECONDS = 24 * 60 * 60 VOICE_TASK_RATE_LIMIT_ERROR_CODES = { "voice_task_user_hourly_limit_exceeded", "voice_task_workspace_hourly_limit_exceeded", + "voice_task_user_daily_limit_exceeded", + "voice_task_workspace_daily_limit_exceeded", + "voice_task_project_daily_limit_exceeded", } VOICE_TASK_PROJECT_MATCH_THRESHOLD = 0.8 VOICE_TASK_ASSIGNEE_MATCH_THRESHOLD = 0.8 @@ -314,84 +318,163 @@ def get_voice_task_preflight(workspace, user, project_id=None): return response -def get_voice_task_rate_limit_state(workspace, user, ai_settings, now=None): +def get_voice_task_quota_project(workspace, project_id=None, ai_settings=None): + if project_id: + project = Project.objects.filter( + id=project_id, + workspace=workspace, + archived_at__isnull=True, + ).first() + if project: + return project + + if ai_settings and ai_settings.default_project_id: + return Project.objects.filter( + id=ai_settings.default_project_id, + workspace=workspace, + archived_at__isnull=True, + ).first() + + return None + + +def get_voice_task_limit_sessions(workspace, window_seconds, now=None): now = now or timezone.now() - window_start = now - timedelta(seconds=VOICE_TASK_RATE_LIMIT_WINDOW_SECONDS) - sessions = VoiceTaskSession.objects.filter( + window_start = now - timedelta(seconds=window_seconds) + return VoiceTaskSession.objects.filter( workspace=workspace, created_at__gte=window_start, created_at__lte=now, ).exclude(error_code__in=VOICE_TASK_RATE_LIMIT_ERROR_CODES) - user_sessions = sessions.filter(user=user) - user_used = user_sessions.count() - workspace_used = sessions.count() - user_limit = max(int(ai_settings.per_user_hourly_limit or 0), 0) - workspace_limit = max(int(ai_settings.workspace_hourly_limit or 0), 0) + +def get_voice_task_rate_limit_state(workspace, user, project, ai_settings, now=None): + hourly_sessions = get_voice_task_limit_sessions( + workspace, + VOICE_TASK_RATE_LIMIT_HOURLY_WINDOW_SECONDS, + now=now, + ) + daily_sessions = get_voice_task_limit_sessions( + workspace, + VOICE_TASK_RATE_LIMIT_DAILY_WINDOW_SECONDS, + now=now, + ) + + user_hourly_used = hourly_sessions.filter(user=user).count() + workspace_hourly_used = hourly_sessions.count() + user_daily_used = daily_sessions.filter(user=user).count() + workspace_daily_used = daily_sessions.count() + project_daily_used = daily_sessions.filter(project=project).count() if project else 0 + + user_hourly_limit = max(int(ai_settings.per_user_hourly_limit or 0), 0) + workspace_hourly_limit = max(int(ai_settings.workspace_hourly_limit or 0), 0) + user_daily_limit = max(int(ai_settings.per_user_daily_limit or 0), 0) + workspace_daily_limit = max(int(ai_settings.workspace_daily_limit or 0), 0) + project_daily_limit = max(int(ai_settings.project_daily_limit or 0), 0) return { - "window_start": window_start, - "window_seconds": VOICE_TASK_RATE_LIMIT_WINDOW_SECONDS, - "user": { - "used": user_used, - "limit": user_limit, - "exceeded": bool(user_limit and user_used >= user_limit), + "user_hourly": { + "scope": "user", + "window": "hour", + "used": user_hourly_used, + "limit": user_hourly_limit, + "exceeded": bool(user_hourly_limit and user_hourly_used >= user_hourly_limit), + "window_seconds": VOICE_TASK_RATE_LIMIT_HOURLY_WINDOW_SECONDS, + "code": "voice_task_user_hourly_limit_exceeded", + "message": "Voice Tasker user hourly limit exceeded.", }, - "workspace": { - "used": workspace_used, - "limit": workspace_limit, - "exceeded": bool(workspace_limit and workspace_used >= workspace_limit), + "workspace_hourly": { + "scope": "workspace", + "window": "hour", + "used": workspace_hourly_used, + "limit": workspace_hourly_limit, + "exceeded": bool(workspace_hourly_limit and workspace_hourly_used >= workspace_hourly_limit), + "window_seconds": VOICE_TASK_RATE_LIMIT_HOURLY_WINDOW_SECONDS, + "code": "voice_task_workspace_hourly_limit_exceeded", + "message": "Voice Tasker workspace hourly limit exceeded.", + }, + "user_daily": { + "scope": "user", + "window": "day", + "used": user_daily_used, + "limit": user_daily_limit, + "exceeded": bool(user_daily_limit and user_daily_used >= user_daily_limit), + "window_seconds": VOICE_TASK_RATE_LIMIT_DAILY_WINDOW_SECONDS, + "code": "voice_task_user_daily_limit_exceeded", + "message": "Voice Tasker user daily limit exceeded.", + }, + "workspace_daily": { + "scope": "workspace", + "window": "day", + "used": workspace_daily_used, + "limit": workspace_daily_limit, + "exceeded": bool(workspace_daily_limit and workspace_daily_used >= workspace_daily_limit), + "window_seconds": VOICE_TASK_RATE_LIMIT_DAILY_WINDOW_SECONDS, + "code": "voice_task_workspace_daily_limit_exceeded", + "message": "Voice Tasker workspace daily limit exceeded.", + }, + "project_daily": { + "scope": "project", + "window": "day", + "used": project_daily_used, + "limit": project_daily_limit, + "exceeded": bool(project and project_daily_limit and project_daily_used >= project_daily_limit), + "window_seconds": VOICE_TASK_RATE_LIMIT_DAILY_WINDOW_SECONDS, + "code": "voice_task_project_daily_limit_exceeded", + "message": "Voice Tasker project daily limit exceeded.", + "project_id": str(project.id) if project else None, + "project_identifier": project.identifier if project else None, + "project_name": project.name if project else None, }, } -def get_voice_task_rate_limit_retry_after(workspace, user, scope, now=None): +def get_voice_task_rate_limit_retry_after(workspace, user, project, scope, window_seconds, now=None): now = now or timezone.now() - window_start = now - timedelta(seconds=VOICE_TASK_RATE_LIMIT_WINDOW_SECONDS) - sessions = VoiceTaskSession.objects.filter( - workspace=workspace, - created_at__gte=window_start, - created_at__lte=now, - ).exclude(error_code__in=VOICE_TASK_RATE_LIMIT_ERROR_CODES) + sessions = get_voice_task_limit_sessions(workspace, window_seconds, now=now) if scope == "user": sessions = sessions.filter(user=user) + elif scope == "project": + sessions = sessions.filter(project=project) oldest_session_at = sessions.order_by("created_at").values_list("created_at", flat=True).first() - reset_at = (oldest_session_at or now) + timedelta(seconds=VOICE_TASK_RATE_LIMIT_WINDOW_SECONDS) + reset_at = (oldest_session_at or now) + timedelta(seconds=window_seconds) retry_after = max(1, math.ceil((reset_at - now).total_seconds())) return retry_after, reset_at -def get_voice_task_rate_limit_error(workspace, user, ai_settings, now=None): +def get_voice_task_rate_limit_error(workspace, user, project, ai_settings, now=None): now = now or timezone.now() - state = get_voice_task_rate_limit_state(workspace, user, ai_settings, now=now) + state = get_voice_task_rate_limit_state(workspace, user, project, ai_settings, now=now) - if state["user"]["exceeded"]: - retry_after, reset_at = get_voice_task_rate_limit_retry_after(workspace, user, "user", now=now) + for key in ("user_hourly", "workspace_hourly", "user_daily", "workspace_daily", "project_daily"): + limit_state = state[key] + if not limit_state["exceeded"]: + continue + + retry_after, reset_at = get_voice_task_rate_limit_retry_after( + workspace, + user, + project, + limit_state["scope"], + limit_state["window_seconds"], + now=now, + ) return { - "code": "voice_task_user_hourly_limit_exceeded", - "message": "Voice Tasker user hourly limit exceeded.", - "scope": "user", - "limit": state["user"]["limit"], - "used": state["user"]["used"], - "window_seconds": state["window_seconds"], - "retry_after": retry_after, - "reset_at": reset_at, - } - - if state["workspace"]["exceeded"]: - retry_after, reset_at = get_voice_task_rate_limit_retry_after(workspace, user, "workspace", now=now) - return { - "code": "voice_task_workspace_hourly_limit_exceeded", - "message": "Voice Tasker workspace hourly limit exceeded.", - "scope": "workspace", - "limit": state["workspace"]["limit"], - "used": state["workspace"]["used"], - "window_seconds": state["window_seconds"], + "code": limit_state["code"], + "message": limit_state["message"], + "scope": limit_state["scope"], + "window": limit_state["window"], + "limit": limit_state["limit"], + "used": limit_state["used"], + "window_seconds": limit_state["window_seconds"], "retry_after": retry_after, "reset_at": reset_at, + "project_id": limit_state.get("project_id"), + "project_identifier": limit_state.get("project_identifier"), + "project_name": limit_state.get("project_name"), } return None @@ -405,10 +488,12 @@ def create_voice_task_rate_limit_session( duration_seconds, client_context, rate_limit_error, + project=None, ): return VoiceTaskSession.objects.create( workspace=workspace, user=user, + project=project, status=VoiceTaskSession.Status.FAILED, audio_duration_seconds=duration_seconds, audio_content_type=audio_content_type, @@ -2495,7 +2580,8 @@ class VoiceTaskParseEndpoint(BaseAPIView): @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def post(self, request, slug): workspace = Workspace.objects.get(slug=slug) - preflight = get_voice_task_preflight(workspace, request.user, project_id=get_request_project_id(request)) + request_project_id = get_request_project_id(request) + preflight = get_voice_task_preflight(workspace, request.user, project_id=request_project_id) if not preflight["available"]: response_status = ( @@ -2558,7 +2644,8 @@ class VoiceTaskParseEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - rate_limit_error = get_voice_task_rate_limit_error(workspace, request.user, ai_settings) + quota_project = get_voice_task_quota_project(workspace, project_id=request_project_id, ai_settings=ai_settings) + rate_limit_error = get_voice_task_rate_limit_error(workspace, request.user, quota_project, ai_settings) if rate_limit_error: voice_session = create_voice_task_rate_limit_session( workspace=workspace, @@ -2568,6 +2655,7 @@ class VoiceTaskParseEndpoint(BaseAPIView): duration_seconds=duration_seconds, client_context=client_context, rate_limit_error=rate_limit_error, + project=quota_project, ) return Response( { @@ -2576,11 +2664,15 @@ class VoiceTaskParseEndpoint(BaseAPIView): "code": rate_limit_error["code"], "error": rate_limit_error["message"], "limit_scope": rate_limit_error["scope"], + "limit_window": rate_limit_error["window"], "limit": rate_limit_error["limit"], "used": rate_limit_error["used"], "window_seconds": rate_limit_error["window_seconds"], "retry_after": rate_limit_error["retry_after"], "reset_at": rate_limit_error["reset_at"].isoformat(), + "project_id": rate_limit_error["project_id"], + "project_identifier": rate_limit_error["project_identifier"], + "project_name": rate_limit_error["project_name"], }, status=status.HTTP_429_TOO_MANY_REQUESTS, ) @@ -2588,6 +2680,7 @@ class VoiceTaskParseEndpoint(BaseAPIView): voice_session = VoiceTaskSession.objects.create( workspace=workspace, user=request.user, + project=quota_project, status=VoiceTaskSession.Status.UPLOADED, audio_duration_seconds=duration_seconds, audio_content_type=audio_content_type, @@ -2652,7 +2745,9 @@ class VoiceTaskParseEndpoint(BaseAPIView): voice_session.status = VoiceTaskSession.Status.PARSED voice_session.intent = parsed["intent"] voice_session.parsed_json = parsed - voice_session.save(update_fields=["status", "intent", "parsed_json", "updated_at"]) + if not voice_session.project_id and parsed.get("project_id"): + voice_session.project = get_voice_task_quota_project(workspace, project_id=parsed.get("project_id")) + voice_session.save(update_fields=["status", "intent", "parsed_json", "project", "updated_at"]) return Response( { @@ -2832,8 +2927,9 @@ class VoiceTaskCommitEndpoint(BaseAPIView): serializer.save() issue.refresh_from_db() voice_session.created_task = issue + voice_session.project = project voice_session.parsed_json = draft - voice_session.save(update_fields=["created_task", "parsed_json", "updated_at"]) + voice_session.save(update_fields=["created_task", "project", "parsed_json", "updated_at"]) issue_activity.delay( type="issue.activity.updated", @@ -2891,8 +2987,9 @@ class VoiceTaskCommitEndpoint(BaseAPIView): issue = serializer.save(created_by_id=request.user.id) voice_session.created_task = issue + voice_session.project = project voice_session.parsed_json = draft - voice_session.save(update_fields=["created_task", "parsed_json", "updated_at"]) + voice_session.save(update_fields=["created_task", "project", "parsed_json", "updated_at"]) requested_data = json.dumps(payload_without_project, cls=DjangoJSONEncoder) issue_activity.delay( @@ -2983,8 +3080,9 @@ class VoiceTaskCommitEndpoint(BaseAPIView): serializer.save() issue.refresh_from_db() voice_session.updated_task = issue + voice_session.project = project voice_session.parsed_json = draft - voice_session.save(update_fields=["updated_task", "parsed_json", "updated_at"]) + voice_session.save(update_fields=["updated_task", "project", "parsed_json", "updated_at"]) issue_activity.delay( type="issue.activity.updated", @@ -3037,8 +3135,9 @@ class VoiceTaskCommitEndpoint(BaseAPIView): entity_name="issue", ).delete(soft=False) voice_session.updated_task = issue + voice_session.project = project voice_session.parsed_json = draft - voice_session.save(update_fields=["updated_task", "parsed_json", "updated_at"]) + voice_session.save(update_fields=["updated_task", "project", "parsed_json", "updated_at"]) issue_activity.delay( type="issue.activity.deleted", diff --git a/plane-src/apps/api/plane/db/migrations/0131_voice_tasker_daily_project_limits.py b/plane-src/apps/api/plane/db/migrations/0131_voice_tasker_daily_project_limits.py new file mode 100644 index 0000000..cb4e9f8 --- /dev/null +++ b/plane-src/apps/api/plane/db/migrations/0131_voice_tasker_daily_project_limits.py @@ -0,0 +1,56 @@ +# Generated by Codex on 2026-04-28 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("db", "0130_project_storage_quotas"), + ] + + operations = [ + migrations.AlterField( + model_name="workspaceaicredential", + name="deleted_at", + field=models.DateTimeField(blank=True, null=True, verbose_name="Deleted At"), + ), + migrations.AlterField( + model_name="workspaceaisettings", + name="deleted_at", + field=models.DateTimeField(blank=True, null=True, verbose_name="Deleted At"), + ), + migrations.AddField( + model_name="workspaceaisettings", + name="per_user_daily_limit", + field=models.PositiveIntegerField(default=100), + ), + migrations.AddField( + model_name="workspaceaisettings", + name="workspace_daily_limit", + field=models.PositiveIntegerField(default=1000), + ), + migrations.AddField( + model_name="workspaceaisettings", + name="project_daily_limit", + field=models.PositiveIntegerField(default=300), + ), + migrations.AddField( + model_name="voicetasksession", + name="project", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="voice_task_sessions", + to="db.project", + ), + ), + migrations.AddIndex( + model_name="voicetasksession", + index=models.Index( + fields=["workspace", "project", "-created_at"], + name="voice_task_session_project_idx", + ), + ), + ] diff --git a/plane-src/apps/api/plane/db/models/voice_tasker.py b/plane-src/apps/api/plane/db/models/voice_tasker.py index 872547c..32f4e27 100644 --- a/plane-src/apps/api/plane/db/models/voice_tasker.py +++ b/plane-src/apps/api/plane/db/models/voice_tasker.py @@ -52,6 +52,9 @@ class WorkspaceAISettings(BaseModel): max_audio_duration_seconds = models.PositiveIntegerField(default=120) per_user_hourly_limit = models.PositiveIntegerField(default=30) workspace_hourly_limit = models.PositiveIntegerField(default=300) + per_user_daily_limit = models.PositiveIntegerField(default=100) + workspace_daily_limit = models.PositiveIntegerField(default=1000) + project_daily_limit = models.PositiveIntegerField(default=300) class Meta: verbose_name = "Workspace AI Settings" @@ -106,6 +109,13 @@ class VoiceTaskSession(BaseModel): on_delete=models.CASCADE, related_name="voice_task_sessions", ) + project = models.ForeignKey( + "db.Project", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="voice_task_sessions", + ) status = models.CharField(max_length=32, choices=Status.choices, default=Status.UPLOADED) audio_duration_seconds = models.FloatField(null=True, blank=True) audio_content_type = models.CharField(max_length=100, blank=True) @@ -138,6 +148,7 @@ class VoiceTaskSession(BaseModel): ordering = ("-created_at",) indexes = [ models.Index(fields=["workspace", "user", "-created_at"], name="voice_task_session_user_idx"), + models.Index(fields=["workspace", "project", "-created_at"], name="voice_task_session_project_idx"), models.Index(fields=["workspace", "status", "-created_at"], name="voice_task_session_status_idx"), ] diff --git a/plane-src/apps/web/core/components/voice-tasker/global-control.tsx b/plane-src/apps/web/core/components/voice-tasker/global-control.tsx index 77a3780..353220a 100644 --- a/plane-src/apps/web/core/components/voice-tasker/global-control.tsx +++ b/plane-src/apps/web/core/components/voice-tasker/global-control.tsx @@ -133,7 +133,9 @@ function getVoiceTaskErrorMessage(error: unknown, fallback: string) { const retryAfter = "retry_after" in error ? formatRetryAfter(error.retry_after) : "позже"; const used = "used" in error && typeof error.used === "number" ? error.used : null; const limit = "limit" in error && typeof error.limit === "number" ? error.limit : null; - const usageText = used !== null && limit !== null ? ` Использовано ${used} из ${limit} за последний час.` : ""; + const limitWindow = "limit_window" in error ? String(error.limit_window) : "hour"; + const windowText = limitWindow === "day" ? "за последние сутки" : "за последний час"; + const usageText = used !== null && limit !== null ? ` Использовано ${used} из ${limit} ${windowText}.` : ""; if (code === "voice_task_user_hourly_limit_exceeded") { return `Лимит Voice Tasker для пользователя исчерпан. Повторите ${retryAfter}.${usageText}`; @@ -143,6 +145,19 @@ function getVoiceTaskErrorMessage(error: unknown, fallback: string) { return `Лимит Voice Tasker для workspace исчерпан. Повторите ${retryAfter}.${usageText}`; } + if (code === "voice_task_user_daily_limit_exceeded") { + return `Суточный лимит Voice Tasker для пользователя исчерпан. Повторите ${retryAfter}.${usageText}`; + } + + if (code === "voice_task_workspace_daily_limit_exceeded") { + return `Суточный лимит Voice Tasker для workspace исчерпан. Повторите ${retryAfter}.${usageText}`; + } + + if (code === "voice_task_project_daily_limit_exceeded") { + const projectName = "project_name" in error && error.project_name ? ` для контура ${String(error.project_name)}` : ""; + return `Суточный лимит Voice Tasker${projectName} исчерпан. Повторите ${retryAfter}.${usageText}`; + } + if ("error" in error && error.error) return String(error.error); } diff --git a/plane-src/apps/web/core/components/workspace/settings/ai-voice-tasker-settings.tsx b/plane-src/apps/web/core/components/workspace/settings/ai-voice-tasker-settings.tsx index 301991c..8c5051b 100644 --- a/plane-src/apps/web/core/components/workspace/settings/ai-voice-tasker-settings.tsx +++ b/plane-src/apps/web/core/components/workspace/settings/ai-voice-tasker-settings.tsx @@ -46,6 +46,9 @@ type TFormState = { max_audio_duration_seconds: number; per_user_hourly_limit: number; workspace_hourly_limit: number; + per_user_daily_limit: number; + workspace_daily_limit: number; + project_daily_limit: number; openai_api_key: string; }; @@ -65,6 +68,9 @@ const getInitialFormState = (settings?: TWorkspaceAISettings): TFormState => ({ max_audio_duration_seconds: settings?.max_audio_duration_seconds ?? 120, per_user_hourly_limit: settings?.per_user_hourly_limit ?? 30, workspace_hourly_limit: settings?.workspace_hourly_limit ?? 300, + per_user_daily_limit: settings?.per_user_daily_limit ?? 100, + workspace_daily_limit: settings?.workspace_daily_limit ?? 1000, + project_daily_limit: settings?.project_daily_limit ?? 300, openai_api_key: "", }); @@ -184,6 +190,9 @@ export const AIVoiceTaskerSettingsContent = observer(function AIVoiceTaskerSetti max_audio_duration_seconds: formState.max_audio_duration_seconds, per_user_hourly_limit: formState.per_user_hourly_limit, workspace_hourly_limit: formState.workspace_hourly_limit, + per_user_daily_limit: formState.per_user_daily_limit, + workspace_daily_limit: formState.workspace_daily_limit, + project_daily_limit: formState.project_daily_limit, }; if (formState.openai_api_key.trim()) payload.openai_api_key = formState.openai_api_key.trim(); @@ -339,7 +348,7 @@ export const AIVoiceTaskerSettingsContent = observer(function AIVoiceTaskerSetti
@@ -356,24 +365,51 @@ export const AIVoiceTaskerSettingsContent = observer(function AIVoiceTaskerSetti className="nodedc-settings-input w-full" /> - + updateFormValue("per_user_hourly_limit", value)} /> - + updateFormValue("workspace_hourly_limit", value)} /> + + updateFormValue("per_user_daily_limit", value)} + /> + + + updateFormValue("workspace_daily_limit", value)} + /> + + + updateFormValue("project_daily_limit", value)} + /> +
diff --git a/plane-src/packages/types/src/ai.ts b/plane-src/packages/types/src/ai.ts index b8db04c..3d54723 100644 --- a/plane-src/packages/types/src/ai.ts +++ b/plane-src/packages/types/src/ai.ts @@ -43,6 +43,9 @@ export type TWorkspaceAISettings = { max_audio_duration_seconds: number; per_user_hourly_limit: number; workspace_hourly_limit: number; + per_user_daily_limit: number; + workspace_daily_limit: number; + project_daily_limit: number; credential: TWorkspaceAICredentialStatus; created_at: string; updated_at: string; @@ -61,6 +64,9 @@ export type TWorkspaceAISettingsPayload = Partial< | "max_audio_duration_seconds" | "per_user_hourly_limit" | "workspace_hourly_limit" + | "per_user_daily_limit" + | "workspace_daily_limit" + | "project_daily_limit" > > & { openai_api_key?: string; @@ -192,12 +198,16 @@ export type TVoiceTaskUploadResult = { size: number; }; client_context?: Record; - limit_scope?: "user" | "workspace"; + limit_scope?: "user" | "workspace" | "project"; + limit_window?: "hour" | "day"; limit?: number; used?: number; window_seconds?: number; retry_after?: number; reset_at?: string; + project_id?: string | null; + project_identifier?: string | null; + project_name?: string | null; code?: string; error?: string; };