-
Notifications
You must be signed in to change notification settings - Fork 46
[Release] Hotfix - OpenRouter auto negative pricing #564
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 41 commits
e30228f
e53edfe
27dbd15
89b3451
3278e3f
af978d0
54ecfe0
7c6a514
b4f7f93
0ec1dc2
56b1dc0
e799344
8b1b227
b3998c5
0cded6c
293fd59
63a38e4
2ac0348
c1ae0cf
9b98dc1
3fd58d0
6facf82
34d7c38
949ea98
b0a9890
a2aec60
8392323
aa1f762
05e59cf
30cc7a9
926aff1
6d16752
c610ce6
afdaba6
600d6aa
447a30a
22150b4
5d63e2f
877125d
4b1f540
d5b2b05
c0056c0
d7b3728
208135b
f27762a
793861c
2eb0e31
08c29d9
1b6daa1
d98d37f
cfd90d6
f6dc1b5
ee90834
17c237c
28d9751
5dab845
21cd217
3719252
46fc77b
1847e34
813fc82
f2202a8
395d070
9584440
ca3ea92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| name: Provider Smoke Tests | ||
|
|
||
| on: | ||
| push: | ||
| branches: [master] | ||
| paths: | ||
| - 'packages/tests/provider-smoke/**' | ||
| - 'packages/sdk/ts/**' | ||
| - 'packages/app/server/**' | ||
| - 'packages/app/control/**' | ||
| - '.github/workflows/provider-smoke-tests.yml' | ||
|
|
||
| jobs: | ||
| provider-smoke-tests: | ||
| name: Run Provider Smoke Tests | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 40 | ||
|
|
||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Install pnpm | ||
| run: npm install -g pnpm | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| cache: 'pnpm' | ||
|
|
||
| - name: Install workspace dependencies | ||
| run: pnpm install --frozen-lockfile | ||
|
|
||
| - name: Wait for Railway Deployment | ||
| env: | ||
| RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} | ||
| run: | | ||
| echo "Installing Railway CLI..." | ||
| npm install -g @railway/cli | ||
|
|
||
| echo "Waiting for Railway deployment to complete..." | ||
| railway status --service echo --environment staging || echo "Service status check failed, continuing..." | ||
|
|
||
| echo "Waiting for deployment to be ready..." | ||
| max_attempts=90 | ||
| attempt=0 | ||
| while [ $attempt -lt $max_attempts ]; do | ||
| if curl -f -s -o /dev/null https://echo-staging.up.railway.app/health 2>/dev/null; then | ||
| echo "Deployment is ready!" | ||
| break | ||
| fi | ||
| attempt=$((attempt + 1)) | ||
| echo "Attempt $attempt/$max_attempts: Deployment not ready yet, waiting 10 seconds..." | ||
| sleep 10 | ||
| done | ||
|
|
||
| if [ $attempt -eq $max_attempts ]; then | ||
| echo "Deployment did not become ready in time (waited 15 minutes)" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Run Provider Smoke Tests | ||
| working-directory: ./packages/tests/provider-smoke | ||
| env: | ||
| ECHO_DATA_SERVER_URL: https://echo-staging.up.railway.app/ | ||
| ECHO_API_KEY: ${{ secrets.ECHO_API_KEY }} | ||
| ECHO_APP_ID: a4e9b928-cac0-4952-9b4e-3be01aaff45b | ||
| run: pnpm run test |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,34 +23,45 @@ import { | |
| } from 'services/facilitator/x402-types'; | ||
| import { Decimal } from '@prisma/client/runtime/library'; | ||
| import logger from 'logger'; | ||
| import { Request, Response } from 'express'; | ||
|
|
||
| export async function handleX402Request({ | ||
| req, | ||
| res, | ||
| headers, | ||
| maxCost, | ||
| isPassthroughProxyRoute, | ||
| provider, | ||
| isStream, | ||
| }: X402HandlerInput) { | ||
| if (isPassthroughProxyRoute) { | ||
| return await makeProxyPassthroughRequest(req, res, provider, headers); | ||
| export async function refund( | ||
| paymentAmountDecimal: Decimal, | ||
| payload: ExactEvmPayload | ||
| ) { | ||
| try { | ||
| const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal); | ||
| const authPayload = payload.authorization; | ||
| await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt); | ||
| } catch (error) { | ||
| logger.error('Failed to refund', error); | ||
| } | ||
| } | ||
|
|
||
| // Apply x402 payment middleware with the calculated maxCost | ||
| export async function settle( | ||
| req: Request, | ||
| res: Response, | ||
| headers: Record<string, string>, | ||
| maxCost: Decimal | ||
| ): Promise< | ||
| { payload: ExactEvmPayload; paymentAmountDecimal: Decimal } | undefined | ||
| > { | ||
| const network = process.env.NETWORK as Network; | ||
|
|
||
| let recipient: string; | ||
| try { | ||
| recipient = (await getSmartAccount()).smartAccount.address; | ||
| } catch (error) { | ||
| return buildX402Response(req, res, maxCost); | ||
| buildX402Response(req, res, maxCost); | ||
| return undefined; | ||
| } | ||
|
|
||
| let xPaymentData: PaymentPayload; | ||
| try { | ||
| xPaymentData = validateXPaymentHeader(headers, req); | ||
| } catch (error) { | ||
| return buildX402Response(req, res, maxCost); | ||
| buildX402Response(req, res, maxCost); | ||
| return undefined; | ||
| } | ||
|
|
||
| const payload = xPaymentData.payload as ExactEvmPayload; | ||
|
|
@@ -62,100 +73,101 @@ export async function handleX402Request({ | |
| // Note(shafu, alvaro): Edge case where client sends the x402-challenge | ||
| // but the payment amount is less than what we returned in the first response | ||
| if (BigInt(paymentAmount) < decimalToUsdcBigInt(maxCost)) { | ||
| return buildX402Response(req, res, maxCost); | ||
| buildX402Response(req, res, maxCost); | ||
| return undefined; | ||
| } | ||
|
|
||
| const facilitatorClient = new FacilitatorClient(); | ||
| const paymentRequirements = PaymentRequirementsSchema.parse({ | ||
| scheme: 'exact', | ||
| network, | ||
| maxAmountRequired: paymentAmount, | ||
| resource: `${req.protocol}://${req.get('host')}${req.url}`, | ||
| description: 'Echo x402', | ||
| mimeType: 'application/json', | ||
| payTo: recipient, | ||
| maxTimeoutSeconds: 60, | ||
| asset: USDC_ADDRESS, | ||
| extra: { | ||
| name: 'USD Coin', | ||
| version: '2', | ||
| }, | ||
| }); | ||
|
|
||
| const settleRequest = SettleRequestSchema.parse({ | ||
| paymentPayload: xPaymentData, | ||
| paymentRequirements, | ||
| }); | ||
|
|
||
| const settleResult = await facilitatorClient.settle(settleRequest); | ||
|
|
||
| if (!settleResult.success || !settleResult.transaction) { | ||
| buildX402Response(req, res, maxCost); | ||
| return undefined; | ||
| } | ||
|
|
||
| return { payload, paymentAmountDecimal }; | ||
| } | ||
|
|
||
| export async function finalize( | ||
| paymentAmountDecimal: Decimal, | ||
| transaction: Transaction, | ||
| payload: ExactEvmPayload | ||
| ) { | ||
| const refundAmount = calculateRefundAmount( | ||
| paymentAmountDecimal, | ||
| transaction.rawTransactionCost | ||
| ); | ||
|
|
||
| if (!refundAmount.equals(0) && refundAmount.greaterThan(0)) { | ||
| const refundAmountUsdcBigInt = decimalToUsdcBigInt(refundAmount); | ||
| const authPayload = payload.authorization; | ||
| await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing error handling for transfer operations in the View Details📝 Patch Detailsdiff --git a/packages/app/server/src/handlers.ts b/packages/app/server/src/handlers.ts
index 49d641e2..4ea7e4e7 100644
--- a/packages/app/server/src/handlers.ts
+++ b/packages/app/server/src/handlers.ts
@@ -109,7 +109,12 @@ export async function finalize(
if (!refundAmount.equals(0) && refundAmount.greaterThan(0)) {
const refundAmountUsdcBigInt = decimalToUsdcBigInt(refundAmount);
const authPayload = payload.authorization;
- await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt);
+ await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt).catch(transferError => {
+ logger.error('Failed to process refund', {
+ error: transferError,
+ refundAmount: refundAmount.toString(),
+ });
+ });
}
}
@@ -156,7 +161,13 @@ export async function handleX402Request({
} catch (error) {
const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal);
const authPayload = payload.authorization;
- await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt);
+ await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt).catch(transferError => {
+ logger.error('Failed to process full refund after error', {
+ error: transferError,
+ originalError: error,
+ refundAmount: paymentAmountDecimal.toString(),
+ });
+ });
}
}
AnalysisMissing error handling for transfer operations causes unhandled exceptionsWhat fails: How to reproduce: # Simulate blockchain transfer failure (network issues, insufficient balance, etc.)
# The transfer() function will throw but no error handling exists
git show 0cded6c0 -- packages/app/server/src/handlers.tsResult: Transfer failures now throw unhandled exceptions that can crash the request handler. Previously these were caught and logged gracefully. Expected: Transfer failures should be caught and logged as they were before commit 0cded6c (Oct 14, 2025) which removed Evidence: Git history shows the original pattern: |
||
| } | ||
| } | ||
|
|
||
| export async function handleX402Request({ | ||
| req, | ||
| res, | ||
| headers, | ||
| maxCost, | ||
| isPassthroughProxyRoute, | ||
| provider, | ||
| isStream, | ||
| }: X402HandlerInput) { | ||
| if (isPassthroughProxyRoute) { | ||
| return await makeProxyPassthroughRequest(req, res, provider, headers); | ||
| } | ||
|
|
||
| const settleResult = await settle(req, res, headers, maxCost); | ||
| if (!settleResult) { | ||
| return; | ||
| } | ||
|
|
||
| const { payload, paymentAmountDecimal } = settleResult; | ||
|
|
||
| try { | ||
| // Default to no refund | ||
| let refundAmount = new Decimal(0); | ||
| let transaction: Transaction | null = null; | ||
| let data: unknown = null; | ||
|
|
||
| // Construct and validate PaymentRequirements using Zod schema | ||
| const paymentRequirements = PaymentRequirementsSchema.parse({ | ||
| scheme: 'exact', | ||
| network, | ||
| maxAmountRequired: paymentAmount, | ||
| resource: `${req.protocol}://${req.get('host')}${req.url}`, | ||
| description: 'Echo x402', | ||
| mimeType: 'application/json', | ||
| payTo: recipient, | ||
| maxTimeoutSeconds: 60, | ||
| asset: USDC_ADDRESS, | ||
| extra: { | ||
| name: 'USD Coin', | ||
| version: '2', | ||
| }, | ||
| }); | ||
| // Validate and execute settle request | ||
| const settleRequest = SettleRequestSchema.parse({ | ||
| paymentPayload: xPaymentData, | ||
| paymentRequirements, | ||
| }); | ||
|
|
||
| const settleResult = await facilitatorClient.settle(settleRequest); | ||
|
|
||
| if (!settleResult.success || !settleResult.transaction) { | ||
| return buildX402Response(req, res, maxCost); | ||
| } | ||
|
|
||
| try { | ||
| const transactionResult = await modelRequestService.executeModelRequest( | ||
| req, | ||
| res, | ||
| headers, | ||
| provider, | ||
| isStream | ||
| ); | ||
| transaction = transactionResult.transaction; | ||
| data = transactionResult.data; | ||
|
|
||
| // Send the response - the middleware has intercepted res.end()/res.json() | ||
| // and will actually send it after settlement completes | ||
| modelRequestService.handleResolveResponse(res, isStream, data); | ||
|
|
||
| refundAmount = calculateRefundAmount( | ||
| paymentAmountDecimal, | ||
| transaction.rawTransactionCost | ||
| ); | ||
|
|
||
| // Process refund if needed | ||
| if (!refundAmount.equals(0) && refundAmount.greaterThan(0)) { | ||
| const refundAmountUsdcBigInt = decimalToUsdcBigInt(refundAmount); | ||
| const authPayload = payload.authorization; | ||
| await transfer( | ||
| authPayload.from as `0x${string}`, | ||
| refundAmountUsdcBigInt | ||
| ).catch(transferError => { | ||
| logger.error('Failed to process refund', { | ||
| error: transferError, | ||
| refundAmount: refundAmount.toString(), | ||
| }); | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| // In case of error, do full refund | ||
| refundAmount = paymentAmountDecimal; | ||
|
|
||
| if (!refundAmount.equals(0) && refundAmount.greaterThan(0)) { | ||
| const refundAmountUsdcBigInt = decimalToUsdcBigInt(refundAmount); | ||
| const authPayload = payload.authorization; | ||
| await transfer( | ||
| authPayload.from as `0x${string}`, | ||
| refundAmountUsdcBigInt | ||
| ).catch(transferError => { | ||
| logger.error('Failed to process full refund after error', { | ||
| error: transferError, | ||
| originalError: error, | ||
| refundAmount: refundAmount.toString(), | ||
| }); | ||
| }); | ||
| } | ||
| } | ||
| const transactionResult = await modelRequestService.executeModelRequest( | ||
| req, | ||
| res, | ||
| headers, | ||
| provider, | ||
| isStream | ||
| ); | ||
|
|
||
| modelRequestService.handleResolveResponse( | ||
| res, | ||
| isStream, | ||
| transactionResult.data | ||
| ); | ||
|
|
||
| await finalize( | ||
| paymentAmountDecimal, | ||
| transactionResult.transaction, | ||
| payload | ||
| ); | ||
| } catch (error) { | ||
| logger.error('Error in handleX402Request', { error }); | ||
| throw error; | ||
| await refund(paymentAmountDecimal, payload); | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.