diff --git a/artifacts/api-server/src/routes/jobs.ts b/artifacts/api-server/src/routes/jobs.ts index f3b0464..511c0da 100644 --- a/artifacts/api-server/src/routes/jobs.ts +++ b/artifacts/api-server/src/routes/jobs.ts @@ -289,6 +289,7 @@ router.get("/jobs/:id", async (req: Request, res: Response) => { costLedger: { actualInputTokens: job.actualInputTokens, actualOutputTokens: job.actualOutputTokens, + totalTokens: (job.actualInputTokens ?? 0) + (job.actualOutputTokens ?? 0), actualCostUsd: job.actualCostUsd, estimatedCostUsd: job.estimatedCostUsd, marginPct: job.marginPct, diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index e21e212..7771eb9 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -40,13 +40,42 @@ export const JobState = { failed: "failed", } as const; +/** + * Cost breakdown shown with the work invoice (estimations at invoice-creation time) + */ +export interface PricingBreakdown { + /** Total estimated cost in USD (token cost + DO infra + margin) */ + estimatedCostUsd?: number; + /** Originator margin percentage applied */ + marginPct?: number; + /** BTC/USD spot price used to convert the invoice to sats */ + btcPriceUsd?: number; +} + +/** + * Actual cost record stored after the job completes + */ +export interface CostLedger { + actualInputTokens?: number; + actualOutputTokens?: number; + /** Sum of actualInputTokens + actualOutputTokens */ + totalTokens?: number; + /** Raw Anthropic token cost (no infra, no margin) */ + actualCostUsd?: number; + estimatedCostUsd?: number; + marginPct?: number; + btcPriceUsd?: number; +} + export interface JobStatusResponse { jobId: string; state: JobState; evalInvoice?: InvoiceInfo; workInvoice?: InvoiceInfo; + pricingBreakdown?: PricingBreakdown; reason?: string; result?: string; + costLedger?: CostLedger; errorMessage?: string; } diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index e8e89e5..390eee7 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -202,6 +202,9 @@ components: type: integer actualOutputTokens: type: integer + totalTokens: + type: integer + description: Sum of actualInputTokens + actualOutputTokens actualCostUsd: type: number description: Raw Anthropic token cost (no infra, no margin) diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index a2eabad..8789c90 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -21,7 +21,7 @@ export const HealthCheckResponse = zod.object({ */ export const CreateJobBody = zod.object({ - request: zod.string().min(1).max(500), + request: zod.string().min(1), }); /** @@ -55,8 +55,47 @@ export const GetJobResponse = zod.object({ amountSats: zod.number(), }) .optional(), + pricingBreakdown: zod + .object({ + estimatedCostUsd: zod + .number() + .optional() + .describe( + "Total estimated cost in USD (token cost + DO infra + margin)", + ), + marginPct: zod + .number() + .optional() + .describe("Originator margin percentage applied"), + btcPriceUsd: zod + .number() + .optional() + .describe("BTC\/USD spot price used to convert the invoice to sats"), + }) + .optional() + .describe( + "Cost breakdown shown with the work invoice (estimations at invoice-creation time)", + ), reason: zod.string().optional(), result: zod.string().optional(), + costLedger: zod + .object({ + actualInputTokens: zod.number().optional(), + actualOutputTokens: zod.number().optional(), + totalTokens: zod + .number() + .optional() + .describe("Sum of actualInputTokens + actualOutputTokens"), + actualCostUsd: zod + .number() + .optional() + .describe("Raw Anthropic token cost (no infra, no margin)"), + estimatedCostUsd: zod.number().optional(), + marginPct: zod.number().optional(), + btcPriceUsd: zod.number().optional(), + }) + .optional() + .describe("Actual cost record stored after the job completes"), errorMessage: zod.string().optional(), }); @@ -65,7 +104,7 @@ export const GetJobResponse = zod.object({ * @summary Free demo (rate-limited) */ export const RunDemoQueryParams = zod.object({ - request: zod.string().min(1).max(500), + request: zod.coerce.string(), }); export const RunDemoResponse = zod.object({ diff --git a/lib/api-zod/src/generated/types/costLedger.ts b/lib/api-zod/src/generated/types/costLedger.ts new file mode 100644 index 0000000..264683a --- /dev/null +++ b/lib/api-zod/src/generated/types/costLedger.ts @@ -0,0 +1,22 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +/** + * Actual cost record stored after the job completes + */ +export interface CostLedger { + actualInputTokens?: number; + actualOutputTokens?: number; + /** Sum of actualInputTokens + actualOutputTokens */ + totalTokens?: number; + /** Raw Anthropic token cost (no infra, no margin) */ + actualCostUsd?: number; + estimatedCostUsd?: number; + marginPct?: number; + btcPriceUsd?: number; +} diff --git a/lib/api-zod/src/generated/types/index.ts b/lib/api-zod/src/generated/types/index.ts index 7e069df..5c7ac17 100644 --- a/lib/api-zod/src/generated/types/index.ts +++ b/lib/api-zod/src/generated/types/index.ts @@ -6,6 +6,7 @@ * OpenAPI spec version: 0.1.0 */ +export * from "./costLedger"; export * from "./createJobRequest"; export * from "./createJobResponse"; export * from "./demoResponse"; @@ -14,4 +15,5 @@ export * from "./healthStatus"; export * from "./invoiceInfo"; export * from "./jobState"; export * from "./jobStatusResponse"; +export * from "./pricingBreakdown"; export * from "./runDemoParams"; diff --git a/lib/api-zod/src/generated/types/jobStatusResponse.ts b/lib/api-zod/src/generated/types/jobStatusResponse.ts index ba35971..4a86bb8 100644 --- a/lib/api-zod/src/generated/types/jobStatusResponse.ts +++ b/lib/api-zod/src/generated/types/jobStatusResponse.ts @@ -5,15 +5,19 @@ * API specification * OpenAPI spec version: 0.1.0 */ +import type { CostLedger } from "./costLedger"; import type { InvoiceInfo } from "./invoiceInfo"; import type { JobState } from "./jobState"; +import type { PricingBreakdown } from "./pricingBreakdown"; export interface JobStatusResponse { jobId: string; state: JobState; evalInvoice?: InvoiceInfo; workInvoice?: InvoiceInfo; + pricingBreakdown?: PricingBreakdown; reason?: string; result?: string; + costLedger?: CostLedger; errorMessage?: string; } diff --git a/lib/api-zod/src/generated/types/pricingBreakdown.ts b/lib/api-zod/src/generated/types/pricingBreakdown.ts new file mode 100644 index 0000000..4c1ed4c --- /dev/null +++ b/lib/api-zod/src/generated/types/pricingBreakdown.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +/** + * Cost breakdown shown with the work invoice (estimations at invoice-creation time) + */ +export interface PricingBreakdown { + /** Total estimated cost in USD (token cost + DO infra + margin) */ + estimatedCostUsd?: number; + /** Originator margin percentage applied */ + marginPct?: number; + /** BTC/USD spot price used to convert the invoice to sats */ + btcPriceUsd?: number; +} diff --git a/lib/db/migrations/0002_cost_based_pricing.sql b/lib/db/migrations/0002_cost_based_pricing.sql new file mode 100644 index 0000000..668be1d --- /dev/null +++ b/lib/db/migrations/0002_cost_based_pricing.sql @@ -0,0 +1,17 @@ +-- Migration: Add cost-based pricing columns to jobs table +-- Task #6: Cost-based work fee pricing with BTC oracle + +ALTER TABLE jobs + ADD COLUMN IF NOT EXISTS estimated_cost_usd REAL, + ADD COLUMN IF NOT EXISTS margin_pct REAL, + ADD COLUMN IF NOT EXISTS btc_price_usd REAL, + ADD COLUMN IF NOT EXISTS actual_input_tokens INTEGER, + ADD COLUMN IF NOT EXISTS actual_output_tokens INTEGER, + ADD COLUMN IF NOT EXISTS actual_cost_usd REAL; + +COMMENT ON COLUMN jobs.estimated_cost_usd IS 'Total estimated cost in USD at work invoice creation time (token + DO infra + margin)'; +COMMENT ON COLUMN jobs.margin_pct IS 'Originator margin percentage applied to this job'; +COMMENT ON COLUMN jobs.btc_price_usd IS 'BTC/USD price used to convert work fee to sats (from CoinGecko at invoice time)'; +COMMENT ON COLUMN jobs.actual_input_tokens IS 'Actual input token count reported by Anthropic after work execution'; +COMMENT ON COLUMN jobs.actual_output_tokens IS 'Actual output token count reported by Anthropic after work execution'; +COMMENT ON COLUMN jobs.actual_cost_usd IS 'Actual raw Anthropic token cost (no infra amortisation, no margin) after work execution';