feat(PAB-21): Idempotency-Key on chat completions + request_dedup table#97
Conversation
Add request_dedup table with uniqueIndex(userId, idempotencyKey) for idempotent chat completion retries. Generate stable requestId via hash of userId:idempotencyKey, preventing double-charge via credit_ledger's existing uniqueIndex on (userId, reason, refId). - HandleDedup: INSERT ON CONFLICT DO NOTHING; replay completed non-streaming; return 409 for streaming/in-flight; reclaim stale in_flight rows (>5min) - Gate behind NEVERMIND_KILL_SWITCHES 'idempotency_dedup' kill switch - 6 new unit tests + smoke-idempotency.sh integration script - Credit ledger insert uses onConflictDoNothing as safety net Ref: PAB-21 Co-authored-by: multica-agent <github@multica.ai>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
| if (existing.status === 'in_flight') { | ||
| const createdAt = new Date(existing.createdAt).getTime(); | ||
| if (Date.now() - createdAt > DEDUP_STALE_MS) { | ||
| await db.update(requestDedup).set({ | ||
| status: 'in_flight', | ||
| requestId, | ||
| createdAt: new Date(), | ||
| requestHash: null, | ||
| responseJson: null, | ||
| responseHeaders: null, | ||
| upstreamStatus: null, | ||
| completedAt: null, | ||
| }).where(eq(requestDedup.id, existing.id)); | ||
| return undefined; | ||
| } |
There was a problem hiding this comment.
Race condition in backend/src/lib/proxy.ts:212-223 allows two stale retries to reclaim the same requestDedup row because the UPDATE only filters on id, so both requests can pass Date.now() - createdAt > DEDUP_STALE_MS, call the upstream, and recordUsage. Guard the reclaim with status = 'in_flight' and fail the second winner with a 409 idempotency_conflict to preserve the dedup guarantee.
if (Date.now() - createdAt > DEDUP_STALE_MS) {
const [reclaimed] = await db.update(requestDedup).set({
status: 'in_flight',
requestId,
createdAt: new Date(),
requestHash: null, responseJson: null, responseHeaders: null, upstreamStatus: null, completedAt: null,
}).where(and(
eq(requestDedup.id, existing.id),
eq(requestDedup.status, 'in_flight'),
)).returning({ id: requestDedup.id });
if (!reclaimed) return withRequestId(Response.json({ error: { type: 'idempotency_conflict', message: 'Request already in progress' } }, { status: 409 }), requestId);
return undefined;
}Prompt for LLM
File backend/src/lib/proxy.ts:
Line 210 to 224:
Race condition in backend/src/lib/proxy.ts:212-223 allows two stale retries to reclaim the same `requestDedup` row because the `UPDATE` only filters on `id`, so both requests can pass `Date.now() - createdAt > DEDUP_STALE_MS`, call the upstream, and recordUsage. Guard the reclaim with `status = 'in_flight'` and fail the second winner with a 409 `idempotency_conflict` to preserve the dedup guarantee.
Suggested Code:
if (Date.now() - createdAt > DEDUP_STALE_MS) {
const [reclaimed] = await db.update(requestDedup).set({
status: 'in_flight',
requestId,
createdAt: new Date(),
requestHash: null, responseJson: null, responseHeaders: null, upstreamStatus: null, completedAt: null,
}).where(and(
eq(requestDedup.id, existing.id),
eq(requestDedup.status, 'in_flight'),
)).returning({ id: requestDedup.id });
if (!reclaimed) return withRequestId(Response.json({ error: { type: 'idempotency_conflict', message: 'Request already in progress' } }, { status: 409 }), requestId);
return undefined;
}
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
Summary
This PR implements idempotency support for the chat completions API endpoint, allowing clients to safely retry requests without risk of duplicate processing or billing.
Key Changes
Idempotency-Key Header Support:
/api/v1/chat/completionsendpoint now reads anIdempotency-Keyheader from incoming requests and passes it through to the proxy pipeline.Request Deduplication Table:
request_deduptable stores deduplication state per user and idempotency key, with a unique index on(user_id, idempotency_key). It tracks request status (in_flight,completed,failed), cached response data, headers, and timestamps.Deduplication Logic (
handleDedup):in_flightrow and proceeds normally with the upstream call.409 Conflictwithidempotency_conflicterror, indicating the request is already being processed.in_flight) and proceeds with the request.409since streaming responses cannot be replayed.Deterministic Request IDs:
user_id:idempotency_key, ensuring replayed responses carry the samex-request-id.Post-Response Persistence:
completedwith the response JSON, headers, and upstream status.completedbut no response body is stored (replay not supported).Kill Switch:
idempotency_dedupbackend kill switch.Testing:
smoke-idempotency.sh) sends two identical requests with the sameIdempotency-Keyand asserts both return 200 with the samex-request-id.