fix(quotas): keyless agent chat bucket, keep disabled policies disabled, cached rates

- check_usage treats a request through a keyless (draft) agent as agent
  traffic, matching how its usage rows are bucketed and the headless rule
- dashboard edits carry the stored enabled flag instead of re-enabling the
  policy; disabled policies are labelled in the Quotas tab and the editor
- quota 429s send x-should-retry: false so OpenAI SDK clients do not retry
  a refusal that cannot succeed before the reset
- cached-input and cache-write rates for Anthropic, OpenRouter and Groq
  gpt-oss-120b; refresh OpenRouter deepseek-v3.2 list prices
- UsageQuota reuses usagePercent; docs note that a user override needs an
  existing user
This commit is contained in:
Alex committed 2026-09-21 15:26:29 +01:00
1 parent b5296df8a9
commit 1e14605ee7
15 files changed
+87 -22

No files matched your search

+3 -1
View File
@@ -132,7 +132,9 @@ class AnswerResource(Resource, BaseAnswerResource):
return make_response({"error": "Unauthorized"}, 401)
if error := self.check_usage(
processor.agent_config, processor.decoded_token
processor.agent_config,
processor.decoded_token,
agent_id=processor.agent_id,
):
return error
+10 -3
View File
@@ -115,7 +115,10 @@ class BaseAnswerResource:
return prepared
def check_usage(
self, agent_config: Dict, decoded_token: Optional[Dict] = None
self,
agent_config: Dict,
decoded_token: Optional[Dict] = None,
agent_id: Optional[str] = None,
) -> Optional[Response]:
"""Refuse the request when a usage limit is exhausted.
@@ -127,6 +130,8 @@ class BaseAnswerResource:
agent_config: The config dict of agent instance
decoded_token: The request's resolved identity; its ``sub`` is the
billable user.
agent_id: The agent the request runs through. A draft agent has no
key, but its usage rows carry the agent id, so it is agent traffic.
Returns:
None or Response if either of limits exceeded.
@@ -134,7 +139,7 @@ class BaseAnswerResource:
"""
api_key = agent_config.get("user_api_key")
user_id = (decoded_token or {}).get("sub") or agent_config.get("user_id")
exceeded = QuotaService.check(user_id, "agent" if api_key else "direct")
exceeded = QuotaService.check(user_id, "agent" if api_key or agent_id else "direct")
if exceeded is not None:
return quota_exceeded_response(exceeded)
if not api_key:
@@ -227,7 +232,9 @@ class BaseAnswerResource:
Returns:
None, or the refusal Response.
"""
error = self.check_usage(processor.agent_config, processor.decoded_token)
error = self.check_usage(
processor.agent_config, processor.decoded_token, agent_id=processor.agent_id
)
if error is None or not conversation_id:
return error
user = processor.initial_user_id or (processor.decoded_token or {}).get("sub")
+3 -1
View File
@@ -154,7 +154,9 @@ class StreamResource(Resource, BaseAnswerResource):
)
if error := self.check_usage(
processor.agent_config, processor.decoded_token
processor.agent_config,
processor.decoded_token,
agent_id=processor.agent_id,
):
return error
should_persist, visibility = resolve_persistence(
+3 -1
View File
@@ -344,7 +344,9 @@ def chat_completions():
if claimed_conversation_id:
usage_error = helper.check_usage_on_resume(processor, claimed_conversation_id)
else:
usage_error = helper.check_usage(processor.agent_config, processor.decoded_token)
usage_error = helper.check_usage(
processor.agent_config, processor.decoded_token, agent_id=processor.agent_id
)
if usage_error:
return usage_error