From 3d2adeec0aa544a270075c4d108cd33faa46dbbd Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:25:19 -0400 Subject: [PATCH 1/2] Display separately billed SQL provider credits --- src/__tests__/alexandria-beta.test.ts | 52 ++++++++++++++++++++++++++ src/__tests__/utils/receipt.test.ts | 54 ++++++++++++++++++++++++++- src/utils/receipt.ts | 39 ++++++++++++++++++- 3 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/__tests__/alexandria-beta.test.ts b/src/__tests__/alexandria-beta.test.ts index 57051f039a..d0d04eb122 100644 --- a/src/__tests__/alexandria-beta.test.ts +++ b/src/__tests__/alexandria-beta.test.ts @@ -1454,3 +1454,55 @@ it('sends missing_capability feedback without requestedFunctionality', async () expect(requests).toHaveLength(1); expect(requests[0].body.capabilityFeedback).toEqual(capability); }); + +it('displays nested SQL cost while preserving the free outer receipt', async () => { + response = { + success: true, + scrape_id: 'sql-outer', + data: { + creditsCost: 0, + alexandria: [ + { + provider: 'firecrawl', + capability: 'sql', + creditsCost: 0, + data: { + kind: 'result', + creditsCost: 110, + rows: [], + receipt: { + creditsUsed: 110, + requestId: 'inner-request', + operationId: 'inner-scrape', + operationType: 'scrape', + }, + }, + }, + ], + }, + }; + const result = await cli([ + 'scrape', + '--alexandria', + 'firecrawl/sql', + '--options', + JSON.stringify({ + query: 'SELECT * FROM "similarweb/web/traffic" LIMIT 1', + execute: true, + }), + '--json', + ]); + expect(result.code).toBe(0); + expect(result.stderr).toContain( + 'Credits: 110 (0 outer request + 110 separately billed provider calls)' + ); + const output = JSON.parse(result.stdout); + expect(output.receipt).toMatchObject({ + creditsUsed: 0, + separatelyBilledCredits: 110, + }); + expect(output.data.alexandria[0].data.receipt).toEqual( + (response as any).data.alexandria[0].data.receipt + ); + expect(requests).toHaveLength(1); +}); diff --git a/src/__tests__/utils/receipt.test.ts b/src/__tests__/utils/receipt.test.ts index 9b8510b9f9..3afbcaaa41 100644 --- a/src/__tests__/utils/receipt.test.ts +++ b/src/__tests__/utils/receipt.test.ts @@ -1,6 +1,6 @@ import { afterEach, expect, it, vi } from 'vitest'; import { apiFailure } from '../../commands/alexandria'; -import { receiptFor } from '../../utils/receipt'; +import { receiptFor, printReceipt } from '../../utils/receipt'; afterEach(() => vi.useRealTimers()); @@ -58,3 +58,55 @@ it('handles HTTP-date retry delays without treating invalid numeric delays as da for (const value of ['-5', 'nonsense', '']) expect(failure(value)).not.toHaveProperty('retryAfterSeconds'); }); + +const sqlResponse = (cost: unknown, overrides = {}) => ({ + data: { + creditsCost: 0, + alexandria: [ + { + provider: 'firecrawl', + capability: 'sql', + data: { kind: 'result', creditsCost: cost }, + ...overrides, + }, + ], + }, +}); +it('shows separately billed SQL costs without changing the outer receipt charge', () => { + const receipt = receiptFor(sqlResponse(110), 'scrape'); + expect(receipt).toEqual({ creditsUsed: 0, separatelyBilledCredits: 110 }); + const log = vi.spyOn(console, 'error').mockImplementation(() => {}); + try { + printReceipt(receipt); + expect(log).toHaveBeenCalledWith( + 'Credits: 110 (0 outer request + 110 separately billed provider calls)' + ); + } finally { + log.mockRestore(); + } +}); +it('preserves zero-cost nested execution', () => { + expect(receiptFor(sqlResponse(0), 'scrape')).toEqual({ + creditsUsed: 0, + separatelyBilledCredits: 0, + }); +}); +it.each([undefined, -1, NaN, Infinity, '110'])( + 'ignores invalid nested costs: %s', + (cost) => { + expect(receiptFor(sqlResponse(cost), 'scrape')).toEqual({ creditsUsed: 0 }); + } +); +it.each([ + { provider: 'other' }, + { capability: 'bash' }, + { error: { code: 'provider_error' } }, + { data: { kind: 'plan', creditsCost: 110 } }, +])( + 'does not treat other payloads as separately billed SQL: %j', + (overrides) => { + expect(receiptFor(sqlResponse(110, overrides), 'scrape')).toEqual({ + creditsUsed: 0, + }); + } +); diff --git a/src/utils/receipt.ts b/src/utils/receipt.ts index 8be60ddf5a..aca28d4050 100644 --- a/src/utils/receipt.ts +++ b/src/utils/receipt.ts @@ -1,5 +1,6 @@ export interface Receipt { creditsUsed?: number; + separatelyBilledCredits?: number; requestId?: string; operationId?: string; operationType?: 'scrape' | 'search'; @@ -20,7 +21,32 @@ export function receiptFor( operationType === 'search' ? value?.id : (value?.metadata?.scrapeId ?? value?.scrape_id ?? value?.scrapeId); + const entries = value?.data?.alexandria; + const sqlCosts = + operationType === 'scrape' && Array.isArray(entries) + ? entries + .filter( + (entry: any) => + entry?.provider === 'firecrawl' && + entry?.capability === 'sql' && + !entry.error && + entry?.data?.kind === 'result' + ) + .map((entry: any) => entry.data.creditsCost) + : []; + const separatelyBilledCredits = + sqlCosts.length > 0 && + sqlCosts.every( + (cost: unknown) => + typeof cost === 'number' && Number.isFinite(cost) && cost >= 0 + ) + ? sqlCosts.reduce((sum: number, cost: number) => sum + cost, 0) + : undefined; return { + ...(separatelyBilledCredits !== undefined && + Number.isFinite(separatelyBilledCredits) + ? { separatelyBilledCredits } + : {}), ...(typeof credits === 'number' && Number.isFinite(credits) && credits >= 0 ? { creditsUsed: credits } : {}), @@ -38,8 +64,19 @@ export function printReceipt(receipt: Receipt, includeRequestId = true): void { console.error( `${receipt.operationType === 'search' ? 'Search' : 'Scrape'} ID: ${receipt.operationId}` ); - if (receipt.creditsUsed !== undefined) + if (receipt.separatelyBilledCredits !== undefined) { + if (receipt.creditsUsed !== undefined) { + console.error( + `Credits: ${receipt.creditsUsed + receipt.separatelyBilledCredits} (${receipt.creditsUsed} outer request + ${receipt.separatelyBilledCredits} separately billed provider calls)` + ); + } else { + console.error( + `Provider credits (billed separately): ${receipt.separatelyBilledCredits}` + ); + } + } else if (receipt.creditsUsed !== undefined) { console.error(`Credits: ${receipt.creditsUsed}`); + } } export function printRetry(failure: Record): void { From b89b1d4601fa9518a163f4aee2e04599afa75b9a Mon Sep 17 00:00:00 2001 From: Developers Digest <124798203+developersdigest@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:34:49 -0400 Subject: [PATCH 2/2] Preserve valid SQL costs when other entries are invalid --- src/__tests__/utils/receipt.test.ts | 29 +++++++++++++++++++++++++++++ src/utils/receipt.ts | 12 ++++++------ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/__tests__/utils/receipt.test.ts b/src/__tests__/utils/receipt.test.ts index 3afbcaaa41..4eb212e411 100644 --- a/src/__tests__/utils/receipt.test.ts +++ b/src/__tests__/utils/receipt.test.ts @@ -110,3 +110,32 @@ it.each([ }); } ); + +it('retains all valid charges when other SQL costs are invalid', () => { + const response = { + data: { + creditsCost: 0, + alexandria: [5, undefined, 110, -1, NaN, Infinity, '15', 0, 15].flatMap( + (cost) => sqlResponse(cost).data.alexandria + ), + }, + }; + expect(receiptFor(response, 'scrape')).toEqual({ + creditsUsed: 0, + separatelyBilledCredits: 130, + }); +}); +it('retains a valid zero alongside invalid costs', () => { + const response = { + data: { + creditsCost: 0, + alexandria: [undefined, 0, -1].flatMap( + (cost) => sqlResponse(cost).data.alexandria + ), + }, + }; + expect(receiptFor(response, 'scrape')).toEqual({ + creditsUsed: 0, + separatelyBilledCredits: 0, + }); +}); diff --git a/src/utils/receipt.ts b/src/utils/receipt.ts index aca28d4050..89f996621f 100644 --- a/src/utils/receipt.ts +++ b/src/utils/receipt.ts @@ -34,13 +34,13 @@ export function receiptFor( ) .map((entry: any) => entry.data.creditsCost) : []; + const validSqlCosts = sqlCosts.filter( + (cost: unknown): cost is number => + typeof cost === 'number' && Number.isFinite(cost) && cost >= 0 + ); const separatelyBilledCredits = - sqlCosts.length > 0 && - sqlCosts.every( - (cost: unknown) => - typeof cost === 'number' && Number.isFinite(cost) && cost >= 0 - ) - ? sqlCosts.reduce((sum: number, cost: number) => sum + cost, 0) + validSqlCosts.length > 0 + ? validSqlCosts.reduce((sum: number, cost: number) => sum + cost, 0) : undefined; return { ...(separatelyBilledCredits !== undefined &&