(traceSpans: CostTraceSpan[] | undefined)
| 133 | } |
| 134 | |
| 135 | export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): CostSummary { |
| 136 | if (!traceSpans || traceSpans.length === 0) { |
| 137 | return { |
| 138 | totalCost: BASE_EXECUTION_CHARGE, |
| 139 | totalInputCost: 0, |
| 140 | totalOutputCost: 0, |
| 141 | totalTokens: 0, |
| 142 | totalPromptTokens: 0, |
| 143 | totalCompletionTokens: 0, |
| 144 | baseExecutionCharge: BASE_EXECUTION_CHARGE, |
| 145 | models: {}, |
| 146 | charges: {}, |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | /** |
| 151 | * Collects spans that contribute to the execution's billable cost. |
| 152 | * |
| 153 | * Rule: when a span has its own `cost` AND has child model segments, the |
| 154 | * parent's block-level cost is authoritative — skip the model children to |
| 155 | * avoid double-counting. The parent cost is set by the provider response |
| 156 | * (and is correctly zeroed by `executeProviderRequest` for BYOK calls); |
| 157 | * model children only carry per-segment cost from the trace enrichers, |
| 158 | * which is unaware of BYOK status. Non-model children are still visited |
| 159 | * so standalone nested costs remain billable. |
| 160 | * |
| 161 | * Spans without their own `cost` (e.g. parent workflow spans for |
| 162 | * subworkflow blocks) still recurse so nested billable spans are counted. |
| 163 | */ |
| 164 | const collectCostSpans = (spans: CostTraceSpan[]): BillableTraceSpan[] => { |
| 165 | const costSpans: BillableTraceSpan[] = [] |
| 166 | |
| 167 | for (const span of spans) { |
| 168 | // `workflow`-typed spans are aggregate containers, not billable units: the |
| 169 | // synthetic "Workflow Execution" root (added to every run by |
| 170 | // buildTraceSpans) and any nested sub-workflow root carry a `cost.total` |
| 171 | // equal to the SUM of their descendants. Counting that aggregate in |
| 172 | // addition to the descendants double-charges the run, so treat these as |
| 173 | // pass-through: never count their own cost, always recurse into all |
| 174 | // children where the real billable leaves (agents, tools) live. |
| 175 | const isAggregateContainer = span.type === 'workflow' |
| 176 | const hasOwnCost = hasBillableCost(span) |
| 177 | const countOwnCost = hasOwnCost && !isAggregateContainer |
| 178 | |
| 179 | if (countOwnCost) { |
| 180 | costSpans.push(span) |
| 181 | } |
| 182 | |
| 183 | if (span.children && Array.isArray(span.children)) { |
| 184 | if (countOwnCost) { |
| 185 | // Authoritative leaf (e.g. an agent block whose block-level cost is set |
| 186 | // by the provider response and already accounts for its model |
| 187 | // segments): only recurse into non-model children to find further |
| 188 | // standalone billable units, skipping the model-breakdown duplicates. |
| 189 | const nonModelChildren = span.children.filter((child) => !isModelBreakdownSpan(child)) |
| 190 | costSpans.push(...collectCostSpans(nonModelChildren)) |
| 191 | } else { |
| 192 | // Container (workflow / sub-workflow root) or a no-cost parent: recurse |
no test coverage detected