Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-governance/src/lexigram/ai/governance/relay_billing/pricing.py: 19%

323 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 07:19 +0800

1"""Decimal price calculation and restricted expression evaluation. 

2 

3Computes per-dimension relay charges from normalized ``RelayUsage`` and 

4configured price snapshots. Price expressions are ported from the 

5relaykit billing conventions and evaluated by a small allow-listed 

6parser: numeric literals, named usage dimensions, ``+ - * /``, ``min``, 

7``max``, and parentheses. Attribute access, arbitrary calls, 

8exponentiation, assignment, imports, and unbounded recursion are 

9rejected. All arithmetic uses non-negative ``Decimal`` with explicit 

10per-dimension rounding; every safety violation returns a 

11``RelayBillingError`` instead of clamping silently. 

12""" 

13 

14from __future__ import annotations 

15 

16from collections.abc import Callable, Mapping 

17from dataclasses import dataclass 

18from decimal import ( 

19 ROUND_HALF_UP, 

20 Decimal, 

21 DecimalException, 

22 InvalidOperation, 

23) 

24import re 

25 

26from lexigram.contracts.ai.governance import ( 

27 RelayBillingError, 

28 RelayChargeBreakdown, 

29 RelayPriceEstimatorProtocol, 

30 charge_overflow, 

31 invalid_usage, 

32 unknown_price, 

33) 

34from lexigram.contracts.ai.llm import CostEstimatorProtocol 

35from lexigram.contracts.ai.relay import RelayUsage 

36from lexigram.contracts.core.result import Err, Ok, Result 

37 

38__all__ = [ 

39 "BREAKDOWN_FIELDS", 

40 "DEFAULT_MAX_CHARGE", 

41 "DEFAULT_MAX_TOKENS", 

42 "DEFAULT_SCALE", 

43 "PriceSnapshot", 

44 "RelayPricingEngine", 

45 "SimpleCostEstimator", 

46 "evaluate_expression", 

47] 

48 

49BREAKDOWN_FIELDS = ( 

50 "prompt", 

51 "cached_prompt", 

52 "completion", 

53 "reasoning", 

54 "audio_input", 

55 "audio_output", 

56 "image", 

57) 

58 

59_DIMENSION_TO_USAGE = { 

60 "prompt": "prompt_tokens", 

61 "cached_prompt": "cache_read_tokens", 

62 "completion": "completion_tokens", 

63 "reasoning": "reasoning_tokens", 

64 "audio_input": "audio_input_tokens", 

65 "audio_output": "audio_output_tokens", 

66 "image": "image_tokens", 

67} 

68 

69_ALLOWED_DIMENSION_NAMES = ( 

70 "prompt_tokens", 

71 "completion_tokens", 

72 "cache_read_tokens", 

73 "cache_creation_tokens", 

74 "reasoning_tokens", 

75 "audio_input_tokens", 

76 "audio_output_tokens", 

77 "image_tokens", 

78 "input_tokens", 

79 "output_tokens", 

80 "total_tokens", 

81) 

82 

83_NUMBER_RE = re.compile(r"\d+(\.\d+)?([eE][+-]?\d+)?") 

84_NAME_RE = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*") 

85 

86MAX_EXPRESSION_LENGTH = 512 

87MAX_PARSE_DEPTH = 64 

88DEFAULT_SCALE = 10 

89DEFAULT_MAX_CHARGE = Decimal("1_000_000") 

90DEFAULT_MAX_TOKENS = 2**31 - 1 

91_PER_1M = Decimal(1_000_000) 

92 

93 

94class _ExpressionError(Exception): 

95 """Internal control flow for expression parsing and evaluation.""" 

96 

97 

98@dataclass(frozen=True, slots=True) 

99class PriceSnapshot: 

100 """Configured per-dimension price expressions for one model. 

101 

102 Each expression is evaluated against the named usage dimensions of 

103 ``RelayUsage``, e.g. ``"prompt_tokens * 0.0000025"``. A dimension 

104 without an expression contributes zero while remaining present in 

105 the audit breakdown. 

106 

107 Attributes: 

108 expressions: Mapping of breakdown field name to expression string. 

109 """ 

110 

111 expressions: Mapping[str, str] 

112 

113 def __post_init__(self) -> None: 

114 """Validate dimension names and expression syntax.""" 

115 unknown = set(self.expressions) - set(BREAKDOWN_FIELDS) 

116 if unknown: 

117 names = ", ".join(sorted(unknown)) 

118 raise ValueError(f"unknown price dimensions: {names}") 

119 for field_name, expression in self.expressions.items(): 

120 if not expression.strip(): 

121 raise ValueError(f"empty expression for dimension {field_name}") 

122 if _syntax_error(expression) is not None: 

123 raise ValueError( 

124 f"invalid expression for dimension {field_name}: {expression}" 

125 ) 

126 

127 @classmethod 

128 def from_per_1m(cls, prices: Mapping[str, Decimal]) -> PriceSnapshot: 

129 """Build a snapshot from per-1M-token Decimal prices. 

130 

131 Args: 

132 prices: Per-1M-token price per breakdown dimension; missing 

133 dimensions default to zero. 

134 

135 Returns: 

136 A snapshot whose expressions multiply each usage dimension 

137 by its per-token price. 

138 

139 Raises: 

140 ValueError: If a price is negative or a dimension is unknown. 

141 """ 

142 unknown = set(prices) - set(BREAKDOWN_FIELDS) 

143 if unknown: 

144 names = ", ".join(sorted(unknown)) 

145 raise ValueError(f"unknown price dimensions: {names}") 

146 expressions: dict[str, str] = {} 

147 for field_name in BREAKDOWN_FIELDS: 

148 price = prices.get(field_name, Decimal(0)) 

149 if price < 0: 

150 raise ValueError(f"negative price for dimension {field_name}") 

151 if price == 0: 

152 expressions[field_name] = "0" 

153 continue 

154 usage_field = _DIMENSION_TO_USAGE[field_name] 

155 per_token = (price / _PER_1M).normalize() 

156 expressions[field_name] = f"{usage_field} * {format(per_token, 'f')}" 

157 return cls(expressions=expressions) 

158 

159 

160@dataclass(frozen=True, slots=True) 

161class _Token: 

162 """One lexical token of a price expression.""" 

163 

164 kind: str 

165 value: str = "" 

166 

167 

168@dataclass(frozen=True, slots=True) 

169class _NumberNode: 

170 """Literal numeric node.""" 

171 

172 value: Decimal 

173 

174 

175@dataclass(frozen=True, slots=True) 

176class _DimensionNode: 

177 """Named usage dimension node.""" 

178 

179 name: str 

180 

181 

182@dataclass(frozen=True, slots=True) 

183class _BinaryNode: 

184 """Binary arithmetic node (``+ - * /``).""" 

185 

186 operator: str 

187 left: _Node 

188 right: _Node 

189 

190 

191@dataclass(frozen=True, slots=True) 

192class _ExtremeNode: 

193 """``min``/``max`` call node.""" 

194 

195 operator: str 

196 left: _Node 

197 right: _Node 

198 

199 

200_Node = _NumberNode | _DimensionNode | _BinaryNode | _ExtremeNode 

201 

202 

203def _syntax_error(expression: str) -> RelayBillingError | None: 

204 """Return the parse error for *expression*, or ``None`` if valid.""" 

205 result = _tokenize(expression) 

206 if result.is_err(): 

207 return result.unwrap_err() 

208 parsed = _parse(result.unwrap()) 

209 if parsed.is_err(): 

210 return parsed.unwrap_err() 

211 return None 

212 

213 

214def _tokenize(expression: str) -> Result[list[_Token], RelayBillingError]: 

215 """Tokenize a price expression, rejecting unsupported characters.""" 

216 if len(expression) > MAX_EXPRESSION_LENGTH: 

217 return Err( 

218 invalid_usage( 

219 message=f"price expression exceeds {MAX_EXPRESSION_LENGTH} chars" 

220 ) 

221 ) 

222 tokens: list[_Token] = [] 

223 index = 0 

224 length = len(expression) 

225 while index < length: 

226 char = expression[index] 

227 if char.isspace(): 

228 index += 1 

229 continue 

230 if char.isdigit(): 

231 match = _NUMBER_RE.match(expression, index) 

232 assert match is not None # noqa: S101 # regex pre-guaranteed by isdigit() branch 

233 tokens.append(_Token(kind="NUMBER", value=match.group(0))) 

234 index = match.end() 

235 continue 

236 if char.isalpha() or char == "_": 

237 match = _NAME_RE.match(expression, index) 

238 assert match is not None # noqa: S101 # regex pre-guaranteed by isalpha() branch 

239 tokens.append(_Token(kind="NAME", value=match.group(0))) 

240 index = match.end() 

241 continue 

242 if char in "+-*/": 

243 tokens.append(_Token(kind="OP", value=char)) 

244 index += 1 

245 continue 

246 if char == "(": 

247 tokens.append(_Token(kind="LPAREN")) 

248 index += 1 

249 continue 

250 if char == ")": 

251 tokens.append(_Token(kind="RPAREN")) 

252 index += 1 

253 continue 

254 if char == ",": 

255 tokens.append(_Token(kind="COMMA")) 

256 index += 1 

257 continue 

258 return Err( 

259 invalid_usage(message=f"unsupported character {char!r} in price expression") 

260 ) 

261 return Ok(tokens) 

262 

263 

264def _parse(tokens: list[_Token]) -> Result[_Node, RelayBillingError]: 

265 """Parse tokens into an allow-listed AST.""" 

266 

267 position = 0 

268 

269 def peek() -> _Token | None: 

270 """Return the next unconsumed token, if any.""" 

271 if position < len(tokens): 

272 return tokens[position] 

273 return None 

274 

275 def advance() -> _Token: 

276 """Consume and return the next token.""" 

277 nonlocal position 

278 token = tokens[position] 

279 position += 1 

280 return token 

281 

282 def expect(kind: str) -> _Token: 

283 """Consume a token of *kind* or raise a parse error.""" 

284 token = peek() 

285 if token is None or token.kind != kind: 

286 raise _ExpressionError(f"expected {kind}") 

287 return advance() 

288 

289 def parse_expression(depth: int) -> _Node: 

290 """Parse ``+ -``; depth bounds recursion.""" 

291 if depth > MAX_PARSE_DEPTH: 

292 raise _ExpressionError("expression nested too deeply") 

293 node = parse_term(depth + 1) 

294 while True: 

295 token = peek() 

296 if token is not None and token.kind == "OP" and token.value in "+-": 

297 advance() 

298 node = _BinaryNode( 

299 operator=token.value, 

300 left=node, 

301 right=parse_term(depth + 1), 

302 ) 

303 else: 

304 return node 

305 

306 def parse_term(depth: int) -> _Node: 

307 """Parse ``* /``; depth bounds recursion.""" 

308 node = parse_factor(depth + 1) 

309 while True: 

310 token = peek() 

311 if token is not None and token.kind == "OP" and token.value in "*/": 

312 advance() 

313 node = _BinaryNode( 

314 operator=token.value, 

315 left=node, 

316 right=parse_factor(depth + 1), 

317 ) 

318 else: 

319 return node 

320 

321 def parse_factor(depth: int) -> _Node: 

322 """Parse literals, dimensions, parentheses, and min/max calls.""" 

323 if depth > MAX_PARSE_DEPTH: 

324 raise _ExpressionError("expression nested too deeply") 

325 token = peek() 

326 if token is None: 

327 raise _ExpressionError("unexpected end of expression") 

328 if token.kind == "NUMBER": 

329 advance() 

330 try: 

331 return _NumberNode(value=Decimal(token.value)) 

332 except InvalidOperation as exc: 

333 raise _ExpressionError("invalid numeric literal") from exc 

334 if token.kind == "NAME": 

335 advance() 

336 if token.value in ("min", "max"): 

337 expect("LPAREN") 

338 left = parse_expression(depth + 1) 

339 expect("COMMA") 

340 right = parse_expression(depth + 1) 

341 expect("RPAREN") 

342 return _ExtremeNode(operator=token.value, left=left, right=right) 

343 following = peek() 

344 if following is not None and following.kind == "LPAREN": 

345 raise _ExpressionError(f"call {token.value!r} not allowed") 

346 return _DimensionNode(name=token.value) 

347 if token.kind == "LPAREN": 

348 advance() 

349 node = parse_expression(depth + 1) 

350 expect("RPAREN") 

351 return node 

352 raise _ExpressionError(f"unexpected token {token.value or token.kind!r}") 

353 

354 if not tokens: 

355 return Err(invalid_usage(message="empty price expression")) 

356 try: 

357 node = parse_expression(0) 

358 if position != len(tokens): 

359 token = tokens[position] 

360 raise _ExpressionError( 

361 f"unexpected trailing token {token.value or token.kind!r}" 

362 ) 

363 return Ok(node) 

364 except _ExpressionError as exc: 

365 return Err(invalid_usage(message=str(exc))) 

366 

367 

368def _evaluate(node: _Node, usage: RelayUsage, max_tokens: int) -> Decimal: 

369 """Evaluate an AST against usage, raising on any safety violation.""" 

370 if isinstance(node, _NumberNode): 

371 return node.value 

372 if isinstance(node, _DimensionNode): 

373 if node.name not in _ALLOWED_DIMENSION_NAMES: 

374 raise _ExpressionError(f"unknown usage dimension {node.name!r}") 

375 value = getattr(usage, node.name) 

376 if value < 0: 

377 raise _ExpressionError(f"negative usage dimension {node.name}") 

378 if value > max_tokens: 

379 raise _ExpressionError(f"usage dimension {node.name} exceeds maximum") 

380 return Decimal(value) 

381 left = _evaluate(node.left, usage, max_tokens) 

382 right = _evaluate(node.right, usage, max_tokens) 

383 try: 

384 if isinstance(node, _BinaryNode): 

385 if node.operator == "+": 

386 result = left + right 

387 elif node.operator == "-": 

388 result = left - right 

389 elif node.operator == "*": 

390 result = left * right 

391 else: 

392 if right == 0: 

393 raise _ExpressionError("division by zero") 

394 result = left / right 

395 else: 

396 result = min(left, right) if node.operator == "min" else max(left, right) 

397 except DecimalException as exc: 

398 raise _ExpressionError("decimal arithmetic failure") from exc 

399 if not result.is_finite(): 

400 raise _ExpressionError("non-finite result") 

401 if result < 0: 

402 raise _ExpressionError("negative result") 

403 return result 

404 

405 

406def evaluate_expression( 

407 expression: str, 

408 usage: RelayUsage, 

409 *, 

410 scale: int = DEFAULT_SCALE, 

411 rounding: str = ROUND_HALF_UP, 

412 max_tokens: int = DEFAULT_MAX_TOKENS, 

413) -> Result[Decimal, RelayBillingError]: 

414 """Evaluate a price expression against normalized usage. 

415 

416 Args: 

417 expression: Price expression over named usage dimensions. 

418 usage: Normalized usage the expression is evaluated against. 

419 scale: Decimal places retained on the result. 

420 rounding: Rounding mode applied to the result. 

421 max_tokens: Integer maximum for any usage dimension. 

422 

423 Returns: 

424 Ok(rounded charge) on success, Err(RelayBillingError) on any 

425 syntax or arithmetic safety violation. 

426 """ 

427 if scale < 0: 

428 return Err(invalid_usage(message="scale must be non-negative")) 

429 tokens = _tokenize(expression) 

430 if tokens.is_err(): 

431 return Err(tokens.unwrap_err()) 

432 parsed = _parse(tokens.unwrap()) 

433 if parsed.is_err(): 

434 return Err(parsed.unwrap_err()) 

435 try: 

436 value = _evaluate(parsed.unwrap(), usage, max_tokens) 

437 except _ExpressionError as exc: 

438 return Err(invalid_usage(message=str(exc))) 

439 try: 

440 quantum = Decimal(1).scaleb(-scale) 

441 return Ok(value.quantize(quantum, rounding=rounding)) 

442 except DecimalException: 

443 return Err(invalid_usage(message="charge rounding failed")) 

444 

445 

446class RelayPricingEngine(RelayPriceEstimatorProtocol): 

447 """Detailed per-dimension relay price estimator. 

448 

449 Args: 

450 price_provider: Callable returning the ``PriceSnapshot`` for a 

451 ``(model, provider, channel)`` triple, or ``None`` when the 

452 model has no configured price. 

453 scale: Default decimal places retained per dimension. 

454 rounding: Default rounding mode applied per dimension. 

455 max_charge: Maximum total charge; above it fails closed. 

456 max_tokens: Integer maximum for any usage dimension. 

457 dimension_scales: Per-dimension decimal places overrides. 

458 dimension_roundings: Per-dimension rounding mode overrides. 

459 

460 Raises: 

461 ValueError: If *max_charge* or *scale* is negative, or a 

462 dimension override references an unknown breakdown field. 

463 """ 

464 

465 def __init__( 

466 self, 

467 price_provider: Callable[[str, str, str], PriceSnapshot | None], 

468 *, 

469 scale: int = DEFAULT_SCALE, 

470 rounding: str = ROUND_HALF_UP, 

471 max_charge: Decimal = DEFAULT_MAX_CHARGE, 

472 max_tokens: int = DEFAULT_MAX_TOKENS, 

473 dimension_scales: Mapping[str, int] | None = None, 

474 dimension_roundings: Mapping[str, str] | None = None, 

475 ) -> None: 

476 if scale < 0: 

477 raise ValueError("scale must be non-negative") 

478 if max_charge < 0: 

479 raise ValueError("max_charge must be non-negative") 

480 unknown_scales = set(dimension_scales or {}) - set(BREAKDOWN_FIELDS) 

481 if unknown_scales: 

482 names = ", ".join(sorted(unknown_scales)) 

483 raise ValueError(f"unknown dimension scales: {names}") 

484 unknown_roundings = set(dimension_roundings or {}) - set(BREAKDOWN_FIELDS) 

485 if unknown_roundings: 

486 names = ", ".join(sorted(unknown_roundings)) 

487 raise ValueError(f"unknown dimension roundings: {names}") 

488 self._price_provider = price_provider 

489 self._scale = scale 

490 self._rounding = rounding 

491 self._max_charge = max_charge 

492 self._max_tokens = max_tokens 

493 self._dimension_scales = dict(dimension_scales or {}) 

494 self._dimension_roundings = dict(dimension_roundings or {}) 

495 

496 def estimate_charge( 

497 self, 

498 model: str, 

499 usage: RelayUsage, 

500 *, 

501 provider: str = "", 

502 channel: str = "", 

503 ) -> Result[RelayChargeBreakdown, RelayBillingError]: 

504 """Compute the per-dimension charge breakdown for *usage*. 

505 

506 Returns: 

507 Ok(breakdown) on success, Err(RelayBillingError) on unknown 

508 prices, negative usage, overflow, or expression failures. 

509 """ 

510 for name in _ALLOWED_DIMENSION_NAMES: 

511 if getattr(usage, name) < 0: 

512 return Err(invalid_usage(message=f"negative usage dimension {name}")) 

513 snapshot = self._price_provider(model, provider, channel) 

514 if snapshot is None: 

515 return Err( 

516 unknown_price(message=f"no price configured for model {model!r}") 

517 ) 

518 charges: dict[str, Decimal] = {} 

519 for field_name in BREAKDOWN_FIELDS: 

520 expression = snapshot.expressions.get(field_name, "0") 

521 scale = self._dimension_scales.get(field_name, self._scale) 

522 rounding = self._dimension_roundings.get(field_name, self._rounding) 

523 charge = evaluate_expression( 

524 expression, 

525 usage, 

526 scale=scale, 

527 rounding=rounding, 

528 max_tokens=self._max_tokens, 

529 ) 

530 if charge.is_err(): 

531 return Err(charge.unwrap_err()) 

532 charges[field_name] = charge.unwrap() 

533 total = sum(charges.values(), Decimal(0)) 

534 if total > self._max_charge: 

535 return Err( 

536 charge_overflow( 

537 message=f"charge {total} exceeds configured maximum {self._max_charge}" 

538 ) 

539 ) 

540 return Ok(RelayChargeBreakdown(**charges, total=total)) 

541 

542 

543class SimpleCostEstimator(RelayPriceEstimatorProtocol): 

544 """Simple prompt/completion price estimator over ``CostEstimatorProtocol``. 

545 

546 Priced at the underlying estimator's input and output rates; the 

547 detailed relay dimensions are always zero. Unknown models yield a 

548 zero charge, matching the estimator's unknown-model-zero policy. 

549 

550 Args: 

551 estimator: Existing LLM cost estimator reused for the simple path. 

552 scale: Decimal places retained on each part of the charge. 

553 rounding: Rounding mode applied to each part of the charge. 

554 max_charge: Maximum total charge; above it fails closed. 

555 """ 

556 

557 def __init__( 

558 self, 

559 estimator: CostEstimatorProtocol, 

560 *, 

561 scale: int = DEFAULT_SCALE, 

562 rounding: str = ROUND_HALF_UP, 

563 max_charge: Decimal = DEFAULT_MAX_CHARGE, 

564 ) -> None: 

565 if scale < 0: 

566 raise ValueError("scale must be non-negative") 

567 if max_charge < 0: 

568 raise ValueError("max_charge must be non-negative") 

569 self._estimator = estimator 

570 self._scale = scale 

571 self._rounding = rounding 

572 self._max_charge = max_charge 

573 

574 def estimate_charge( 

575 self, 

576 model: str, 

577 usage: RelayUsage, 

578 *, 

579 provider: str = "", 

580 channel: str = "", 

581 ) -> Result[RelayChargeBreakdown, RelayBillingError]: 

582 """Estimate prompt and completion charges from the LLM estimator.""" 

583 provider_arg = provider or None 

584 try: 

585 prompt_cost = float( 

586 self._estimator.estimate_cost( 

587 model, 

588 usage.prompt_tokens, 

589 provider_arg, 

590 prompt_tokens=usage.prompt_tokens, 

591 completion_tokens=0, 

592 ) 

593 ) 

594 completion_cost = float( 

595 self._estimator.estimate_cost( 

596 model, 

597 usage.completion_tokens, 

598 provider_arg, 

599 prompt_tokens=0, 

600 completion_tokens=usage.completion_tokens, 

601 ) 

602 ) 

603 except (TypeError, ValueError, OverflowError) as exc: 

604 return Err(invalid_usage(message=f"estimator failed: {exc}")) 

605 prompt = Decimal(str(prompt_cost)) 

606 completion = Decimal(str(completion_cost)) 

607 if not prompt.is_finite() or not completion.is_finite(): 

608 return Err(invalid_usage(message="estimator returned a non-finite cost")) 

609 if prompt < 0 or completion < 0: 

610 return Err(invalid_usage(message="estimator returned a negative cost")) 

611 quantum = Decimal(1).scaleb(-self._scale) 

612 try: 

613 prompt = prompt.quantize(quantum, rounding=self._rounding) 

614 completion = completion.quantize(quantum, rounding=self._rounding) 

615 except DecimalException: 

616 return Err(invalid_usage(message="charge rounding failed")) 

617 zero = Decimal(0) 

618 total = prompt + completion 

619 if total > self._max_charge: 

620 return Err( 

621 charge_overflow( 

622 message=f"charge {total} exceeds configured maximum {self._max_charge}" 

623 ) 

624 ) 

625 return Ok( 

626 RelayChargeBreakdown( 

627 prompt=prompt, 

628 cached_prompt=zero, 

629 completion=completion, 

630 reasoning=zero, 

631 audio_input=zero, 

632 audio_output=zero, 

633 image=zero, 

634 total=total, 

635 ) 

636 )