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