Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 52 additions & 0 deletions src/__tests__/alexandria-beta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
83 changes: 82 additions & 1 deletion src/__tests__/utils/receipt.test.ts
Original file line number Diff line number Diff line change
@@ -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());

Expand Down Expand Up @@ -58,3 +58,84 @@ 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,
});
}
);

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,
});
});
39 changes: 38 additions & 1 deletion src/utils/receipt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface Receipt {
creditsUsed?: number;
separatelyBilledCredits?: number;
requestId?: string;
operationId?: string;
operationType?: 'scrape' | 'search';
Expand All @@ -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 validSqlCosts = sqlCosts.filter(
(cost: unknown): cost is number =>
typeof cost === 'number' && Number.isFinite(cost) && cost >= 0
);
const separatelyBilledCredits =
validSqlCosts.length > 0
? validSqlCosts.reduce((sum: number, cost: number) => sum + cost, 0)
: undefined;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return {
...(separatelyBilledCredits !== undefined &&
Number.isFinite(separatelyBilledCredits)
? { separatelyBilledCredits }
: {}),
...(typeof credits === 'number' && Number.isFinite(credits) && credits >= 0
? { creditsUsed: credits }
: {}),
Expand All @@ -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<string, unknown>): void {
Expand Down
Loading