fix: idempotency guard for agent retries (#402) + pagination for list tools (#388)#419
Open
originaljayeshsharma wants to merge 1 commit into
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix #402 & #388 — Idempotency guard + pagination for list/search tools
Summary
This PR fixes two open bugs in the Python
stripe_agent_toolkit:Fix #402 — Idempotency guard against duplicate charges
Root cause
The Stripe SDK generates idempotency keys automatically for network-level retries within a single session. But when an agent framework retries a tool call as a new invocation (after a timeout, crash, or model loop) a fresh session starts with a new key, and a second charge is created.
Solution
New module
stripe_agent_toolkit/idempotency.pyderives a stable, deterministic idempotency key fromSHA-256(tool_name + sorted_args_json)before every mutating call:Every mutating API wrapper in
api.pynow callswith_idempotency()and forwards the key inoptions={"idempotency_key": ...}:Because the key is deterministic, a retry of the same logical call with the same arguments sends the same key to Stripe, which returns the original receipt without creating a second charge (Stripe deduplicates for 24 h).
Read-only tools (
list_*,retrieve_*) are excluded — Stripe rejects idempotency keys on GET requests.Affected tools (mutating)
create_customer,create_product,create_price,create_payment_link,create_payment_intent,create_refund,create_invoice,create_invoice_item,finalize_invoice,create_subscription,cancel_subscription,update_subscription,create_couponFix #388 — Pagination (
starting_after/ending_before)Root cause
All list tools silently capped results at 100 with no way to fetch subsequent pages. The
starting_aftercursor accepted by the underlying Stripe REST API was never exposed.Solution
starting_afterandending_beforeoptional parameters are now wired through to the Stripe SDK call for every list tool:For
search_stripe_resources, Stripe's Search API usespage(notstarting_after) as the cursor parameter. We acceptstarting_afterfor consistency and forward it internally aspage.Affected tools
list_subscriptions,list_products,list_prices,list_customers,list_invoices,list_coupons,list_payment_links,search_stripe_resourcesExample — paginating through all active subscriptions
Files changed
Test results
Backwards compatibility
- All changes are additive. No existing call signatures change.
- Callers that don't pass
- The idempotency key is injected transparently; callers that already pass
their own
# Fix #402 & #388 — Idempotency guard + pagination for list/search toolsstarting_after/ending_beforebehave identically to before.idempotency_keyinargswill have it preserved (it will override the generated key becausewith_idempotencyspreadsargslast).Summary
This PR fixes two open bugs in the Python
stripe_agent_toolkit:bugstarting_after) across all list and search toolsFix #402 — Idempotency guard against duplicate charges
Root cause
The Stripe SDK generates idempotency keys automatically for network-level
retries within a single session. But when an agent framework retries a
tool call as a new invocation (after a timeout, crash, or model loop) a
fresh session starts with a new key, and a second charge is created.
Solution
New module
stripe_agent_toolkit/idempotency.pyderives a stable,deterministic idempotency key from
SHA-256(tool_name + sorted_args_json)before every mutating call:
Every mutating API wrapper in
api.pynow callswith_idempotency()andforwards the key in
options={"idempotency_key": ...}:Because the key is deterministic, a retry of the same logical call with the
same arguments sends the same key to Stripe, which returns the original
receipt without creating a second charge (Stripe deduplicates for 24 h).
Read-only tools (
list_*,retrieve_*) are excluded — Stripe rejectsidempotency keys on GET requests.
Affected tools (mutating)
create_customer,create_product,create_price,create_payment_link,create_payment_intent,create_refund,create_invoice,create_invoice_item,finalize_invoice,create_subscription,cancel_subscription,update_subscription,create_couponFix #388 — Pagination (
starting_after/ending_before)Root cause
All list tools silently capped results at 100 with no way to fetch subsequent
pages. The
starting_aftercursor accepted by the underlying Stripe REST APIwas never exposed.
Solution
starting_afterandending_beforeoptional parameters are now wired throughto the Stripe SDK call for every list tool:
For
search_stripe_resources, Stripe's Search API usespage(notstarting_after) as the cursor parameter. We acceptstarting_afterforconsistency and forward it internally as
page.Affected tools
list_subscriptions,list_products,list_prices,list_customers,list_invoices,list_coupons,list_payment_links,search_stripe_resourcesExample — paginating through all active subscriptions
Files changed
Test results
Backwards compatibility
starting_after/ending_beforebehaveidentically to before.
their own
idempotency_keyinargswill have it preserved (it willoverride the generated key because
with_idempotencyspreadsargslast).