Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cbdde3a
feat(seer): Gate agent chat messages on Seer credits
srest2021 Nov 6, 2026
c247328
feat(seer): Check Seer credits before Autofix steps and Open PR
srest2021 Nov 6, 2026
e2855ba
ref(seer): Keep the token-billed check inside can_run_seer_operation
srest2021 Nov 6, 2026
33f5287
Merge branch 'srest2021/CW-3061-agent-chat' into srest2021/CW-3061-au…
srest2021 Nov 6, 2026
d70d6cb
ref(seer): Split has_enough_seer_credits out of can_run_seer_operation
srest2021 Nov 6, 2026
58a1b63
ref(seer): Combine the credit check flag conditions
srest2021 Nov 6, 2026
8856cd4
Merge branch 'srest2021/CW-3061-agent-chat' into srest2021/CW-3061-au…
srest2021 Nov 6, 2026
c49f7ed
Merge remote-tracking branch 'origin/master' into srest2021/CW-3061-a…
srest2021 Nov 6, 2026
8e5ca8f
Merge branch 'srest2021/CW-3061-agent-chat' into srest2021/CW-3061-au…
srest2021 Nov 6, 2026
0ab3b10
feat(seer): Check Seer credits before replying to an Autofix single run
srest2021 Nov 6, 2026
69f5488
Merge remote-tracking branch 'origin/master' into srest2021/CW-3061-a…
srest2021 Nov 6, 2026
0f648e8
Merge branch 'srest2021/CW-3061-agent-chat' into srest2021/CW-3061-au…
srest2021 Nov 6, 2026
a3dae0d
ref(seer): Check the credit check flag first
srest2021 Nov 6, 2026
f738ddf
Merge branch 'srest2021/CW-3061-agent-chat' into srest2021/CW-3061-au…
srest2021 Nov 6, 2026
94b979c
test(seer): Align credit check tests with their files
srest2021 Nov 6, 2026
302de4f
Merge branch 'srest2021/CW-3061-agent-chat' into srest2021/CW-3061-au…
srest2021 Nov 6, 2026
0ec42da
Merge remote-tracking branch 'origin/master' into srest2021/CW-3061-a…
srest2021 Nov 6, 2026
6f7cb6f
ref(seer): Leave single run replies out of credit gating
srest2021 Nov 6, 2026
99eb2ac
fix(seer): Don't let API callers claim the agentic triage referrer
srest2021 Nov 6, 2026
e8f415e
ref(seer): Clarify why the endpoint ignores the agentic triage referrer
srest2021 Nov 6, 2026
396779d
feat(seer): Check Seer credits before replying to an Autofix single run
srest2021 Nov 6, 2026
90c5db5
cleaning up diff
srest2021 Nov 6, 2026
ff4faae
ref(seer): Restore the billing comment in trigger_autofix_agent
srest2021 Nov 6, 2026
b86335d
cleaning up
srest2021 Nov 6, 2026
245aca9
fix ordering
srest2021 Nov 6, 2026
80bba23
Restore comment
srest2021 Nov 6, 2026
914a60c
ref(seer): Keep all billing skip rules in check_autofix_billing
srest2021 Nov 6, 2026
28e5990
refactor billing helper
srest2021 Nov 6, 2026
766bc10
move logs out of helper
srest2021 Nov 6, 2026
6c0043d
small test fix:
srest2021 Nov 6, 2026
1c7c190
cleaning up tests
srest2021 Nov 6, 2026
4e289d3
Merge remote-tracking branch 'origin/master' into srest2021/CW-3061-a…
srest2021 Nov 7, 2026
451f3bd
ref(seer-billing): Pass organization to check_autofix_billing
sentry-junior[bot] Nov 9, 2026
ab0d0aa
ref(seer-billing): Check Autofix billing only in dispatch
srest2021 Nov 9, 2026
89bdfdc
ref(seer-billing): Drop the agentic triage referrer exemption
srest2021 Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 13 additions & 18 deletions src/sentry/seer/autofix/autofix_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from scm.types import GetBranchProtocol, GetRepositoryProtocol
from sentry_sdk import traces

from sentry import features, quotas
from sentry import features
from sentry.analytics.events.autofix_events import (
AiAutofixAgentHandoffEvent,
AiAutofixCodeChangesCompletedEvent,
Expand All @@ -23,18 +23,19 @@
AiAutofixSolutionCompletedEvent,
AiAutofixSolutionStartedEvent,
)
from sentry.constants import ENABLE_SEER_CODING_DEFAULT, DataCategory
from sentry.constants import ENABLE_SEER_CODING_DEFAULT
from sentry.integrations.services.integration import integration_service
from sentry.models.group import Group
from sentry.seer.agent.client import SeerAgentClient
from sentry.seer.agent.client_models import SeerRunState
from sentry.seer.autofix.analytics import record_autofix_event
from sentry.seer.autofix.commit_author import SeerCommitAuthor
from sentry.seer.autofix.constants import AutofixReferrer
from sentry.seer.autofix.exceptions import NoSeerQuotaException
from sentry.seer.autofix.exceptions import NoSeerQuotaException, SeerOutOfCreditsException
from sentry.seer.autofix.feature.dispatch import (
AutofixFeatureArgs,
autofix_run_targets_other_issue,
check_autofix_billing,
require_autofix_run_for_group,
trigger_autofix_feature,
)
Expand Down Expand Up @@ -63,6 +64,7 @@
is_free_cohort_org,
read_preference_from_sentry_db,
)
from sentry.seer.credits import SeerOperation
from sentry.seer.entrypoints.operator import (
SeerActivityAttribution,
SeerAutofixOperator,
Expand Down Expand Up @@ -461,20 +463,6 @@ def trigger_autofix_agent(
"""
outcome = "success"
try:
# check billing quota for triggering a new autofix run
# Free cohort orgs bypass quota only when called from agentic triage
# (allow_free_cohort=True). The API endpoint never sets this flag,
# so manual triggers still require quota.
if run_id is None:
skip_quota_check = allow_free_cohort and is_free_cohort_org(group.organization)
if not skip_quota_check:
has_budget: bool = quotas.backend.check_seer_quota(
org_id=group.organization.id,
data_category=DataCategory.SEER_AUTOFIX,
)
if not has_budget:
raise NoSeerQuotaException()

if run_id is not None:
_assert_existing_run_belongs_to_group(group, run_id)

Expand Down Expand Up @@ -548,7 +536,12 @@ def trigger_autofix_agent(
actor_user_id=actor_user_id,
)
return feature_run
except (NoSeerQuotaException, SeerPermissionError, PrIterationNoPullRequestException):
except (
NoSeerQuotaException,
SeerOutOfCreditsException,
SeerPermissionError,
PrIterationNoPullRequestException,
):
outcome = "declined"
raise
except BaseException:
Expand Down Expand Up @@ -839,6 +832,8 @@ def trigger_push_changes(
):
raise PermissionDenied("Code generation is disabled for this organization")

check_autofix_billing(group.organization, SeerOperation.AUTOFIX_OPEN_PR, is_new_run=False)

client = get_autofix_agent_client(group, user=user)

if state is None:
Expand Down
4 changes: 4 additions & 0 deletions src/sentry/seer/autofix/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ class NoSeerQuotaException(Exception):
"""Raised when an Autofix run cannot consume Seer quota."""


class SeerOutOfCreditsException(Exception):
"""Raised when the organization doesn't have enough Seer credits for an Autofix operation."""


class IssueSummaryUnavailable(Exception):
"""Raised when an issue summary cannot be generated for an expected reason."""

Expand Down
91 changes: 72 additions & 19 deletions src/sentry/seer/autofix/feature/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
DataCategory,
)
from sentry.models.group import Group
from sentry.models.organization import Organization
from sentry.models.project import Project
from sentry.seer.agent.client import SeerAgentClient
from sentry.seer.agent.client_utils import (
Expand All @@ -22,7 +23,7 @@
)
from sentry.seer.agent.on_completion_hook import extract_hook_definition
from sentry.seer.autofix.constants import AutofixReferrer
from sentry.seer.autofix.exceptions import NoSeerQuotaException
from sentry.seer.autofix.exceptions import NoSeerQuotaException, SeerOutOfCreditsException
from sentry.seer.autofix.feature.models import (
FEATURE_ID,
LEGACY_FEATURE_ID,
Expand All @@ -41,6 +42,7 @@
get_automation_handoff,
is_free_cohort_org,
)
from sentry.seer.credits import SeerOperation, has_enough_seer_credits
from sentry.seer.models.run import SeerAgentRun, SeerRun
from sentry.seer.models.seer_api_models import UNKNOWN_RUN_ID_FOR_GROUP, SeerPermissionError
from sentry.users.models.user import User
Expand Down Expand Up @@ -148,6 +150,39 @@ def _org_allows_coding(group: Group) -> bool:
)


def check_autofix_billing(
organization: Organization,
operation: SeerOperation,
*,
is_new_run: bool,
skip_quota_for_free_cohort: bool = False,
) -> bool:
"""
Raise if the organization can't pay for the operation. Token-billed orgs are
checked for credits, and everyone else is checked for quota once per run.
Returns whether the operation uses quota.
"""
# Check credits for token-billed orgs.
if features.has("organizations:token-billed-seer-enabled", organization):
if features.has(
"organizations:seer-credit-checks", organization
) and not has_enough_seer_credits(organization, operation):
raise SeerOutOfCreditsException()
return False
Comment thread
srest2021 marked this conversation as resolved.
Resolved

# Check quota for non-token-billed orgs once per run.
# Free cohort orgs skip quota when the caller passes skip_quota_for_free_cohort=True.
# Only agentic triage does, and it is not exposed via API.
skips_quota = skip_quota_for_free_cohort and is_free_cohort_org(organization)
uses_quota = is_new_run and not skips_quota
if uses_quota and not quotas.backend.check_seer_quota(
org_id=organization.id,
data_category=DataCategory.SEER_AUTOFIX,
):
raise NoSeerQuotaException()
return uses_quota


def continue_single_run(
agent_run: SeerAgentRun,
group: Group,
Expand All @@ -159,6 +194,12 @@ def continue_single_run(
) -> SeerRun:
from sentry.seer.autofix.on_completion_hook import AutofixOnCompletionHook

check_autofix_billing(
group.organization,
SeerOperation.AUTOFIX_SINGLE_RUN_REPLY,
Comment thread
srest2021 marked this conversation as resolved.
Resolved
is_new_run=False,
)
Comment on lines +197 to +201

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-assisted review, posted on behalf of @billyvg.

Should a user's reply to an agentic triage run skip the credit check? continue_single_run only runs when a user replies in Seer chat. agent_run.run.referrer is the run's original referrer, though, and SeerRun.referrer is set when the run starts and never updated. So every reply on a run that agentic triage started skips the check. A reply can write code and open a PR, which means a user on a token-billed org with no credits left can keep driving an agentic triage run indefinitely.

Continuing a step-based agentic triage run from the issue page does get checked, because trigger_autofix_agent passes the caller's referrer. If only agentic triage's own automated work is meant to be free, passing the reply's referrer here would match that path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in #130552


enable_coding = _org_allows_coding(group)
open_pr = enable_coding and can_publish
payload = AutofixSingleRunFeaturePayload(
Expand Down Expand Up @@ -209,24 +250,36 @@ def trigger_autofix_feature(
from sentry.seer.autofix.on_completion_hook import AutofixOnCompletionHook

is_new_run = args.existing_run_id is None
# Free cohort orgs bypass quota only when called from agentic triage
# (allow_free_cohort=True). Not exposed via the API.
skip_quota = is_new_run and args.allow_free_cohort and is_free_cohort_org(group.organization)
if is_new_run and not skip_quota:
has_budget: bool = quotas.backend.check_seer_quota(
org_id=group.organization.id,
data_category=DataCategory.SEER_AUTOFIX,
try:
uses_quota = check_autofix_billing(
group.organization,
args.step.seer_operation,
is_new_run=is_new_run,
# Free cohort orgs bypass quota only when called from agentic triage
# (allow_free_cohort=True). Not exposed via the API.
skip_quota_for_free_cohort=args.allow_free_cohort,
)
except NoSeerQuotaException:
logger.warning(
"autofix_feature.dispatch.quota_denied",
extra={
"group_id": group.id,
"organization_id": group.organization.id,
"referrer": args.referrer.value,
},
)
raise
except SeerOutOfCreditsException:
logger.info(
"autofix_feature.dispatch.out_of_credits",
extra={
"group_id": group.id,
"organization_id": group.organization.id,
"operation": args.step.seer_operation.value,
"referrer": args.referrer.value,
},
)
if not has_budget:
logger.warning(
"autofix_feature.dispatch.quota_denied",
extra={
"group_id": group.id,
"organization_id": group.organization.id,
"referrer": args.referrer.value,
},
)
raise NoSeerQuotaException()
raise

single_run = should_use_single_run(
group,
Expand Down Expand Up @@ -342,7 +395,7 @@ def trigger_autofix_feature(
else:
raise Exception("Unhandled run_id branch, this should never happen")

if is_new_run and not skip_quota:
if uses_quota:
quotas.backend.record_seer_run(
group.organization.id, group.project.id, DataCategory.SEER_AUTOFIX
)
Expand Down
14 changes: 14 additions & 0 deletions src/sentry/seer/autofix/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from enum import StrEnum

from sentry.seer.autofix.utils import AutofixStoppingPoint
from sentry.seer.credits import SeerOperation


class AutofixStep(StrEnum):
Expand All @@ -13,6 +14,19 @@ class AutofixStep(StrEnum):
CODE_CHANGES = "code_changes"
PR_ITERATION = "pr_iteration"

@property
def seer_operation(self) -> SeerOperation:
"""The Seer credit check operation corresponding to this step."""
match self:
case AutofixStep.ROOT_CAUSE:
return SeerOperation.AUTOFIX_ROOT_CAUSE
case AutofixStep.SOLUTION:
return SeerOperation.AUTOFIX_SOLUTION
case AutofixStep.CODE_CHANGES:
return SeerOperation.AUTOFIX_CODE_CHANGES
case AutofixStep.PR_ITERATION:
return SeerOperation.AUTOFIX_PR_ITERATION

@staticmethod
def from_autofix_stopping_point(
autofix_stopping_point: AutofixStoppingPoint,
Expand Down
25 changes: 18 additions & 7 deletions src/sentry/seer/credits.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class SeerOperation(StrEnum):
AUTOFIX_CODE_CHANGES = "autofix_code_changes"
AUTOFIX_OPEN_PR = "autofix_open_pr"
AUTOFIX_PR_ITERATION = "autofix_pr_iteration"
AUTOFIX_SINGLE_RUN_REPLY = "autofix_single_run_reply"


class SeerFeature(StrEnum):
Expand Down Expand Up @@ -57,16 +58,17 @@ class SeerOperationConfig:
SeerOperation.AUTOFIX_PR_ITERATION: SeerOperationConfig(
SeerFeature.AUTOFIX, lambda: PLACEHOLDER_CREDIT_ESTIMATE
),
SeerOperation.AUTOFIX_SINGLE_RUN_REPLY: SeerOperationConfig(
SeerFeature.AUTOFIX, lambda: PLACEHOLDER_CREDIT_ESTIMATE
),
}


def can_run_seer_operation(organization: Organization, operation: SeerOperation) -> bool:
"""Whether the organization is token-billed and has enough Seer credits to run the operation."""
if not features.has("organizations:seer-credit-checks", organization) or not features.has(
"organizations:token-billed-seer-enabled", organization
):
return True

def has_enough_seer_credits(organization: Organization, operation: SeerOperation) -> bool:
"""
Checks whether the organization has enough Seer credits to run the operation,
for callers that already checked the credit check and token-billed flags.
"""
config = SEER_OPERATIONS[operation]
response = seer_credit.backend.check_seer_credits(
CheckSeerCreditsRequest(
Expand All @@ -77,3 +79,12 @@ def can_run_seer_operation(organization: Organization, operation: SeerOperation)
)
)
return response.has_enough_credits


def can_run_seer_operation(organization: Organization, operation: SeerOperation) -> bool:
"""Checks whether the organization is token-billed and has enough Seer credits to run the operation."""
if not features.has("organizations:seer-credit-checks", organization) or not features.has(
"organizations:token-billed-seer-enabled", organization
):
return True
return has_enough_seer_credits(organization, operation)
Loading
Loading