diff --git a/apps/fabro-web/app/components/stage-popover.test.tsx b/apps/fabro-web/app/components/stage-popover.test.tsx index a27db5aa7..3f3c9fe92 100644 --- a/apps/fabro-web/app/components/stage-popover.test.tsx +++ b/apps/fabro-web/app/components/stage-popover.test.tsx @@ -9,7 +9,7 @@ import { StagePopover } from "./stage-popover"; import { deriveStageSummary } from "./stage-popover-summary"; import type { Stage } from "../lib/stage-sidebar"; import { generatedAxios } from "../lib/api-client"; -import { testBilledTokenCounts } from "../lib/test-fixtures"; +import { makeBilledTokenCounts } from "../lib/test-fixtures"; function makeEvent(overrides: Partial): EventEnvelope { return { @@ -35,7 +35,7 @@ function makeStage(overrides: Partial = {}): Stage { duration: "1m 30s", startedAt: "2026-05-24T11:58:30Z", providerUsed: { mode: "policy", model: "claude-opus-4-7", reasoning_effort: "high" }, - billing: testBilledTokenCounts(), + billing: makeBilledTokenCounts(), ...overrides, }; } diff --git a/apps/fabro-web/app/components/stage-renderers/fan-in-results.test.tsx b/apps/fabro-web/app/components/stage-renderers/fan-in-results.test.tsx index f7a3e4057..14bdb94b0 100644 --- a/apps/fabro-web/app/components/stage-renderers/fan-in-results.test.tsx +++ b/apps/fabro-web/app/components/stage-renderers/fan-in-results.test.tsx @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import type { EventEnvelope } from "@qltysh/fabro-api-client"; import TestRenderer, { act } from "react-test-renderer"; -import { testBilledTokenCounts } from "../../lib/test-fixtures"; +import { makeBilledTokenCounts } from "../../lib/test-fixtures"; import { makeEventEnvelope, setupReactTestEnv } from "../../lib/test-utils"; import type { Stage } from "../stage-sidebar"; import { FanInResults } from "./fan-in-results"; @@ -25,7 +25,7 @@ const fanInStage: Stage = { resumedFromStageId: null, startedAt: "2026-04-09T12:00:00Z", providerUsed: null, - billing: testBilledTokenCounts(), + billing: makeBilledTokenCounts(), }; function event(seq: number, partial: Partial): EventEnvelope { diff --git a/apps/fabro-web/app/components/stage-renderers/parallel-children.test.tsx b/apps/fabro-web/app/components/stage-renderers/parallel-children.test.tsx index c3f2d81c3..60d256991 100644 --- a/apps/fabro-web/app/components/stage-renderers/parallel-children.test.tsx +++ b/apps/fabro-web/app/components/stage-renderers/parallel-children.test.tsx @@ -3,7 +3,7 @@ import type { EventEnvelope } from "@qltysh/fabro-api-client"; import TestRenderer, { act } from "react-test-renderer"; import { MemoryRouter } from "react-router"; -import { testBilledTokenCounts } from "../../lib/test-fixtures"; +import { makeBilledTokenCounts } from "../../lib/test-fixtures"; import { makeEventEnvelope, setupReactTestEnv } from "../../lib/test-utils"; import type { Stage } from "../stage-sidebar"; import { ParallelChildren } from "./parallel-children"; @@ -26,7 +26,7 @@ const parallelStage: Stage = { resumedFromStageId: null, startedAt: "2026-04-09T12:00:00Z", providerUsed: null, - billing: testBilledTokenCounts(), + billing: makeBilledTokenCounts(), }; function event(partial: Partial): EventEnvelope { diff --git a/apps/fabro-web/app/components/stage-sidebar.test.tsx b/apps/fabro-web/app/components/stage-sidebar.test.tsx index 171059826..663734196 100644 --- a/apps/fabro-web/app/components/stage-sidebar.test.tsx +++ b/apps/fabro-web/app/components/stage-sidebar.test.tsx @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"; import TestRenderer, { act } from "react-test-renderer"; import { MemoryRouter } from "react-router"; -import { testBilledTokenCounts } from "../lib/test-fixtures"; +import { makeBilledTokenCounts } from "../lib/test-fixtures"; import { StageSidebar, type Stage } from "./stage-sidebar"; function makeStage(overrides: Partial = {}): Stage { @@ -18,7 +18,7 @@ function makeStage(overrides: Partial = {}): Stage { duration: "--", startedAt: null, providerUsed: null, - billing: testBilledTokenCounts(), + billing: makeBilledTokenCounts(), ...overrides, }; } diff --git a/apps/fabro-web/app/lib/billing.test.ts b/apps/fabro-web/app/lib/billing.test.ts index 0764a0f75..aee1f8e7f 100644 --- a/apps/fabro-web/app/lib/billing.test.ts +++ b/apps/fabro-web/app/lib/billing.test.ts @@ -5,13 +5,13 @@ import { formatBillingTokenCount, hasBillingUsage, } from "./billing"; -import { testBilledTokenCounts } from "./test-fixtures"; +import { makeBilledTokenCounts } from "./test-fixtures"; describe("billingTokenBuckets", () => { test("returns the shared display order and folds reasoning into output", () => { expect( billingTokenBuckets( - testBilledTokenCounts({ + makeBilledTokenCounts({ input_tokens: 10, output_tokens: 20, reasoning_tokens: 5, @@ -30,12 +30,15 @@ describe("billingTokenBuckets", () => { describe("hasBillingUsage", () => { test("includes cache-only and cost-only usage", () => { - expect(hasBillingUsage(testBilledTokenCounts())).toBe(false); + expect(hasBillingUsage(makeBilledTokenCounts())).toBe(false); expect( - hasBillingUsage(testBilledTokenCounts({ cache_read_tokens: 1 })), + hasBillingUsage(makeBilledTokenCounts({ cache_read_tokens: 1 })), ).toBe(true); expect( - hasBillingUsage(testBilledTokenCounts({ total_usd_micros: 1 })), + hasBillingUsage(makeBilledTokenCounts({ total_usd_micros: 1 })), + ).toBe(true); + expect( + hasBillingUsage(makeBilledTokenCounts({ total_tokens: 1 })), ).toBe(true); }); }); diff --git a/apps/fabro-web/app/lib/billing.ts b/apps/fabro-web/app/lib/billing.ts index f11ecde54..08092907a 100644 --- a/apps/fabro-web/app/lib/billing.ts +++ b/apps/fabro-web/app/lib/billing.ts @@ -7,6 +7,10 @@ export interface BillingTokenBucket { value: number; } +export function billableOutputTokens(billing: BilledTokenCounts): number { + return billing.output_tokens + billing.reasoning_tokens; +} + /** * Return the disjoint billing buckets in their shared display order. * Reasoning tokens are billed as output tokens. @@ -16,10 +20,7 @@ export function billingTokenBuckets(billing: BilledTokenCounts): BillingTokenBuc { label: "Cache read", value: billing.cache_read_tokens }, { label: "Cache creation", value: billing.cache_write_tokens }, { label: "Uncached", value: billing.input_tokens }, - { - label: "Output", - value: billing.output_tokens + billing.reasoning_tokens, - }, + { label: "Output", value: billableOutputTokens(billing) }, ]; } @@ -30,6 +31,7 @@ export function hasBillingUsage(billing: BilledTokenCounts): boolean { billing.reasoning_tokens !== 0 || billing.cache_read_tokens !== 0 || billing.cache_write_tokens !== 0 || + billing.total_tokens !== 0 || (billing.total_usd_micros ?? 0) !== 0 ); } diff --git a/apps/fabro-web/app/lib/stage-sidebar.test.ts b/apps/fabro-web/app/lib/stage-sidebar.test.ts index e6206634a..a16b325af 100644 --- a/apps/fabro-web/app/lib/stage-sidebar.test.ts +++ b/apps/fabro-web/app/lib/stage-sidebar.test.ts @@ -8,13 +8,13 @@ import type { import type { Stage } from "../components/stage-sidebar"; import { aggregateGraphNodeStatus, formatStageLabel, mapRunStagesToSidebarStages } from "./stage-sidebar"; -import { testBilledTokenCounts } from "./test-fixtures"; +import { makeBilledTokenCounts } from "./test-fixtures"; function runStage( stage: Omit & Partial>, ): RunStage { return { - billing: testBilledTokenCounts(), + billing: makeBilledTokenCounts(), ...stage, }; } @@ -32,7 +32,7 @@ function makeStage(nodeId: string, visit: number, status: StageState): Stage { duration: "--", startedAt: null, providerUsed: null, - billing: testBilledTokenCounts(), + billing: makeBilledTokenCounts(), }; } @@ -54,7 +54,7 @@ describe("mapRunStagesToSidebarStages", () => { model: "gpt-5.5", reasoning_effort: "high", }, - billing: testBilledTokenCounts({ + billing: makeBilledTokenCounts({ input_tokens: 28_640, output_tokens: 7_550, total_tokens: 43_690, diff --git a/apps/fabro-web/app/lib/test-fixtures.ts b/apps/fabro-web/app/lib/test-fixtures.ts index bf131a6fb..521e46e14 100644 --- a/apps/fabro-web/app/lib/test-fixtures.ts +++ b/apps/fabro-web/app/lib/test-fixtures.ts @@ -7,16 +7,16 @@ export const TEST_PRINCIPAL: Principal = { auth_method: "dev_token", }; -export function testBilledTokenCounts( +export function makeBilledTokenCounts( overrides: Partial = {}, ): BilledTokenCounts { return { - input_tokens: 0, - output_tokens: 0, - total_tokens: 0, - reasoning_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0, + input_tokens: 0, + output_tokens: 0, + reasoning_tokens: 0, + total_tokens: 0, ...overrides, }; } diff --git a/apps/fabro-web/app/routes/run-billing.test.tsx b/apps/fabro-web/app/routes/run-billing.test.tsx index 1d7531503..cdb1bc934 100644 --- a/apps/fabro-web/app/routes/run-billing.test.tsx +++ b/apps/fabro-web/app/routes/run-billing.test.tsx @@ -3,7 +3,7 @@ import TestRenderer from "react-test-renderer"; import type { RunBilling, StageTiming } from "@qltysh/fabro-api-client"; -import { testBilledTokenCounts } from "../lib/test-fixtures"; +import { makeBilledTokenCounts } from "../lib/test-fixtures"; function stageTiming(wall_time_ms = 0, inference_time_ms = 0, tool_time_ms = 0): StageTiming { return { @@ -27,7 +27,7 @@ function billing(overrides: Partial = {}): RunBilling { stages: [], totals: { timing: stageTiming(), - ...testBilledTokenCounts(), + ...makeBilledTokenCounts(), }, by_model: [], ...overrides, @@ -71,21 +71,21 @@ describe("RunBilling", () => { { stage: { id: "start", name: "start" }, model: null, - billing: testBilledTokenCounts(), + billing: makeBilledTokenCounts(), timing: stageTiming(), state: "succeeded", }, { stage: { id: "command", name: "command" }, model: null, - billing: testBilledTokenCounts(), + billing: makeBilledTokenCounts(), timing: stageTiming(61000), state: "succeeded", }, ], totals: { timing: stageTiming(61000), - ...testBilledTokenCounts(), + ...makeBilledTokenCounts(), }, }), ); @@ -106,7 +106,7 @@ describe("RunBilling", () => { { stage: { id: "start", name: "start" }, model: null, - billing: testBilledTokenCounts(), + billing: makeBilledTokenCounts(), timing: stageTiming(), state: "succeeded", }, @@ -116,7 +116,7 @@ describe("RunBilling", () => { provider: "anthropic", model_id: "claude-sonnet-4-5", }, - billing: testBilledTokenCounts({ + billing: makeBilledTokenCounts({ input_tokens: 1200, output_tokens: 300, total_tokens: 1500, @@ -128,7 +128,7 @@ describe("RunBilling", () => { ], totals: { timing: stageTiming(42000), - ...testBilledTokenCounts({ + ...makeBilledTokenCounts({ input_tokens: 1200, output_tokens: 300, total_tokens: 1500, @@ -142,7 +142,7 @@ describe("RunBilling", () => { model_id: "claude-sonnet-4-5", }, stages: 1, - billing: testBilledTokenCounts({ + billing: makeBilledTokenCounts({ input_tokens: 1200, output_tokens: 300, total_tokens: 1500, @@ -189,7 +189,7 @@ describe("RunBilling", () => { model_id: "claude-opus-4-6", speed: "fast", }, - billing: testBilledTokenCounts({ + billing: makeBilledTokenCounts({ input_tokens: 1200, output_tokens: 300, total_tokens: 1500, @@ -202,7 +202,7 @@ describe("RunBilling", () => { ], totals: { timing: stageTiming(), - ...testBilledTokenCounts({ + ...makeBilledTokenCounts({ input_tokens: 1200, output_tokens: 300, total_tokens: 1500, @@ -217,7 +217,7 @@ describe("RunBilling", () => { speed: "fast", }, stages: 1, - billing: testBilledTokenCounts({ + billing: makeBilledTokenCounts({ input_tokens: 1200, output_tokens: 300, total_tokens: 1500, diff --git a/apps/fabro-web/app/routes/run-billing.tsx b/apps/fabro-web/app/routes/run-billing.tsx index 68ea7be04..9b35dfc7c 100644 --- a/apps/fabro-web/app/routes/run-billing.tsx +++ b/apps/fabro-web/app/routes/run-billing.tsx @@ -3,6 +3,7 @@ import { Fragment, useMemo } from "react"; import { EmptyState } from "../components/state"; import { Tooltip } from "../components/ui"; import { + billableOutputTokens, billingTokenBuckets, formatBillingTokenCount, hasBillingUsage, @@ -75,18 +76,18 @@ function mapStageRow(stage: RunBillingStage, wallTimeMs: number): MappedStageRow /** Hover breakdown of the disjoint token buckets behind an `in / out` count. */ function TokenBreakdown({ billing }: { billing: BilledTokenCounts }) { - const rows = billingTokenBuckets(billing); + const buckets = billingTokenBuckets(billing); return (
-
+
Tokens in / out
- {rows.map((row) => ( - -
{row.label}
-
- {formatTokens(row.value)} + {buckets.map((bucket) => ( + +
{bucket.label}
+
+ {formatTokens(bucket.value)}
))} @@ -99,21 +100,14 @@ function TokenBreakdown({ billing }: { billing: BilledTokenCounts }) { * Renders an `input / output` token count. When the row has model usage, * hovering the count reveals the cache breakdown. */ -function TokensCell({ - billing, -}: { - billing: BilledTokenCounts | null; -}) { - const inputTokens = billing?.input_tokens; - const outputTokens = - billing == null ? null : billing.output_tokens + billing.reasoning_tokens; +function TokensCell({ billing }: { billing: BilledTokenCounts | null }) { const display = ( <> - {formatTokens(inputTokens)} /{" "} - {formatTokens(outputTokens)} + {formatTokens(billing?.input_tokens)} /{" "} + {formatTokens(billing ? billableOutputTokens(billing) : null)} ); - if (billing == null) return display; + if (!billing) return display; return ( }> {display} diff --git a/apps/fabro-web/app/routes/run-stages-chat.test.tsx b/apps/fabro-web/app/routes/run-stages-chat.test.tsx index b56261269..1142ae6b8 100644 --- a/apps/fabro-web/app/routes/run-stages-chat.test.tsx +++ b/apps/fabro-web/app/routes/run-stages-chat.test.tsx @@ -3,7 +3,7 @@ import { renderToStaticMarkup } from "react-dom/server"; import { StageState } from "@qltysh/fabro-api-client"; import type { Stage } from "../lib/stage-sidebar"; -import { testBilledTokenCounts } from "../lib/test-fixtures"; +import { makeBilledTokenCounts } from "../lib/test-fixtures"; import { StageChatView } from "./run-stages"; function stage(overrides: Partial = {}): Stage { @@ -19,7 +19,7 @@ function stage(overrides: Partial = {}): Stage { resumedFromStageId: null, startedAt: "2026-04-09T12:00:00Z", providerUsed: null, - billing: testBilledTokenCounts(), + billing: makeBilledTokenCounts(), ...overrides, }; } diff --git a/apps/fabro-web/app/routes/run-stages-details.test.tsx b/apps/fabro-web/app/routes/run-stages-details.test.tsx index ee0931833..6c3de37c3 100644 --- a/apps/fabro-web/app/routes/run-stages-details.test.tsx +++ b/apps/fabro-web/app/routes/run-stages-details.test.tsx @@ -7,7 +7,7 @@ import type { StageModelUsage, } from "@qltysh/fabro-api-client"; -import { testBilledTokenCounts } from "../lib/test-fixtures"; +import { makeBilledTokenCounts } from "../lib/test-fixtures"; import { EventDetails, ModelUsagePopover } from "./run-stages"; const RUN_START = "2026-04-09T12:00:00Z"; @@ -94,7 +94,7 @@ function popoverMarkup(counts: BilledTokenCounts): string { describe("ModelUsagePopover billing", () => { test("shows the visit's token buckets and cost next to the model", () => { const html = popoverMarkup( - testBilledTokenCounts({ + makeBilledTokenCounts({ input_tokens: 28_640, output_tokens: 7_550, reasoning_tokens: 1_200, @@ -120,7 +120,7 @@ describe("ModelUsagePopover billing", () => { }); test("omits the token section for a stage that called no model", () => { - const html = popoverMarkup(testBilledTokenCounts()); + const html = popoverMarkup(makeBilledTokenCounts()); expect(html).toContain("kimi-k3"); expect(html).not.toContain("Tokens"); @@ -129,7 +129,7 @@ describe("ModelUsagePopover billing", () => { test("still shows tokens when nothing priced the stage", () => { const html = popoverMarkup( - testBilledTokenCounts({ + makeBilledTokenCounts({ input_tokens: 1_000, output_tokens: 500, total_tokens: 1_500, @@ -141,14 +141,14 @@ describe("ModelUsagePopover billing", () => { expect(html).not.toContain("Cost"); }); - test("shows a reported cost when token buckets are empty", () => { + test("shows a provider-reported cost when token counts are unavailable", () => { const html = popoverMarkup( - testBilledTokenCounts({ total_usd_micros: 1_000_000 }), + makeBilledTokenCounts({ total_usd_micros: 720_000 }), ); expect(html).toContain("kimi-k3"); expect(html).toContain("Tokens"); expect(html).toContain("Cost"); - expect(html).toContain("$1.00"); + expect(html).toContain("$0.72"); }); }); diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index a6a8c5ff1..b3d57cfa2 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -5259,6 +5259,64 @@ fn test_billed_usage( .unwrap() } +async fn create_billed_retry_run(state: &Arc, run_id: RunId) { + create_durable_run_with_events(state, run_id, &[ + workflow_event::Event::RunSubmitted { + definition_blob: None, + }, + workflow_event::Event::RunStarting, + workflow_event::Event::RunRunning, + ]) + .await; + + append_scoped_stage_event( + state, + run_id, + "verify", + 1, + &workflow_event::Event::StageFailed { + node_id: "verify".to_string(), + name: "Verify".to_string(), + index: 1, + failure: FailureDetail::new("try again", FailureCategory::TransientInfra), + will_retry: true, + timing: fabro_types::StageTiming::wall_only(1200), + billing: Some(test_billed_usage("gpt-old", 100, 10)), + actor: None, + }, + ) + .await; + append_scoped_stage_event( + state, + run_id, + "verify", + 2, + &workflow_event::Event::StageCompleted { + node_id: "verify".to_string(), + name: "Verify".to_string(), + index: 1, + timing: fabro_types::StageTiming::wall_only(800), + status: "succeeded".to_string(), + preferred_label: None, + suggested_next_ids: Vec::new(), + billing: Some(test_billed_usage("gpt-new", 200, 20)), + failure: None, + notes: None, + files_touched: Vec::new(), + context_updates: None, + jump_to_node: None, + context_values: None, + node_visits: None, + loop_failure_signatures: None, + restart_failure_signatures: None, + response: None, + attempt: 2, + max_attempts: 2, + }, + ) + .await; +} + #[tokio::test] async fn list_run_stages_distinguishes_visits() { let state = test_app_state_with_isolated_storage(); @@ -5650,69 +5708,13 @@ async fn run_billing_dedups_retried_nodes_and_sums_their_durations() { } #[tokio::test] -async fn billing_endpoints_report_retry_usage_per_node_and_per_visit() { +async fn run_billing_sums_usage_across_retry_visits_and_uses_latest_model() { let state = test_app_state_with_isolated_storage(); let app = crate::test_support::build_test_router(Arc::clone(&state)); let run_id = RunId::new(); - let failed_usage = test_billed_usage("gpt-old", 100, 10); + + create_billed_retry_run(&state, run_id).await; let success_usage = test_billed_usage("gpt-new", 200, 20); - - create_durable_run_with_events(&state, run_id, &[ - workflow_event::Event::RunSubmitted { - definition_blob: None, - }, - workflow_event::Event::RunStarting, - workflow_event::Event::RunRunning, - ]) - .await; - - append_scoped_stage_event( - &state, - run_id, - "verify", - 1, - &workflow_event::Event::StageFailed { - node_id: "verify".to_string(), - name: "Verify".to_string(), - index: 1, - failure: FailureDetail::new("try again", FailureCategory::TransientInfra), - will_retry: true, - timing: fabro_types::StageTiming::wall_only(1200), - billing: Some(failed_usage), - actor: None, - }, - ) - .await; - append_scoped_stage_event( - &state, - run_id, - "verify", - 2, - &workflow_event::Event::StageCompleted { - node_id: "verify".to_string(), - name: "Verify".to_string(), - index: 1, - timing: fabro_types::StageTiming::wall_only(800), - status: "succeeded".to_string(), - preferred_label: None, - suggested_next_ids: Vec::new(), - billing: Some(success_usage.clone()), - failure: None, - notes: None, - files_touched: Vec::new(), - context_updates: None, - jump_to_node: None, - context_values: None, - node_visits: None, - loop_failure_signatures: None, - restart_failure_signatures: None, - response: None, - attempt: 2, - max_attempts: 2, - }, - ) - .await; - let mut latest_outcome: Outcome> = Outcome::success(); latest_outcome.usage = Some(success_usage); latest_outcome.timing = Some(fabro_types::StageTiming::wall_only(800)); @@ -5746,7 +5748,6 @@ async fn billing_endpoints_report_retry_usage_per_node_and_per_visit() { .unwrap(); let response = app - .clone() .oneshot( Request::builder() .method("GET") @@ -5789,6 +5790,18 @@ async fn billing_endpoints_report_retry_usage_per_node_and_per_visit() { assert_eq!(old_model["billing"]["input_tokens"], 100); assert_eq!(new_model["stages"], 1); assert_eq!(new_model["billing"]["input_tokens"], 200); +} + +/// The stage popover reads `billing` off the stages list, so it must be scoped +/// to one visit — unlike the Billing tab's rows, which sum every visit of a +/// node. +#[tokio::test] +async fn list_run_stages_reports_billing_per_visit() { + let state = test_app_state_with_isolated_storage(); + let app = crate::test_support::build_test_router(Arc::clone(&state)); + let run_id = RunId::new(); + + create_billed_retry_run(&state, run_id).await; let response = app .oneshot( @@ -5827,23 +5840,8 @@ async fn list_run_stages_reports_zero_billing_for_a_stage_that_called_no_model() workflow_event::Event::RunRunning, ]) .await; - append_scoped_stage_event( - &state, - run_id, - "script", - 1, - &workflow_event::Event::StageStarted { - graph_visit: None, - resumed_from_stage_id: None, - node_id: "script".to_string(), - name: "Script".to_string(), - index: 0, - handler_type: "command".to_string(), - attempt: 1, - max_attempts: 1, - }, - ) - .await; + let started = stage_started_event("script", "command"); + append_scoped_stage_event(&state, run_id, "script", 1, &started).await; let response = app .oneshot( diff --git a/lib/foundation/fabro-types/src/run_projection.rs b/lib/foundation/fabro-types/src/run_projection.rs index 4e7d27af8..f60018361 100644 --- a/lib/foundation/fabro-types/src/run_projection.rs +++ b/lib/foundation/fabro-types/src/run_projection.rs @@ -1054,7 +1054,7 @@ mod iter_stages_tests { } #[test] - fn billed_usage_leaves_an_unused_model_uncosted() { + fn billed_usage_leaves_zero_tokens_uncosted() { let mut stage = priced_stage(None); stage.usage = BilledTokenCounts::default();