$periods consecutive periods, newest first * @return list, priced:int, unpriced:array, not_priced:array}> */ public function summarize(int $siteId, array $periods): array { $result = array_map(static fn(): array => ['cost' => [], 'priced' => 0, 'unpriced' => [], 'not_priced' => []], $periods); if ($periods === []) { return []; } foreach ($this->priceable($siteId, $periods) as $g) { $i = (int) $g['period']; if (!isset($result[$i])) { continue; } $out = $this->price($g); if ($out['amount'] === null) { $result[$i]['unpriced'][$out['reason']] = ($result[$i]['unpriced'][$out['reason']] ?? 0) + (int) $g['n']; continue; } $cur = $out['currency']; $result[$i]['cost'][$cur] = ($result[$i]['cost'][$cur] ?? Amount::zero())->plus($out['amount']); $result[$i]['priced'] += (int) $g['n']; } foreach ($this->others($siteId, $periods) as $row) { $i = (int) $row['period']; $kind = self::kind((string) $row['state'], (string) ($row['usage_basis'] ?? ''), (int) $row['has_split'] === 1); if (isset($result[$i]) && $kind !== null) { $result[$i]['not_priced'][$kind] = ($result[$i]['not_priced'][$kind] ?? 0) + (int) $row['n']; } } return $result; } /** * Estimated cost by provider and model over one window. * * @return list */ public function byModel(int $siteId, int $from, int $to): array { $by = []; $unpriced = []; foreach ($this->priceable($siteId, [['start' => $from, 'end' => $to]]) as $g) { $served = (string) ($g['served_model'] ?? ''); $model = $served !== '' ? $served : (string) ($g['request_model'] ?? ''); $provider = (string) ($g['provider'] ?? ''); $out = $this->price($g); if ($out['amount'] === null) { $unpriced[$provider . '|' . $model] = ($unpriced[$provider . '|' . $model] ?? 0) + (int) $g['n']; continue; } $key = $provider . '|' . $model . '|' . $out['currency']; $by[$key] ??= ['provider' => $provider, 'model' => $model, 'currency' => $out['currency'], 'amount' => Amount::zero(), 'priced' => 0, 'unpriced' => 0, 'origin' => $out['origin'], 'verified' => $out['verified']]; $by[$key]['amount'] = $by[$key]['amount']->plus($out['amount']); $by[$key]['priced'] += (int) $g['n']; } foreach ($unpriced as $pm => $n) { $placed = false; foreach ($by as $key => $entry) { if (str_starts_with($key, $pm . '|')) { $by[$key]['unpriced'] += $n; $placed = true; break; } } if (!$placed) { [$provider, $model] = explode('|', $pm, 2); $by[$pm . '|'] = ['provider' => $provider, 'model' => $model, 'currency' => null, 'amount' => null, 'priced' => 0, 'unpriced' => $n, 'origin' => null, 'verified' => null]; } } uasort($by, static fn(array $a, array $b): int => [$b['priced'] + $b['unpriced'], $a['model']] <=> [$a['priced'] + $a['unpriced'], $b['model']]); return array_values($by); } /** * @param array $g one group of rows * @return array{amount:?Amount, reason:?string, currency:?string, origin:?string, verified?:string} */ private function price(array $g): array { $served = (string) ($g['served_model'] ?? ''); $model = $served !== '' ? $served : (string) ($g['request_model'] ?? ''); $provider = (string) ($g['provider'] ?? ''); $fail = static fn(string $reason): array => ['amount' => null, 'reason' => $reason, 'currency' => null, 'origin' => null]; if ($model === '' || $provider === '') { return $fail(CostCalculator::MODEL_UNKNOWN); } // The group says which side of each boundary and tier its rows are on; a representative request on that side picks the rate. $at = 0; foreach ($this->book->boundaries() as $k => $boundary) { if ((int) $g['b' . $k] === 1) { $at = max($at, $boundary); } } $input = 0; foreach ($this->book->tierCaps() as $k => $cap) { if ((int) $g['t' . $k] === 1) { $input = max($input, $cap + 1); } } $rate = $this->book->rateFor($provider, $model, $input, $at); if (is_string($rate)) { return $fail($rate); } if ((int) $g['c_bad'] === 1) { return $fail(CostCalculator::DETAILS_INVALID); } $oState = (int) $g['o_state']; $cached = (int) $g['c_null'] === 1 ? null : (int) $g['cached']; $other = $oState === 0 ? null : ($oState === 1 ? 0 : 1); [$amount, $reason] = CostCalculator::price($rate, (int) $g['tin'], (int) $g['tout'], $cached, $other, isset($g['processing_tier']) ? (string) $g['processing_tier'] : null); // Several requests were summed into one group: the exact total of the group is the group's token sums priced once, which equals // the sum of the requests because every term is linear in the token counts of one rate and one cached split. return $amount === null ? $fail((string) $reason) : ['amount' => $amount, 'reason' => null, 'currency' => $rate->currency, 'origin' => $rate->origin, 'verified' => $rate->verified]; } /** @param list $periods */ private function periodCase(array $periods, array &$params): string { $case = 'CASE'; foreach ($periods as $i => $p) { $case .= ' WHEN created_at >= ? THEN ' . (int) $i; $params[] = (int) $p['start']; } return $case . ' END'; } /** * @param list $periods * @return list> */ private function priceable(int $siteId, array $periods): array { $params = []; $select = $this->periodCase($periods, $params) . ' AS period'; foreach ($this->book->boundaries() as $k => $boundary) { $select .= ", (created_at >= ?) AS b$k"; $params[] = $boundary; } foreach ($this->book->tierCaps() as $k => $cap) { $select .= ", (provider_input_tokens > ?) AS t$k"; $params[] = $cap; } $group = 'period, provider, request_model, served_model, processing_tier, c_null, c_pos, c_bad, o_state'; foreach (array_keys($this->book->boundaries()) as $k) { $group .= ", b$k"; } foreach (array_keys($this->book->tierCaps()) as $k) { $group .= ", t$k"; } $oldest = $periods[count($periods) - 1]; $newest = $periods[0]; return $this->db->select( "SELECT $select, provider, request_model, served_model, processing_tier, (provider_cached_input_tokens IS NULL) AS c_null, (provider_cached_input_tokens > 0) AS c_pos, (provider_cached_input_tokens > provider_input_tokens) AS c_bad, CASE WHEN provider_other_tokens IS NULL THEN 0 WHEN provider_other_tokens = 0 THEN 1 ELSE 2 END AS o_state, COUNT(*) AS n, SUM(provider_input_tokens) AS tin, SUM(provider_output_tokens) AS tout, SUM(COALESCE(provider_cached_input_tokens, 0)) AS cached FROM {reservations} WHERE site_id = ? AND buckets <> ? AND created_at >= ? AND created_at < ? AND state = ? AND usage_basis = ? AND provider_input_tokens IS NOT NULL AND provider_output_tokens IS NOT NULL GROUP BY $group", array_merge($params, [$siteId, EmbeddingUsage::BUCKETS_JSON, (int) $oldest['start'], (int) $newest['end'], Reservation::SETTLED, Reservation::BASIS_REPORTED]) ); } /** * @param list $periods * @return list> */ private function others(int $siteId, array $periods): array { $params = []; $case = $this->periodCase($periods, $params); $oldest = $periods[count($periods) - 1]; $newest = $periods[0]; return $this->db->select( "SELECT $case AS period, state, usage_basis, (provider_input_tokens IS NOT NULL AND provider_output_tokens IS NOT NULL) AS has_split, COUNT(*) AS n FROM {reservations} WHERE site_id = ? AND buckets <> ? AND created_at >= ? AND created_at < ? GROUP BY period, state, usage_basis, has_split", array_merge($params, [$siteId, EmbeddingUsage::BUCKETS_JSON, (int) $oldest['start'], (int) $newest['end']]) ); } /** What kind of row this is when it is not a priceable one, or null when the priceable query already covers it. */ private static function kind(string $state, string $basis, bool $split): ?string { return match ($state) { Reservation::SETTLED => match (true) { $basis === Reservation::BASIS_REPORTED => $split ? null : 'no_split', $basis === Reservation::BASIS_SIMULATED => 'simulated', $basis === Reservation::BASIS_ESTIMATED => 'estimated', $basis === Reservation::BASIS_ADMIN_ENTERED => 'administrator_entered', $basis === Reservation::BASIS_NOT_DISPATCHED, $basis === Reservation::BASIS_REJECTED => 'nothing_billed', default => 'legacy', }, Reservation::UNCERTAIN => 'uncertain', Reservation::RESERVED, Reservation::DISPATCHED => 'in_progress', Reservation::RELEASED => 'released', default => null, }; } }