alysis-code 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,1990 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Iterable, Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from statistics import median
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .llm.metadata import strip_provider_metadata_from_message
|
|
15
|
+
from .llm.types import BillingMode, CostSource, UsageConfidence, UsageContract, UsageSource
|
|
16
|
+
from .model_registry import ModelMeta, ModelRegistry
|
|
17
|
+
from .provider_telemetry import base_url_host
|
|
18
|
+
from .request_estimation import (
|
|
19
|
+
RequestTokenBreakdown,
|
|
20
|
+
estimate_request_token_breakdown,
|
|
21
|
+
request_contains_media,
|
|
22
|
+
request_message_signatures,
|
|
23
|
+
tool_schema_signature,
|
|
24
|
+
)
|
|
25
|
+
from .session_store import read_session_events
|
|
26
|
+
from .token_budget import compute_input_budget, estimate_tokens
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def now_iso() -> str:
|
|
30
|
+
return datetime.now(UTC).replace(microsecond=0).isoformat()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class UsageRecord:
|
|
35
|
+
timestamp: str
|
|
36
|
+
role: str
|
|
37
|
+
requested_model: str
|
|
38
|
+
response_model: str | None
|
|
39
|
+
prompt_tokens: int
|
|
40
|
+
completion_tokens: int
|
|
41
|
+
total_tokens: int
|
|
42
|
+
input_cost_per_token: float | None
|
|
43
|
+
output_cost_per_token: float | None
|
|
44
|
+
cost_usd: float | None
|
|
45
|
+
usage_source: str
|
|
46
|
+
billing_mode: str = BillingMode.METERED_API.value
|
|
47
|
+
cost_source: str = CostSource.UNKNOWN.value
|
|
48
|
+
provider_cost_usd: float | None = None
|
|
49
|
+
usage_source_detail: str = UsageSource.LOCAL_ESTIMATE.value
|
|
50
|
+
usage_confidence: str = UsageConfidence.ESTIMATED.value
|
|
51
|
+
output_includes_reasoning: bool = True
|
|
52
|
+
usage_schema_version: int = 6
|
|
53
|
+
provider_key: str | None = None
|
|
54
|
+
protocol: str | None = None
|
|
55
|
+
base_url_host: str | None = None
|
|
56
|
+
operation: str | None = None
|
|
57
|
+
request_mode: str | None = None
|
|
58
|
+
cache_strategy: str | None = None
|
|
59
|
+
request_plan: dict[str, Any] | None = None
|
|
60
|
+
request_has_media: bool = False
|
|
61
|
+
cache_read_input_cost_per_token: float | None = None
|
|
62
|
+
cache_creation_input_cost_per_token: float | None = None
|
|
63
|
+
cache_creation_5m_input_cost_per_token: float | None = None
|
|
64
|
+
cache_creation_1h_input_cost_per_token: float | None = None
|
|
65
|
+
reasoning_output_cost_per_token: float | None = None
|
|
66
|
+
cached_prompt_tokens: int | None = None
|
|
67
|
+
uncached_prompt_tokens: int | None = None
|
|
68
|
+
input_tokens_uncached: int | None = None
|
|
69
|
+
input_tokens_uncached_derived: bool = False
|
|
70
|
+
cache_read_input_tokens: int | None = None
|
|
71
|
+
cache_creation_input_tokens: int | None = None
|
|
72
|
+
cache_creation_5m_input_tokens: int | None = None
|
|
73
|
+
cache_creation_1h_input_tokens: int | None = None
|
|
74
|
+
reasoning_tokens: int | None = None
|
|
75
|
+
raw_provider_usage: dict[str, Any] | None = None
|
|
76
|
+
cache_cost_pricing_missing: bool = False
|
|
77
|
+
request_token_estimate: RequestTokenBreakdown | None = None
|
|
78
|
+
prompt_estimate_tokens: int | None = None
|
|
79
|
+
prompt_estimate_error_tokens: int | None = None
|
|
80
|
+
prompt_estimate_error_ratio: float | None = None
|
|
81
|
+
raw_api_prompt_tokens: int | None = None
|
|
82
|
+
raw_api_completion_tokens: int | None = None
|
|
83
|
+
raw_api_total_tokens: int | None = None
|
|
84
|
+
usage_correction_reason: str | None = None
|
|
85
|
+
|
|
86
|
+
def to_payload(self) -> dict[str, Any]:
|
|
87
|
+
return {
|
|
88
|
+
"event_type": "llm_usage",
|
|
89
|
+
"usage_schema_version": self.usage_schema_version,
|
|
90
|
+
"timestamp": self.timestamp,
|
|
91
|
+
"role": self.role,
|
|
92
|
+
"requested_model": self.requested_model,
|
|
93
|
+
"response_model": self.response_model,
|
|
94
|
+
"prompt_tokens": self.prompt_tokens,
|
|
95
|
+
"completion_tokens": self.completion_tokens,
|
|
96
|
+
"total_tokens": self.total_tokens,
|
|
97
|
+
"input_cost_per_token": self.input_cost_per_token,
|
|
98
|
+
"output_cost_per_token": self.output_cost_per_token,
|
|
99
|
+
"cache_read_input_cost_per_token": self.cache_read_input_cost_per_token,
|
|
100
|
+
"cache_creation_input_cost_per_token": self.cache_creation_input_cost_per_token,
|
|
101
|
+
"cache_creation_5m_input_cost_per_token": (self.cache_creation_5m_input_cost_per_token),
|
|
102
|
+
"cache_creation_1h_input_cost_per_token": (self.cache_creation_1h_input_cost_per_token),
|
|
103
|
+
"reasoning_output_cost_per_token": self.reasoning_output_cost_per_token,
|
|
104
|
+
"cost_usd": self.cost_usd,
|
|
105
|
+
"billing_mode": self.billing_mode,
|
|
106
|
+
"cost_source": self.cost_source,
|
|
107
|
+
"provider_cost_usd": self.provider_cost_usd,
|
|
108
|
+
"usage_source": self.usage_source,
|
|
109
|
+
"usage_source_detail": self.usage_source_detail,
|
|
110
|
+
"usage_confidence": self.usage_confidence,
|
|
111
|
+
"output_includes_reasoning": self.output_includes_reasoning,
|
|
112
|
+
"provider_key": self.provider_key,
|
|
113
|
+
"protocol": self.protocol,
|
|
114
|
+
"base_url_host": self.base_url_host,
|
|
115
|
+
"operation": self.operation,
|
|
116
|
+
"request_mode": self.request_mode,
|
|
117
|
+
"cache_strategy": self.cache_strategy,
|
|
118
|
+
"request_plan": copy.deepcopy(self.request_plan),
|
|
119
|
+
"request_has_media": self.request_has_media,
|
|
120
|
+
"cached_prompt_tokens": self.cached_prompt_tokens,
|
|
121
|
+
"uncached_prompt_tokens": self.uncached_prompt_tokens,
|
|
122
|
+
"input_tokens_uncached": self.input_tokens_uncached,
|
|
123
|
+
"input_tokens_uncached_derived": self.input_tokens_uncached_derived,
|
|
124
|
+
"cache_read_input_tokens": self.cache_read_input_tokens,
|
|
125
|
+
"cache_creation_input_tokens": self.cache_creation_input_tokens,
|
|
126
|
+
"cache_creation_5m_input_tokens": self.cache_creation_5m_input_tokens,
|
|
127
|
+
"cache_creation_1h_input_tokens": self.cache_creation_1h_input_tokens,
|
|
128
|
+
"reasoning_tokens": self.reasoning_tokens,
|
|
129
|
+
"raw_provider_usage": copy.deepcopy(self.raw_provider_usage),
|
|
130
|
+
"cache_cost_pricing_missing": self.cache_cost_pricing_missing,
|
|
131
|
+
"request_token_estimate": (
|
|
132
|
+
self.request_token_estimate.to_payload()
|
|
133
|
+
if self.request_token_estimate is not None
|
|
134
|
+
else None
|
|
135
|
+
),
|
|
136
|
+
"prompt_estimate_tokens": self.prompt_estimate_tokens,
|
|
137
|
+
"prompt_estimate_error_tokens": self.prompt_estimate_error_tokens,
|
|
138
|
+
"prompt_estimate_error_ratio": self.prompt_estimate_error_ratio,
|
|
139
|
+
"raw_api_prompt_tokens": self.raw_api_prompt_tokens,
|
|
140
|
+
"raw_api_completion_tokens": self.raw_api_completion_tokens,
|
|
141
|
+
"raw_api_total_tokens": self.raw_api_total_tokens,
|
|
142
|
+
"usage_correction_reason": self.usage_correction_reason,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass(frozen=True)
|
|
147
|
+
class ContextLeft:
|
|
148
|
+
model_name: str
|
|
149
|
+
max_input_tokens: int | None
|
|
150
|
+
used_input_tokens: int
|
|
151
|
+
remaining_tokens: int | None
|
|
152
|
+
percent_left: float | None
|
|
153
|
+
source: str
|
|
154
|
+
context_window_tokens: int | None = None
|
|
155
|
+
context_window_remaining_tokens: int | None = None
|
|
156
|
+
context_window_percent_left: float | None = None
|
|
157
|
+
effective_input_budget: int | None = None
|
|
158
|
+
effective_remaining_tokens: int | None = None
|
|
159
|
+
effective_percent_left: float | None = None
|
|
160
|
+
startup_baseline_tokens: int = 0
|
|
161
|
+
dynamic_context_budget_tokens: int | None = None
|
|
162
|
+
dynamic_context_used_tokens: int = 0
|
|
163
|
+
dynamic_context_remaining_tokens: int | None = None
|
|
164
|
+
dynamic_context_percent_left: float | None = None
|
|
165
|
+
token_count_source: str = UsageSource.LOCAL_ESTIMATE.value
|
|
166
|
+
token_count_confidence: str = UsageConfidence.ESTIMATED.value
|
|
167
|
+
local_request_estimate_tokens: int = 0
|
|
168
|
+
anchor_token_count_source: str | None = None
|
|
169
|
+
anchor_token_count_confidence: str | None = None
|
|
170
|
+
provider_projection_applied: bool = False
|
|
171
|
+
capacity_provider_key: str | None = None
|
|
172
|
+
context_window_source: str | None = None
|
|
173
|
+
max_output_tokens: int | None = None
|
|
174
|
+
max_output_source: str | None = None
|
|
175
|
+
safety_margin_tokens: int = 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True)
|
|
179
|
+
class RequestContextMeasurement:
|
|
180
|
+
"""Provider-visible input measurement anchored to persistent session state.
|
|
181
|
+
|
|
182
|
+
``input_tokens`` measures the fully assembled provider request, including
|
|
183
|
+
ephemeral controller messages. ``anchor_estimate_tokens`` is the local
|
|
184
|
+
estimate of that same full request. ``persistent_anchor_estimate_tokens``
|
|
185
|
+
is the local estimate of durable session state at that instant, allowing
|
|
186
|
+
the HUD to add later persistent growth without losing recurring ephemeral
|
|
187
|
+
request overhead.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
input_tokens: int
|
|
191
|
+
anchor_estimate_tokens: int
|
|
192
|
+
source: str
|
|
193
|
+
confidence: str
|
|
194
|
+
persistent_anchor_estimate_tokens: int = 0
|
|
195
|
+
requested_model: str = ""
|
|
196
|
+
provider_key: str | None = None
|
|
197
|
+
protocol: str | None = None
|
|
198
|
+
base_url_host: str | None = None
|
|
199
|
+
operation: str | None = None
|
|
200
|
+
request_mode: str | None = None
|
|
201
|
+
cache_strategy: str | None = None
|
|
202
|
+
request_message_signatures: tuple[str, ...] = ()
|
|
203
|
+
persistent_message_signatures: tuple[str, ...] = ()
|
|
204
|
+
tool_schema_signature: str = ""
|
|
205
|
+
request_has_media: bool = False
|
|
206
|
+
persistent_has_media: bool = False
|
|
207
|
+
|
|
208
|
+
def __post_init__(self) -> None:
|
|
209
|
+
for field_name, value in (
|
|
210
|
+
("input_tokens", self.input_tokens),
|
|
211
|
+
("anchor_estimate_tokens", self.anchor_estimate_tokens),
|
|
212
|
+
("persistent_anchor_estimate_tokens", self.persistent_anchor_estimate_tokens),
|
|
213
|
+
):
|
|
214
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
215
|
+
raise ValueError(f"{field_name} must be a non-negative integer")
|
|
216
|
+
|
|
217
|
+
def matches_route(
|
|
218
|
+
self,
|
|
219
|
+
*,
|
|
220
|
+
requested_model: str,
|
|
221
|
+
provider_key: str | None,
|
|
222
|
+
protocol: str | None,
|
|
223
|
+
base_url_host: str | None,
|
|
224
|
+
) -> bool:
|
|
225
|
+
expected = (
|
|
226
|
+
(self.requested_model, requested_model),
|
|
227
|
+
(self.provider_key, provider_key),
|
|
228
|
+
(self.protocol, protocol),
|
|
229
|
+
(self.base_url_host, base_url_host),
|
|
230
|
+
)
|
|
231
|
+
return all(not left or str(left) == str(right or "") for left, right in expected)
|
|
232
|
+
|
|
233
|
+
def projection_kind(
|
|
234
|
+
self,
|
|
235
|
+
*,
|
|
236
|
+
messages: list[dict[str, Any]],
|
|
237
|
+
tool_list: list[dict[str, Any]] | None,
|
|
238
|
+
) -> str | None:
|
|
239
|
+
current = request_message_signatures(messages)
|
|
240
|
+
if not self.request_message_signatures or self.anchor_estimate_tokens <= 0:
|
|
241
|
+
return None
|
|
242
|
+
if tool_schema_signature(tool_list) != self.tool_schema_signature:
|
|
243
|
+
return None
|
|
244
|
+
if current == self.request_message_signatures:
|
|
245
|
+
return "exact_request"
|
|
246
|
+
prefix = self.persistent_message_signatures
|
|
247
|
+
if (
|
|
248
|
+
not prefix
|
|
249
|
+
or self.persistent_anchor_estimate_tokens <= 0
|
|
250
|
+
or (self.request_has_media and not self.persistent_has_media)
|
|
251
|
+
or len(current) < len(prefix)
|
|
252
|
+
or current[: len(prefix)] != prefix
|
|
253
|
+
):
|
|
254
|
+
return None
|
|
255
|
+
if request_contains_media(messages[len(prefix) :]):
|
|
256
|
+
return None
|
|
257
|
+
return "append_only_projection"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass
|
|
261
|
+
class _ModelUsageTotals:
|
|
262
|
+
prompt_tokens: int = 0
|
|
263
|
+
completion_tokens: int = 0
|
|
264
|
+
total_tokens: int = 0
|
|
265
|
+
known_cost_usd: float = 0.0
|
|
266
|
+
known_cost_calls: int = 0
|
|
267
|
+
unknown_cost_count: int = 0
|
|
268
|
+
provider_reported_cost_calls: int = 0
|
|
269
|
+
catalog_estimated_cost_calls: int = 0
|
|
270
|
+
subscription_calls: int = 0
|
|
271
|
+
included_calls: int = 0
|
|
272
|
+
local_calls: int = 0
|
|
273
|
+
api_usage_calls: int = 0
|
|
274
|
+
estimate_usage_calls: int = 0
|
|
275
|
+
authoritative_usage_calls: int = 0
|
|
276
|
+
reported_usage_calls: int = 0
|
|
277
|
+
cached_prompt_tokens: int = 0
|
|
278
|
+
uncached_prompt_tokens: int = 0
|
|
279
|
+
input_tokens_uncached: int = 0
|
|
280
|
+
cache_read_input_tokens: int = 0
|
|
281
|
+
cache_creation_input_tokens: int = 0
|
|
282
|
+
cache_creation_5m_input_tokens: int = 0
|
|
283
|
+
cache_creation_1h_input_tokens: int = 0
|
|
284
|
+
input_tokens_uncached_reported_calls: int = 0
|
|
285
|
+
input_tokens_uncached_derived_calls: int = 0
|
|
286
|
+
cache_read_reported_calls: int = 0
|
|
287
|
+
cache_creation_reported_calls: int = 0
|
|
288
|
+
reasoning_tokens: int = 0
|
|
289
|
+
cache_cost_pricing_missing_calls: int = 0
|
|
290
|
+
estimated_bootstrap_prompt_tokens: int = 0
|
|
291
|
+
estimated_tool_schema_tokens: int = 0
|
|
292
|
+
estimated_live_conversation_history_tokens: int = 0
|
|
293
|
+
estimated_inline_tool_transcript_tokens: int = 0
|
|
294
|
+
estimated_memory_summary_tokens: int = 0
|
|
295
|
+
estimated_pins_tokens: int = 0
|
|
296
|
+
estimated_total_request_tokens: int = 0
|
|
297
|
+
tool_schema_budget_reported_calls: int = 0
|
|
298
|
+
tool_schema_budget_exceeded_calls: int = 0
|
|
299
|
+
tool_schema_budget_overage_tokens: int = 0
|
|
300
|
+
tool_schema_largest_tool_tokens: int = 0
|
|
301
|
+
corrected_usage_calls: int = 0
|
|
302
|
+
prompt_estimate_calibration_calls: int = 0
|
|
303
|
+
prompt_estimate_error_ratio_sum: float = 0.0
|
|
304
|
+
prompt_estimate_error_ratio_max: float = 0.0
|
|
305
|
+
prompt_estimate_underestimate_calls: int = 0
|
|
306
|
+
prompt_estimate_overestimate_calls: int = 0
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _calibration_group_key(record: UsageRecord) -> tuple[str, str, str, str, str, str, str]:
|
|
310
|
+
return (
|
|
311
|
+
record.requested_model.strip() or "unknown-model",
|
|
312
|
+
record.provider_key or "",
|
|
313
|
+
record.protocol or "",
|
|
314
|
+
record.base_url_host or "",
|
|
315
|
+
record.operation or "",
|
|
316
|
+
record.request_mode or "",
|
|
317
|
+
record.cache_strategy or "",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _record_matches_calibration_filters(
|
|
322
|
+
record: UsageRecord,
|
|
323
|
+
*,
|
|
324
|
+
requested_model: str,
|
|
325
|
+
provider_key: str,
|
|
326
|
+
protocol: str,
|
|
327
|
+
base_url_host_filter: str,
|
|
328
|
+
operation: str,
|
|
329
|
+
request_mode: str,
|
|
330
|
+
cache_strategy: str,
|
|
331
|
+
) -> bool:
|
|
332
|
+
return (
|
|
333
|
+
(not requested_model or record.requested_model.strip() == requested_model)
|
|
334
|
+
and (not provider_key or (record.provider_key or "") == provider_key)
|
|
335
|
+
and (not protocol or (record.protocol or "") == protocol)
|
|
336
|
+
and (not base_url_host_filter or (record.base_url_host or "") == base_url_host_filter)
|
|
337
|
+
and (not operation or (record.operation or "") == operation)
|
|
338
|
+
and (not request_mode or (record.request_mode or "") == request_mode)
|
|
339
|
+
and (not cache_strategy or (record.cache_strategy or "") == cache_strategy)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _messages_without_provider_metadata(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
344
|
+
return [
|
|
345
|
+
strip_provider_metadata_from_message(message)
|
|
346
|
+
for message in messages
|
|
347
|
+
if isinstance(message, dict)
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def estimate_prompt_tokens(messages: list[dict[str, Any]]) -> int:
|
|
352
|
+
if not messages:
|
|
353
|
+
return 0
|
|
354
|
+
serialized = json.dumps(
|
|
355
|
+
_messages_without_provider_metadata(messages),
|
|
356
|
+
ensure_ascii=False,
|
|
357
|
+
sort_keys=True,
|
|
358
|
+
)
|
|
359
|
+
return estimate_tokens(serialized)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _completion_payload_for_estimation(content: str, tool_calls: list[Any]) -> str:
|
|
363
|
+
if content.strip():
|
|
364
|
+
base = content
|
|
365
|
+
else:
|
|
366
|
+
base = ""
|
|
367
|
+
if tool_calls:
|
|
368
|
+
base += "\n" + json.dumps(tool_calls, ensure_ascii=False, sort_keys=True)
|
|
369
|
+
return base
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def estimate_completion_tokens(content: str, tool_calls: list[Any]) -> int:
|
|
373
|
+
base = _completion_payload_for_estimation(content, tool_calls)
|
|
374
|
+
if not base.strip():
|
|
375
|
+
return 0
|
|
376
|
+
return estimate_tokens(base)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _prompt_character_count(
|
|
380
|
+
messages: list[dict[str, Any]],
|
|
381
|
+
tool_list: list[dict[str, Any]] | None,
|
|
382
|
+
) -> int:
|
|
383
|
+
payload: dict[str, Any] = {"messages": _messages_without_provider_metadata(messages)}
|
|
384
|
+
if tool_list:
|
|
385
|
+
payload["tools"] = tool_list
|
|
386
|
+
return len(json.dumps(payload, ensure_ascii=False, sort_keys=True))
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _looks_like_character_count(
|
|
390
|
+
*,
|
|
391
|
+
api_count: int | None,
|
|
392
|
+
estimated_tokens: int,
|
|
393
|
+
character_count: int,
|
|
394
|
+
) -> bool:
|
|
395
|
+
if api_count is None or api_count <= 0 or estimated_tokens <= 0:
|
|
396
|
+
return False
|
|
397
|
+
if character_count <= 0:
|
|
398
|
+
return api_count >= max(estimated_tokens * 3, estimated_tokens + 256)
|
|
399
|
+
|
|
400
|
+
# Some OpenAI-compatible providers put character counts in token fields.
|
|
401
|
+
# Trust plausible API token counts, but replace counts that are much closer
|
|
402
|
+
# to raw text size than to the local tokenizer estimate.
|
|
403
|
+
minimum_delta = 64 if character_count < 2048 else 256
|
|
404
|
+
far_above_tokens = api_count >= max(estimated_tokens * 2.75, estimated_tokens + minimum_delta)
|
|
405
|
+
close_to_chars = abs(api_count - character_count) <= max(16, int(character_count * 0.15))
|
|
406
|
+
return far_above_tokens and close_to_chars
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _safe_int(value: Any) -> int | None:
|
|
410
|
+
try:
|
|
411
|
+
parsed = int(value)
|
|
412
|
+
except (TypeError, ValueError):
|
|
413
|
+
return None
|
|
414
|
+
if parsed < 0:
|
|
415
|
+
return None
|
|
416
|
+
return parsed
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _safe_float(value: Any) -> float | None:
|
|
420
|
+
try:
|
|
421
|
+
parsed = float(value)
|
|
422
|
+
except (TypeError, ValueError):
|
|
423
|
+
return None
|
|
424
|
+
return parsed
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _safe_bool(value: Any) -> bool:
|
|
428
|
+
if isinstance(value, bool):
|
|
429
|
+
return value
|
|
430
|
+
text = str(value or "").strip().lower()
|
|
431
|
+
return text in {"1", "true", "yes", "on"}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _safe_label(value: Any) -> str | None:
|
|
435
|
+
text = str(value or "").strip()
|
|
436
|
+
return text or None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _safe_request_plan_payload(payload: Any) -> dict[str, Any] | None:
|
|
440
|
+
if not isinstance(payload, Mapping):
|
|
441
|
+
return None
|
|
442
|
+
safe: dict[str, Any] = {}
|
|
443
|
+
for key in (
|
|
444
|
+
"input_mode",
|
|
445
|
+
"continuation_strategy",
|
|
446
|
+
"cache_strategy",
|
|
447
|
+
"cache_mode",
|
|
448
|
+
"cacheable_prefix_hash",
|
|
449
|
+
"request_messages_signature",
|
|
450
|
+
"tool_schema_hash",
|
|
451
|
+
):
|
|
452
|
+
value = _safe_label(payload.get(key))
|
|
453
|
+
if value is not None:
|
|
454
|
+
safe[key] = value
|
|
455
|
+
for key in ("previous_response_id_used", "fallback_used", "stream"):
|
|
456
|
+
if key in payload:
|
|
457
|
+
safe[key] = bool(payload.get(key))
|
|
458
|
+
for key in (
|
|
459
|
+
"schema_version",
|
|
460
|
+
"message_count",
|
|
461
|
+
"request_message_count",
|
|
462
|
+
"tool_count",
|
|
463
|
+
"stable_prefix_message_count",
|
|
464
|
+
"dynamic_suffix_message_count",
|
|
465
|
+
"provider_metadata_message_count",
|
|
466
|
+
"stable_prefix_estimated_tokens",
|
|
467
|
+
"dynamic_suffix_estimated_tokens",
|
|
468
|
+
"tool_schema_tokens",
|
|
469
|
+
"total_estimated_tokens",
|
|
470
|
+
"serialized_request_estimate_tokens",
|
|
471
|
+
"sent_serialized_request_estimate_tokens",
|
|
472
|
+
"full_input_item_count",
|
|
473
|
+
"sent_input_item_count",
|
|
474
|
+
"continuation_anchor_index",
|
|
475
|
+
"resent_stable_instruction_count",
|
|
476
|
+
):
|
|
477
|
+
value = _safe_int(payload.get(key))
|
|
478
|
+
if value is not None:
|
|
479
|
+
safe[key] = value
|
|
480
|
+
return safe or None
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _request_plan_from_response(response: Any | None) -> dict[str, Any] | None:
|
|
484
|
+
metadata = getattr(response, "provider_metadata", None)
|
|
485
|
+
if not isinstance(metadata, Mapping):
|
|
486
|
+
return None
|
|
487
|
+
direct = _safe_request_plan_payload(metadata.get("request_plan"))
|
|
488
|
+
if direct is not None:
|
|
489
|
+
return direct
|
|
490
|
+
for value in metadata.values():
|
|
491
|
+
if not isinstance(value, Mapping):
|
|
492
|
+
continue
|
|
493
|
+
nested = _safe_request_plan_payload(value.get("request_plan"))
|
|
494
|
+
if nested is not None:
|
|
495
|
+
return nested
|
|
496
|
+
return None
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _cache_strategy_from_response(response: Any | None) -> str | None:
|
|
500
|
+
metadata = getattr(response, "provider_metadata", None)
|
|
501
|
+
if not isinstance(metadata, Mapping):
|
|
502
|
+
return None
|
|
503
|
+
for value in metadata.values():
|
|
504
|
+
if not isinstance(value, Mapping):
|
|
505
|
+
continue
|
|
506
|
+
cache_policy = value.get("cache_policy")
|
|
507
|
+
if isinstance(cache_policy, Mapping):
|
|
508
|
+
strategy = _safe_label(cache_policy.get("strategy"))
|
|
509
|
+
if strategy is not None:
|
|
510
|
+
return strategy
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _protocol_from_client(client: Any | None) -> str | None:
|
|
515
|
+
for attr in ("protocol", "provider_protocol", "protocol_name"):
|
|
516
|
+
value = _safe_label(getattr(client, attr, None))
|
|
517
|
+
if value is not None:
|
|
518
|
+
return value
|
|
519
|
+
module = str(getattr(getattr(client, "__class__", None), "__module__", "") or "")
|
|
520
|
+
leaf = module.rsplit(".", 1)[-1]
|
|
521
|
+
if leaf in {
|
|
522
|
+
"openai_responses",
|
|
523
|
+
"openai_compat",
|
|
524
|
+
"anthropic_messages",
|
|
525
|
+
"gemini_generate_content",
|
|
526
|
+
"gemini_interactions",
|
|
527
|
+
}:
|
|
528
|
+
return leaf
|
|
529
|
+
class_name = str(getattr(getattr(client, "__class__", None), "__name__", "") or "")
|
|
530
|
+
if class_name:
|
|
531
|
+
return class_name
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def usage_context_from_client_response(
|
|
536
|
+
*,
|
|
537
|
+
client: Any | None,
|
|
538
|
+
response: Any | None,
|
|
539
|
+
operation: str,
|
|
540
|
+
request_plan: Mapping[str, Any] | None = None,
|
|
541
|
+
cache_strategy: str | None = None,
|
|
542
|
+
request_mode: str | None = None,
|
|
543
|
+
) -> dict[str, Any]:
|
|
544
|
+
safe_plan = _safe_request_plan_payload(request_plan) or _request_plan_from_response(response)
|
|
545
|
+
inferred_cache_strategy = (
|
|
546
|
+
_safe_label(cache_strategy)
|
|
547
|
+
or (
|
|
548
|
+
str(safe_plan.get("cache_strategy"))
|
|
549
|
+
if safe_plan and safe_plan.get("cache_strategy")
|
|
550
|
+
else None
|
|
551
|
+
)
|
|
552
|
+
or _cache_strategy_from_response(response)
|
|
553
|
+
)
|
|
554
|
+
inferred_request_mode = (
|
|
555
|
+
_safe_label(request_mode)
|
|
556
|
+
or (str(safe_plan.get("input_mode")) if safe_plan and safe_plan.get("input_mode") else None)
|
|
557
|
+
or "full"
|
|
558
|
+
)
|
|
559
|
+
usage_contract = getattr(client, "usage_contract", None)
|
|
560
|
+
if not isinstance(usage_contract, UsageContract):
|
|
561
|
+
usage_contract = UsageContract(
|
|
562
|
+
response_usage_confidence=(
|
|
563
|
+
UsageConfidence.AUTHORITATIVE
|
|
564
|
+
if bool(getattr(client, "usage_counts_authoritative", False))
|
|
565
|
+
else UsageConfidence.REPORTED
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
contract_billing_mode = getattr(usage_contract, "billing_mode", BillingMode.METERED_API)
|
|
569
|
+
billing_mode_value = str(
|
|
570
|
+
getattr(contract_billing_mode, "value", contract_billing_mode)
|
|
571
|
+
or BillingMode.METERED_API.value
|
|
572
|
+
)
|
|
573
|
+
return {
|
|
574
|
+
"provider_key": _safe_label(getattr(client, "provider_key", None)),
|
|
575
|
+
"protocol": _protocol_from_client(client),
|
|
576
|
+
"base_url_host": base_url_host(getattr(client, "base_url", None)),
|
|
577
|
+
"operation": _safe_label(operation),
|
|
578
|
+
"request_mode": inferred_request_mode,
|
|
579
|
+
"cache_strategy": inferred_cache_strategy,
|
|
580
|
+
"request_plan": safe_plan,
|
|
581
|
+
"api_usage_counts_authoritative": usage_contract.response_usage_authoritative,
|
|
582
|
+
"api_prompt_tokens_authoritative": usage_contract.response_usage_authoritative,
|
|
583
|
+
"api_usage_confidence": usage_contract.response_usage_confidence.value,
|
|
584
|
+
"api_usage_source_detail": UsageSource.PROVIDER_RESPONSE.value,
|
|
585
|
+
"api_output_includes_reasoning": (usage_contract.normalized_output_includes_reasoning),
|
|
586
|
+
"billing_mode": billing_mode_value,
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _prompt_estimate_calibration(
|
|
591
|
+
*,
|
|
592
|
+
api_prompt_tokens: int | None,
|
|
593
|
+
estimated_prompt_tokens: int,
|
|
594
|
+
corrected_fields: list[str],
|
|
595
|
+
usage_source: str,
|
|
596
|
+
api_prompt_tokens_authoritative: bool = False,
|
|
597
|
+
) -> tuple[int | None, float | None]:
|
|
598
|
+
if (
|
|
599
|
+
api_prompt_tokens is None
|
|
600
|
+
or estimated_prompt_tokens <= 0
|
|
601
|
+
or (usage_source != "api" and not api_prompt_tokens_authoritative)
|
|
602
|
+
or "prompt_tokens" in corrected_fields
|
|
603
|
+
):
|
|
604
|
+
return None, None
|
|
605
|
+
error_tokens = api_prompt_tokens - estimated_prompt_tokens
|
|
606
|
+
return error_tokens, api_prompt_tokens / max(1, estimated_prompt_tokens)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _robust_calibration_ratios(values: Iterable[float]) -> list[float]:
|
|
610
|
+
"""Return a coherent route-local sample without imposing a universal cap."""
|
|
611
|
+
|
|
612
|
+
ratios = [float(value) for value in values if math.isfinite(value) and value > 0]
|
|
613
|
+
if len(ratios) < 3:
|
|
614
|
+
return []
|
|
615
|
+
center = float(median(ratios))
|
|
616
|
+
deviations = [abs(value - center) for value in ratios]
|
|
617
|
+
mad = float(median(deviations))
|
|
618
|
+
# An evenly split or otherwise high-spread sample has no defensible center.
|
|
619
|
+
# Reject it instead of letting the uncapped upper mode become the p90.
|
|
620
|
+
if center <= 0 or (mad > 0 and (mad / center) > 0.25):
|
|
621
|
+
return []
|
|
622
|
+
tolerance = max(0.05, abs(center) * 0.10) if mad == 0 else max(0.05, mad * 5.2)
|
|
623
|
+
coherent = [value for value in ratios if abs(value - center) <= tolerance]
|
|
624
|
+
# A valid cluster must contain at least three calls and a strict majority;
|
|
625
|
+
# equal-sized modes remain unstable until one shape dominates.
|
|
626
|
+
return coherent if len(coherent) >= 3 and len(coherent) * 2 > len(ratios) else []
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _resolve_total_tokens(
|
|
630
|
+
*,
|
|
631
|
+
api_total_tokens: int | None,
|
|
632
|
+
prompt_tokens: int,
|
|
633
|
+
completion_tokens: int,
|
|
634
|
+
prompt_character_count: int,
|
|
635
|
+
completion_character_count: int,
|
|
636
|
+
) -> tuple[int, str | None]:
|
|
637
|
+
expected_total = prompt_tokens + completion_tokens
|
|
638
|
+
if api_total_tokens is None:
|
|
639
|
+
return expected_total, None
|
|
640
|
+
if api_total_tokens < expected_total:
|
|
641
|
+
return expected_total, "api_usage_total_below_component_sum"
|
|
642
|
+
if _looks_like_character_count(
|
|
643
|
+
api_count=api_total_tokens,
|
|
644
|
+
estimated_tokens=expected_total,
|
|
645
|
+
character_count=prompt_character_count + completion_character_count,
|
|
646
|
+
):
|
|
647
|
+
return expected_total, "api_usage_looked_like_character_counts:total_tokens"
|
|
648
|
+
return api_total_tokens, None
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _resolve_model_rate(
|
|
652
|
+
requested_meta: ModelMeta,
|
|
653
|
+
response_meta: ModelMeta | None,
|
|
654
|
+
field: str,
|
|
655
|
+
) -> float | None:
|
|
656
|
+
value = getattr(requested_meta, field, None)
|
|
657
|
+
if value is None and response_meta is not None:
|
|
658
|
+
value = getattr(response_meta, field, None)
|
|
659
|
+
return parse_rate(value)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def parse_rate(value: Any) -> float | None:
|
|
663
|
+
try:
|
|
664
|
+
parsed = float(value)
|
|
665
|
+
except (TypeError, ValueError):
|
|
666
|
+
return None
|
|
667
|
+
if parsed < 0:
|
|
668
|
+
return None
|
|
669
|
+
return parsed
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
@dataclass(frozen=True)
|
|
673
|
+
class _CostComputation:
|
|
674
|
+
cost_usd: float | None
|
|
675
|
+
cache_cost_pricing_missing: bool
|
|
676
|
+
cost_source: CostSource = CostSource.UNKNOWN
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _usage_cost_usd(
|
|
680
|
+
*,
|
|
681
|
+
prompt_tokens: int,
|
|
682
|
+
completion_tokens: int,
|
|
683
|
+
input_tokens_uncached: int | None,
|
|
684
|
+
cache_read_input_tokens: int | None,
|
|
685
|
+
cache_creation_input_tokens: int | None,
|
|
686
|
+
cache_creation_5m_input_tokens: int | None,
|
|
687
|
+
cache_creation_1h_input_tokens: int | None,
|
|
688
|
+
reasoning_tokens: int | None,
|
|
689
|
+
input_rate: float | None,
|
|
690
|
+
output_rate: float | None,
|
|
691
|
+
cache_read_input_rate: float | None,
|
|
692
|
+
cache_creation_input_rate: float | None,
|
|
693
|
+
cache_creation_5m_input_rate: float | None,
|
|
694
|
+
cache_creation_1h_input_rate: float | None,
|
|
695
|
+
reasoning_output_rate: float | None,
|
|
696
|
+
) -> _CostComputation:
|
|
697
|
+
if input_rate is None or output_rate is None:
|
|
698
|
+
return _CostComputation(cost_usd=None, cache_cost_pricing_missing=False)
|
|
699
|
+
|
|
700
|
+
read_tokens = max(0, cache_read_input_tokens or 0)
|
|
701
|
+
creation_5m_tokens = max(0, cache_creation_5m_input_tokens or 0)
|
|
702
|
+
creation_1h_tokens = max(0, cache_creation_1h_input_tokens or 0)
|
|
703
|
+
creation_total_tokens = max(0, cache_creation_input_tokens or 0)
|
|
704
|
+
creation_ttl_tokens = creation_5m_tokens + creation_1h_tokens
|
|
705
|
+
creation_generic_tokens = max(0, creation_total_tokens - creation_ttl_tokens)
|
|
706
|
+
if creation_total_tokens == 0:
|
|
707
|
+
creation_generic_tokens = 0
|
|
708
|
+
|
|
709
|
+
cache_pricing_missing = False
|
|
710
|
+
cost = 0.0
|
|
711
|
+
uncached_tokens = input_tokens_uncached
|
|
712
|
+
if uncached_tokens is None:
|
|
713
|
+
uncached_tokens = max(0, prompt_tokens - read_tokens - creation_total_tokens)
|
|
714
|
+
cost += max(0, uncached_tokens) * input_rate
|
|
715
|
+
|
|
716
|
+
if read_tokens > 0:
|
|
717
|
+
if cache_read_input_rate is None:
|
|
718
|
+
cache_pricing_missing = True
|
|
719
|
+
else:
|
|
720
|
+
cost += read_tokens * cache_read_input_rate
|
|
721
|
+
|
|
722
|
+
if creation_5m_tokens > 0:
|
|
723
|
+
rate = cache_creation_5m_input_rate or cache_creation_input_rate
|
|
724
|
+
if rate is None:
|
|
725
|
+
cache_pricing_missing = True
|
|
726
|
+
else:
|
|
727
|
+
cost += creation_5m_tokens * rate
|
|
728
|
+
|
|
729
|
+
if creation_1h_tokens > 0:
|
|
730
|
+
rate = cache_creation_1h_input_rate or cache_creation_input_rate
|
|
731
|
+
if rate is None:
|
|
732
|
+
cache_pricing_missing = True
|
|
733
|
+
else:
|
|
734
|
+
cost += creation_1h_tokens * rate
|
|
735
|
+
|
|
736
|
+
if creation_generic_tokens > 0:
|
|
737
|
+
if cache_creation_input_rate is None:
|
|
738
|
+
cache_pricing_missing = True
|
|
739
|
+
else:
|
|
740
|
+
cost += creation_generic_tokens * cache_creation_input_rate
|
|
741
|
+
|
|
742
|
+
billed_completion_tokens = max(0, completion_tokens)
|
|
743
|
+
reasoning = max(0, reasoning_tokens or 0)
|
|
744
|
+
if reasoning > 0 and reasoning_output_rate is not None:
|
|
745
|
+
visible_completion_tokens = max(0, billed_completion_tokens - reasoning)
|
|
746
|
+
cost += visible_completion_tokens * output_rate
|
|
747
|
+
cost += reasoning * reasoning_output_rate
|
|
748
|
+
else:
|
|
749
|
+
cost += billed_completion_tokens * output_rate
|
|
750
|
+
|
|
751
|
+
if cache_pricing_missing:
|
|
752
|
+
return _CostComputation(cost_usd=None, cache_cost_pricing_missing=True)
|
|
753
|
+
return _CostComputation(
|
|
754
|
+
cost_usd=cost,
|
|
755
|
+
cache_cost_pricing_missing=False,
|
|
756
|
+
cost_source=CostSource.CATALOG_ESTIMATE,
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def build_usage_record(
|
|
761
|
+
*,
|
|
762
|
+
role: str,
|
|
763
|
+
requested_model: str,
|
|
764
|
+
response_model: str | None,
|
|
765
|
+
messages: list[dict[str, Any]],
|
|
766
|
+
response_content: str,
|
|
767
|
+
response_tool_calls: list[Any],
|
|
768
|
+
api_prompt_tokens: int | None,
|
|
769
|
+
api_completion_tokens: int | None,
|
|
770
|
+
api_total_tokens: int | None,
|
|
771
|
+
registry: ModelRegistry,
|
|
772
|
+
api_usage: Any | None = None,
|
|
773
|
+
api_cached_prompt_tokens: int | None = None,
|
|
774
|
+
api_input_tokens_uncached: int | None = None,
|
|
775
|
+
api_input_tokens_uncached_derived: bool | None = None,
|
|
776
|
+
api_cache_read_input_tokens: int | None = None,
|
|
777
|
+
api_cache_creation_input_tokens: int | None = None,
|
|
778
|
+
api_cache_creation_5m_input_tokens: int | None = None,
|
|
779
|
+
api_cache_creation_1h_input_tokens: int | None = None,
|
|
780
|
+
api_reasoning_tokens: int | None = None,
|
|
781
|
+
api_provider_cost_usd: float | None = None,
|
|
782
|
+
api_raw_provider_usage: dict[str, Any] | None = None,
|
|
783
|
+
tool_list: list[dict[str, Any]] | None = None,
|
|
784
|
+
pinned_prefix_len: int = 0,
|
|
785
|
+
provider_key: str | None = None,
|
|
786
|
+
protocol: str | None = None,
|
|
787
|
+
base_url_host: str | None = None,
|
|
788
|
+
operation: str | None = None,
|
|
789
|
+
request_mode: str | None = None,
|
|
790
|
+
cache_strategy: str | None = None,
|
|
791
|
+
request_plan: Mapping[str, Any] | None = None,
|
|
792
|
+
api_usage_counts_authoritative: bool = False,
|
|
793
|
+
api_prompt_tokens_authoritative: bool | None = None,
|
|
794
|
+
api_usage_confidence: str = UsageConfidence.REPORTED.value,
|
|
795
|
+
api_usage_source_detail: str = UsageSource.PROVIDER_RESPONSE.value,
|
|
796
|
+
api_output_includes_reasoning: bool = True,
|
|
797
|
+
billing_mode: str = BillingMode.METERED_API.value,
|
|
798
|
+
) -> UsageRecord:
|
|
799
|
+
prompt_tokens_authoritative = (
|
|
800
|
+
bool(api_usage_counts_authoritative)
|
|
801
|
+
if api_prompt_tokens_authoritative is None
|
|
802
|
+
else bool(api_prompt_tokens_authoritative)
|
|
803
|
+
)
|
|
804
|
+
request_token_estimate = estimate_request_token_breakdown(
|
|
805
|
+
messages=messages,
|
|
806
|
+
tool_list=tool_list,
|
|
807
|
+
pinned_prefix_len=pinned_prefix_len,
|
|
808
|
+
)
|
|
809
|
+
estimated_prompt_tokens = request_token_estimate.total_tokens or estimate_prompt_tokens(
|
|
810
|
+
messages
|
|
811
|
+
)
|
|
812
|
+
safe_request_plan = _safe_request_plan_payload(request_plan)
|
|
813
|
+
provider_prompt_estimate_tokens = (
|
|
814
|
+
_safe_int(safe_request_plan.get("serialized_request_estimate_tokens"))
|
|
815
|
+
if safe_request_plan is not None
|
|
816
|
+
else None
|
|
817
|
+
)
|
|
818
|
+
fallback_prompt_estimate_tokens = (
|
|
819
|
+
provider_prompt_estimate_tokens
|
|
820
|
+
if provider_prompt_estimate_tokens is not None
|
|
821
|
+
else estimated_prompt_tokens
|
|
822
|
+
)
|
|
823
|
+
estimated_completion_tokens = estimate_completion_tokens(response_content, response_tool_calls)
|
|
824
|
+
prompt_character_count = _prompt_character_count(messages, tool_list)
|
|
825
|
+
completion_character_count = len(
|
|
826
|
+
_completion_payload_for_estimation(response_content, response_tool_calls)
|
|
827
|
+
)
|
|
828
|
+
if api_usage is not None:
|
|
829
|
+
reported_output_includes_reasoning = bool(
|
|
830
|
+
getattr(api_usage, "output_includes_reasoning", True)
|
|
831
|
+
)
|
|
832
|
+
reported_total_includes_reasoning = bool(
|
|
833
|
+
getattr(api_usage, "total_includes_reasoning", True)
|
|
834
|
+
)
|
|
835
|
+
normalizer = getattr(api_usage, "normalized", None)
|
|
836
|
+
if callable(normalizer):
|
|
837
|
+
api_usage = normalizer()
|
|
838
|
+
api_output_includes_reasoning = bool(
|
|
839
|
+
getattr(api_usage, "output_includes_reasoning", True)
|
|
840
|
+
)
|
|
841
|
+
if not reported_output_includes_reasoning:
|
|
842
|
+
api_completion_tokens = getattr(api_usage, "completion_tokens", None)
|
|
843
|
+
if not reported_total_includes_reasoning:
|
|
844
|
+
api_total_tokens = getattr(api_usage, "total_tokens", None)
|
|
845
|
+
if api_cached_prompt_tokens is None:
|
|
846
|
+
api_cached_prompt_tokens = getattr(api_usage, "cached_prompt_tokens", None)
|
|
847
|
+
if api_input_tokens_uncached is None:
|
|
848
|
+
api_input_tokens_uncached = getattr(api_usage, "input_tokens_uncached", None)
|
|
849
|
+
if api_input_tokens_uncached_derived is None:
|
|
850
|
+
api_input_tokens_uncached_derived = bool(
|
|
851
|
+
getattr(api_usage, "input_tokens_uncached_derived", False)
|
|
852
|
+
)
|
|
853
|
+
if api_cache_read_input_tokens is None:
|
|
854
|
+
api_cache_read_input_tokens = getattr(api_usage, "cache_read_input_tokens", None)
|
|
855
|
+
if api_cache_creation_input_tokens is None:
|
|
856
|
+
api_cache_creation_input_tokens = getattr(
|
|
857
|
+
api_usage,
|
|
858
|
+
"cache_creation_input_tokens",
|
|
859
|
+
None,
|
|
860
|
+
)
|
|
861
|
+
if api_cache_creation_5m_input_tokens is None:
|
|
862
|
+
api_cache_creation_5m_input_tokens = getattr(
|
|
863
|
+
api_usage,
|
|
864
|
+
"cache_creation_5m_input_tokens",
|
|
865
|
+
None,
|
|
866
|
+
)
|
|
867
|
+
if api_cache_creation_1h_input_tokens is None:
|
|
868
|
+
api_cache_creation_1h_input_tokens = getattr(
|
|
869
|
+
api_usage,
|
|
870
|
+
"cache_creation_1h_input_tokens",
|
|
871
|
+
None,
|
|
872
|
+
)
|
|
873
|
+
if api_reasoning_tokens is None:
|
|
874
|
+
api_reasoning_tokens = getattr(api_usage, "reasoning_tokens", None)
|
|
875
|
+
if api_provider_cost_usd is None:
|
|
876
|
+
api_provider_cost_usd = getattr(api_usage, "provider_cost_usd", None)
|
|
877
|
+
if api_raw_provider_usage is None:
|
|
878
|
+
api_raw_provider_usage = getattr(api_usage, "raw_provider_usage", None)
|
|
879
|
+
|
|
880
|
+
raw_api_prompt_tokens = _safe_int(api_prompt_tokens)
|
|
881
|
+
raw_api_completion_tokens = _safe_int(api_completion_tokens)
|
|
882
|
+
raw_api_total_tokens = _safe_int(api_total_tokens)
|
|
883
|
+
prompt_tokens = raw_api_prompt_tokens
|
|
884
|
+
completion_tokens = raw_api_completion_tokens
|
|
885
|
+
total_tokens = raw_api_total_tokens
|
|
886
|
+
prompt_measurement_source = str(api_usage_source_detail or "").strip()
|
|
887
|
+
prompt_measurement_is_local = prompt_measurement_source == UsageSource.LOCAL_ESTIMATE.value
|
|
888
|
+
any_provider_usage_reported = any(
|
|
889
|
+
value is not None for value in (raw_api_completion_tokens, raw_api_total_tokens)
|
|
890
|
+
) or (raw_api_prompt_tokens is not None and not prompt_measurement_is_local)
|
|
891
|
+
usage_source = "estimate" if prompt_measurement_is_local else "api"
|
|
892
|
+
corrected_fields: list[str] = []
|
|
893
|
+
# Reasoning models (OpenAI o-series / gpt-5, etc.) report output_tokens that
|
|
894
|
+
# INCLUDE hidden reasoning tokens, so the API completion count legitimately
|
|
895
|
+
# exceeds the visible response text. The character-count heuristic compares
|
|
896
|
+
# the API count against a visible-only estimate, so on a concise answer with
|
|
897
|
+
# heavy reasoning it false-positives and would overwrite a correct API count
|
|
898
|
+
# with a far-too-low estimate. Authoritative protocols bypass all such
|
|
899
|
+
# corrections; compatible providers also bypass the completion correction
|
|
900
|
+
# whenever they explicitly report reasoning tokens.
|
|
901
|
+
api_reasoning_present = (_safe_int(api_reasoning_tokens) or 0) > 0
|
|
902
|
+
|
|
903
|
+
if prompt_tokens is None:
|
|
904
|
+
prompt_tokens = fallback_prompt_estimate_tokens
|
|
905
|
+
usage_source = "estimate"
|
|
906
|
+
elif (
|
|
907
|
+
prompt_measurement_source == UsageSource.PROVIDER_RESPONSE.value
|
|
908
|
+
and not api_usage_counts_authoritative
|
|
909
|
+
and _looks_like_character_count(
|
|
910
|
+
api_count=prompt_tokens,
|
|
911
|
+
estimated_tokens=fallback_prompt_estimate_tokens,
|
|
912
|
+
character_count=prompt_character_count,
|
|
913
|
+
)
|
|
914
|
+
):
|
|
915
|
+
prompt_tokens = fallback_prompt_estimate_tokens
|
|
916
|
+
usage_source = "estimate"
|
|
917
|
+
corrected_fields.append("prompt_tokens")
|
|
918
|
+
if completion_tokens is None:
|
|
919
|
+
completion_tokens = estimated_completion_tokens
|
|
920
|
+
usage_source = "estimate"
|
|
921
|
+
elif (
|
|
922
|
+
not api_usage_counts_authoritative
|
|
923
|
+
and not api_reasoning_present
|
|
924
|
+
and _looks_like_character_count(
|
|
925
|
+
api_count=completion_tokens,
|
|
926
|
+
estimated_tokens=estimated_completion_tokens,
|
|
927
|
+
character_count=completion_character_count,
|
|
928
|
+
)
|
|
929
|
+
):
|
|
930
|
+
completion_tokens = estimated_completion_tokens
|
|
931
|
+
usage_source = "estimate"
|
|
932
|
+
corrected_fields.append("completion_tokens")
|
|
933
|
+
total_correction_reason: str | None = None
|
|
934
|
+
if total_tokens is None or usage_source == "estimate":
|
|
935
|
+
total_tokens = prompt_tokens + completion_tokens
|
|
936
|
+
if usage_source != "estimate":
|
|
937
|
+
usage_source = "api"
|
|
938
|
+
elif api_usage_counts_authoritative:
|
|
939
|
+
# Native/provider-owned protocols define these counts as part of their
|
|
940
|
+
# response contract. Preserve them exactly, including non-visible
|
|
941
|
+
# reasoning, formatting, and tool-structure tokens.
|
|
942
|
+
total_correction_reason = None
|
|
943
|
+
else:
|
|
944
|
+
total_tokens, total_correction_reason = _resolve_total_tokens(
|
|
945
|
+
api_total_tokens=total_tokens,
|
|
946
|
+
prompt_tokens=prompt_tokens,
|
|
947
|
+
completion_tokens=completion_tokens,
|
|
948
|
+
prompt_character_count=prompt_character_count,
|
|
949
|
+
completion_character_count=completion_character_count,
|
|
950
|
+
)
|
|
951
|
+
if total_correction_reason is not None:
|
|
952
|
+
corrected_fields.append("total_tokens")
|
|
953
|
+
if usage_source == "estimate":
|
|
954
|
+
usage_source_detail = (
|
|
955
|
+
UsageSource.MIXED.value
|
|
956
|
+
if any_provider_usage_reported
|
|
957
|
+
else UsageSource.LOCAL_ESTIMATE.value
|
|
958
|
+
)
|
|
959
|
+
usage_confidence = UsageConfidence.ESTIMATED.value
|
|
960
|
+
else:
|
|
961
|
+
usage_source_detail = (
|
|
962
|
+
str(api_usage_source_detail or "").strip() or UsageSource.PROVIDER_RESPONSE.value
|
|
963
|
+
)
|
|
964
|
+
requested_confidence = str(api_usage_confidence or "").strip().lower()
|
|
965
|
+
if api_usage_counts_authoritative and prompt_tokens_authoritative:
|
|
966
|
+
usage_confidence = UsageConfidence.AUTHORITATIVE.value
|
|
967
|
+
elif requested_confidence in {item.value for item in UsageConfidence}:
|
|
968
|
+
usage_confidence = requested_confidence
|
|
969
|
+
else:
|
|
970
|
+
usage_confidence = UsageConfidence.REPORTED.value
|
|
971
|
+
cache_read_input_tokens = _safe_int(api_cache_read_input_tokens)
|
|
972
|
+
if cache_read_input_tokens is None:
|
|
973
|
+
cache_read_input_tokens = _safe_int(api_cached_prompt_tokens)
|
|
974
|
+
cache_creation_5m_input_tokens = _safe_int(api_cache_creation_5m_input_tokens)
|
|
975
|
+
cache_creation_1h_input_tokens = _safe_int(api_cache_creation_1h_input_tokens)
|
|
976
|
+
cache_creation_input_tokens = _safe_int(api_cache_creation_input_tokens)
|
|
977
|
+
if cache_creation_input_tokens is None:
|
|
978
|
+
creation_parts = [
|
|
979
|
+
value
|
|
980
|
+
for value in (cache_creation_5m_input_tokens, cache_creation_1h_input_tokens)
|
|
981
|
+
if value is not None
|
|
982
|
+
]
|
|
983
|
+
if creation_parts:
|
|
984
|
+
cache_creation_input_tokens = sum(creation_parts)
|
|
985
|
+
input_tokens_uncached = _safe_int(api_input_tokens_uncached)
|
|
986
|
+
input_tokens_uncached_derived = bool(api_input_tokens_uncached_derived)
|
|
987
|
+
reasoning_tokens = _safe_int(api_reasoning_tokens)
|
|
988
|
+
raw_provider_usage = (
|
|
989
|
+
copy.deepcopy(api_raw_provider_usage) if isinstance(api_raw_provider_usage, dict) else None
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
if "prompt_tokens" in corrected_fields:
|
|
993
|
+
cache_read_input_tokens = None
|
|
994
|
+
cache_creation_input_tokens = None
|
|
995
|
+
cache_creation_5m_input_tokens = None
|
|
996
|
+
cache_creation_1h_input_tokens = None
|
|
997
|
+
input_tokens_uncached = None
|
|
998
|
+
input_tokens_uncached_derived = False
|
|
999
|
+
|
|
1000
|
+
cached_prompt_tokens = cache_read_input_tokens
|
|
1001
|
+
uncached_prompt_tokens: int | None = None
|
|
1002
|
+
if prompt_tokens is not None:
|
|
1003
|
+
if cached_prompt_tokens is not None:
|
|
1004
|
+
uncached_prompt_tokens = max(0, prompt_tokens - cached_prompt_tokens)
|
|
1005
|
+
elif input_tokens_uncached is not None:
|
|
1006
|
+
uncached_prompt_tokens = input_tokens_uncached
|
|
1007
|
+
if input_tokens_uncached is None and prompt_tokens is not None:
|
|
1008
|
+
cached_read = cache_read_input_tokens or 0
|
|
1009
|
+
cache_creation = cache_creation_input_tokens or 0
|
|
1010
|
+
if cached_read > 0 or cache_creation > 0:
|
|
1011
|
+
input_tokens_uncached = max(0, prompt_tokens - cached_read - cache_creation)
|
|
1012
|
+
input_tokens_uncached_derived = True
|
|
1013
|
+
prompt_estimate_error_tokens, prompt_estimate_error_ratio = _prompt_estimate_calibration(
|
|
1014
|
+
api_prompt_tokens=(None if prompt_measurement_is_local else raw_api_prompt_tokens),
|
|
1015
|
+
estimated_prompt_tokens=fallback_prompt_estimate_tokens,
|
|
1016
|
+
corrected_fields=corrected_fields,
|
|
1017
|
+
usage_source=usage_source,
|
|
1018
|
+
api_prompt_tokens_authoritative=prompt_tokens_authoritative,
|
|
1019
|
+
)
|
|
1020
|
+
correction_reasons: list[str] = []
|
|
1021
|
+
character_count_corrected_fields = [
|
|
1022
|
+
field for field in corrected_fields if field in {"prompt_tokens", "completion_tokens"}
|
|
1023
|
+
]
|
|
1024
|
+
if character_count_corrected_fields:
|
|
1025
|
+
correction_reasons.append(
|
|
1026
|
+
"api_usage_looked_like_character_counts:" + ",".join(character_count_corrected_fields)
|
|
1027
|
+
)
|
|
1028
|
+
if total_correction_reason is not None:
|
|
1029
|
+
correction_reasons.append(total_correction_reason)
|
|
1030
|
+
usage_correction_reason = ";".join(correction_reasons) if correction_reasons else None
|
|
1031
|
+
requested_meta = registry.get(requested_model)
|
|
1032
|
+
response_meta: ModelMeta | None = None
|
|
1033
|
+
if response_model:
|
|
1034
|
+
response_meta = registry.get(response_model)
|
|
1035
|
+
|
|
1036
|
+
input_rate = requested_meta.input_cost_per_token
|
|
1037
|
+
output_rate = requested_meta.output_cost_per_token
|
|
1038
|
+
if input_rate is None and response_meta is not None:
|
|
1039
|
+
input_rate = response_meta.input_cost_per_token
|
|
1040
|
+
if output_rate is None and response_meta is not None:
|
|
1041
|
+
output_rate = response_meta.output_cost_per_token
|
|
1042
|
+
cache_read_input_rate = _resolve_model_rate(
|
|
1043
|
+
requested_meta,
|
|
1044
|
+
response_meta,
|
|
1045
|
+
"cache_read_input_cost_per_token",
|
|
1046
|
+
)
|
|
1047
|
+
cache_creation_input_rate = _resolve_model_rate(
|
|
1048
|
+
requested_meta,
|
|
1049
|
+
response_meta,
|
|
1050
|
+
"cache_creation_input_cost_per_token",
|
|
1051
|
+
)
|
|
1052
|
+
cache_creation_5m_input_rate = _resolve_model_rate(
|
|
1053
|
+
requested_meta,
|
|
1054
|
+
response_meta,
|
|
1055
|
+
"cache_creation_5m_input_cost_per_token",
|
|
1056
|
+
)
|
|
1057
|
+
cache_creation_1h_input_rate = _resolve_model_rate(
|
|
1058
|
+
requested_meta,
|
|
1059
|
+
response_meta,
|
|
1060
|
+
"cache_creation_1h_input_cost_per_token",
|
|
1061
|
+
)
|
|
1062
|
+
reasoning_output_rate = _resolve_model_rate(
|
|
1063
|
+
requested_meta,
|
|
1064
|
+
response_meta,
|
|
1065
|
+
"reasoning_output_cost_per_token",
|
|
1066
|
+
)
|
|
1067
|
+
safe_billing_mode = str(billing_mode or "").strip().lower()
|
|
1068
|
+
if safe_billing_mode not in {item.value for item in BillingMode}:
|
|
1069
|
+
safe_billing_mode = BillingMode.UNKNOWN.value
|
|
1070
|
+
provider_cost_usd = _safe_float(api_provider_cost_usd)
|
|
1071
|
+
if safe_billing_mode == BillingMode.SUBSCRIPTION.value:
|
|
1072
|
+
cost = _CostComputation(None, False, CostSource.SUBSCRIPTION)
|
|
1073
|
+
elif safe_billing_mode == BillingMode.INCLUDED.value:
|
|
1074
|
+
cost = _CostComputation(None, False, CostSource.INCLUDED)
|
|
1075
|
+
elif safe_billing_mode == BillingMode.LOCAL.value:
|
|
1076
|
+
cost = _CostComputation(None, False, CostSource.LOCAL)
|
|
1077
|
+
elif provider_cost_usd is not None and provider_cost_usd >= 0:
|
|
1078
|
+
cost = _CostComputation(
|
|
1079
|
+
cost_usd=provider_cost_usd,
|
|
1080
|
+
cache_cost_pricing_missing=False,
|
|
1081
|
+
cost_source=CostSource.PROVIDER_REPORTED,
|
|
1082
|
+
)
|
|
1083
|
+
else:
|
|
1084
|
+
cost = _usage_cost_usd(
|
|
1085
|
+
prompt_tokens=prompt_tokens,
|
|
1086
|
+
completion_tokens=completion_tokens,
|
|
1087
|
+
input_tokens_uncached=input_tokens_uncached,
|
|
1088
|
+
cache_read_input_tokens=cache_read_input_tokens,
|
|
1089
|
+
cache_creation_input_tokens=cache_creation_input_tokens,
|
|
1090
|
+
cache_creation_5m_input_tokens=cache_creation_5m_input_tokens,
|
|
1091
|
+
cache_creation_1h_input_tokens=cache_creation_1h_input_tokens,
|
|
1092
|
+
reasoning_tokens=reasoning_tokens,
|
|
1093
|
+
input_rate=input_rate,
|
|
1094
|
+
output_rate=output_rate,
|
|
1095
|
+
cache_read_input_rate=cache_read_input_rate,
|
|
1096
|
+
cache_creation_input_rate=cache_creation_input_rate,
|
|
1097
|
+
cache_creation_5m_input_rate=cache_creation_5m_input_rate,
|
|
1098
|
+
cache_creation_1h_input_rate=cache_creation_1h_input_rate,
|
|
1099
|
+
reasoning_output_rate=reasoning_output_rate,
|
|
1100
|
+
)
|
|
1101
|
+
safe_request_mode = _safe_label(request_mode) or (
|
|
1102
|
+
str(safe_request_plan.get("input_mode"))
|
|
1103
|
+
if safe_request_plan and safe_request_plan.get("input_mode")
|
|
1104
|
+
else None
|
|
1105
|
+
)
|
|
1106
|
+
safe_cache_strategy = _safe_label(cache_strategy) or (
|
|
1107
|
+
str(safe_request_plan.get("cache_strategy"))
|
|
1108
|
+
if safe_request_plan and safe_request_plan.get("cache_strategy")
|
|
1109
|
+
else None
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
return UsageRecord(
|
|
1113
|
+
usage_schema_version=6,
|
|
1114
|
+
timestamp=now_iso(),
|
|
1115
|
+
role=role,
|
|
1116
|
+
requested_model=requested_model,
|
|
1117
|
+
response_model=response_model,
|
|
1118
|
+
prompt_tokens=prompt_tokens,
|
|
1119
|
+
completion_tokens=completion_tokens,
|
|
1120
|
+
total_tokens=total_tokens,
|
|
1121
|
+
input_cost_per_token=input_rate,
|
|
1122
|
+
output_cost_per_token=output_rate,
|
|
1123
|
+
cache_read_input_cost_per_token=cache_read_input_rate,
|
|
1124
|
+
cache_creation_input_cost_per_token=cache_creation_input_rate,
|
|
1125
|
+
cache_creation_5m_input_cost_per_token=cache_creation_5m_input_rate,
|
|
1126
|
+
cache_creation_1h_input_cost_per_token=cache_creation_1h_input_rate,
|
|
1127
|
+
reasoning_output_cost_per_token=reasoning_output_rate,
|
|
1128
|
+
cost_usd=cost.cost_usd,
|
|
1129
|
+
billing_mode=safe_billing_mode,
|
|
1130
|
+
cost_source=cost.cost_source.value,
|
|
1131
|
+
provider_cost_usd=provider_cost_usd,
|
|
1132
|
+
usage_source=usage_source,
|
|
1133
|
+
usage_source_detail=usage_source_detail,
|
|
1134
|
+
usage_confidence=usage_confidence,
|
|
1135
|
+
output_includes_reasoning=bool(api_output_includes_reasoning),
|
|
1136
|
+
provider_key=_safe_label(provider_key),
|
|
1137
|
+
protocol=_safe_label(protocol),
|
|
1138
|
+
base_url_host=_safe_label(base_url_host),
|
|
1139
|
+
operation=_safe_label(operation),
|
|
1140
|
+
request_mode=safe_request_mode,
|
|
1141
|
+
cache_strategy=safe_cache_strategy,
|
|
1142
|
+
request_plan=safe_request_plan,
|
|
1143
|
+
request_has_media=request_contains_media(messages),
|
|
1144
|
+
cached_prompt_tokens=cached_prompt_tokens,
|
|
1145
|
+
uncached_prompt_tokens=uncached_prompt_tokens,
|
|
1146
|
+
input_tokens_uncached=input_tokens_uncached,
|
|
1147
|
+
input_tokens_uncached_derived=input_tokens_uncached_derived,
|
|
1148
|
+
cache_read_input_tokens=cache_read_input_tokens,
|
|
1149
|
+
cache_creation_input_tokens=cache_creation_input_tokens,
|
|
1150
|
+
cache_creation_5m_input_tokens=cache_creation_5m_input_tokens,
|
|
1151
|
+
cache_creation_1h_input_tokens=cache_creation_1h_input_tokens,
|
|
1152
|
+
reasoning_tokens=reasoning_tokens,
|
|
1153
|
+
raw_provider_usage=raw_provider_usage,
|
|
1154
|
+
cache_cost_pricing_missing=cost.cache_cost_pricing_missing,
|
|
1155
|
+
request_token_estimate=request_token_estimate,
|
|
1156
|
+
prompt_estimate_tokens=fallback_prompt_estimate_tokens,
|
|
1157
|
+
prompt_estimate_error_tokens=prompt_estimate_error_tokens,
|
|
1158
|
+
prompt_estimate_error_ratio=prompt_estimate_error_ratio,
|
|
1159
|
+
raw_api_prompt_tokens=raw_api_prompt_tokens if corrected_fields else None,
|
|
1160
|
+
raw_api_completion_tokens=raw_api_completion_tokens if corrected_fields else None,
|
|
1161
|
+
raw_api_total_tokens=raw_api_total_tokens if corrected_fields else None,
|
|
1162
|
+
usage_correction_reason=usage_correction_reason,
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
class UsageSummary:
|
|
1167
|
+
def __init__(self) -> None:
|
|
1168
|
+
self._lock = threading.RLock()
|
|
1169
|
+
self._by_model: dict[str, _ModelUsageTotals] = {}
|
|
1170
|
+
self._records: list[UsageRecord] = []
|
|
1171
|
+
self.calls = 0
|
|
1172
|
+
|
|
1173
|
+
def add_record(self, record: UsageRecord) -> None:
|
|
1174
|
+
with self._lock:
|
|
1175
|
+
key = record.requested_model.strip() or "unknown-model"
|
|
1176
|
+
totals = self._by_model.setdefault(key, _ModelUsageTotals())
|
|
1177
|
+
self._records.append(record)
|
|
1178
|
+
totals.prompt_tokens += record.prompt_tokens
|
|
1179
|
+
totals.completion_tokens += record.completion_tokens
|
|
1180
|
+
totals.total_tokens += record.total_tokens
|
|
1181
|
+
if record.cost_usd is None:
|
|
1182
|
+
if record.cost_source == CostSource.SUBSCRIPTION.value:
|
|
1183
|
+
totals.subscription_calls += 1
|
|
1184
|
+
elif record.cost_source == CostSource.INCLUDED.value:
|
|
1185
|
+
totals.included_calls += 1
|
|
1186
|
+
elif record.cost_source == CostSource.LOCAL.value:
|
|
1187
|
+
totals.local_calls += 1
|
|
1188
|
+
else:
|
|
1189
|
+
totals.unknown_cost_count += 1
|
|
1190
|
+
else:
|
|
1191
|
+
totals.known_cost_usd += float(record.cost_usd)
|
|
1192
|
+
totals.known_cost_calls += 1
|
|
1193
|
+
if record.cost_source == CostSource.PROVIDER_REPORTED.value:
|
|
1194
|
+
totals.provider_reported_cost_calls += 1
|
|
1195
|
+
elif record.cost_source == CostSource.CATALOG_ESTIMATE.value:
|
|
1196
|
+
totals.catalog_estimated_cost_calls += 1
|
|
1197
|
+
if record.usage_source == "api":
|
|
1198
|
+
totals.api_usage_calls += 1
|
|
1199
|
+
else:
|
|
1200
|
+
totals.estimate_usage_calls += 1
|
|
1201
|
+
if record.usage_confidence == UsageConfidence.AUTHORITATIVE.value:
|
|
1202
|
+
totals.authoritative_usage_calls += 1
|
|
1203
|
+
elif record.usage_confidence == UsageConfidence.REPORTED.value:
|
|
1204
|
+
totals.reported_usage_calls += 1
|
|
1205
|
+
if record.usage_correction_reason:
|
|
1206
|
+
totals.corrected_usage_calls += 1
|
|
1207
|
+
if record.prompt_estimate_error_ratio is not None:
|
|
1208
|
+
totals.prompt_estimate_calibration_calls += 1
|
|
1209
|
+
totals.prompt_estimate_error_ratio_sum += float(record.prompt_estimate_error_ratio)
|
|
1210
|
+
totals.prompt_estimate_error_ratio_max = max(
|
|
1211
|
+
totals.prompt_estimate_error_ratio_max,
|
|
1212
|
+
float(record.prompt_estimate_error_ratio),
|
|
1213
|
+
)
|
|
1214
|
+
error_tokens = record.prompt_estimate_error_tokens or 0
|
|
1215
|
+
if error_tokens > 0:
|
|
1216
|
+
totals.prompt_estimate_underestimate_calls += 1
|
|
1217
|
+
elif error_tokens < 0:
|
|
1218
|
+
totals.prompt_estimate_overestimate_calls += 1
|
|
1219
|
+
totals.cached_prompt_tokens += max(0, record.cached_prompt_tokens or 0)
|
|
1220
|
+
totals.uncached_prompt_tokens += max(0, record.uncached_prompt_tokens or 0)
|
|
1221
|
+
totals.input_tokens_uncached += max(0, record.input_tokens_uncached or 0)
|
|
1222
|
+
totals.cache_read_input_tokens += max(0, record.cache_read_input_tokens or 0)
|
|
1223
|
+
totals.cache_creation_input_tokens += max(0, record.cache_creation_input_tokens or 0)
|
|
1224
|
+
if record.input_tokens_uncached is not None:
|
|
1225
|
+
if record.input_tokens_uncached_derived:
|
|
1226
|
+
totals.input_tokens_uncached_derived_calls += 1
|
|
1227
|
+
else:
|
|
1228
|
+
totals.input_tokens_uncached_reported_calls += 1
|
|
1229
|
+
if record.cache_read_input_tokens is not None:
|
|
1230
|
+
totals.cache_read_reported_calls += 1
|
|
1231
|
+
if record.cache_creation_input_tokens is not None:
|
|
1232
|
+
totals.cache_creation_reported_calls += 1
|
|
1233
|
+
totals.cache_creation_5m_input_tokens += max(
|
|
1234
|
+
0,
|
|
1235
|
+
record.cache_creation_5m_input_tokens or 0,
|
|
1236
|
+
)
|
|
1237
|
+
totals.cache_creation_1h_input_tokens += max(
|
|
1238
|
+
0,
|
|
1239
|
+
record.cache_creation_1h_input_tokens or 0,
|
|
1240
|
+
)
|
|
1241
|
+
totals.reasoning_tokens += max(0, record.reasoning_tokens or 0)
|
|
1242
|
+
if record.cache_cost_pricing_missing:
|
|
1243
|
+
totals.cache_cost_pricing_missing_calls += 1
|
|
1244
|
+
breakdown = record.request_token_estimate
|
|
1245
|
+
if breakdown is not None:
|
|
1246
|
+
totals.estimated_bootstrap_prompt_tokens += breakdown.bootstrap_prompt_tokens
|
|
1247
|
+
totals.estimated_tool_schema_tokens += breakdown.tool_schema_tokens
|
|
1248
|
+
totals.estimated_live_conversation_history_tokens += (
|
|
1249
|
+
breakdown.live_conversation_history_tokens
|
|
1250
|
+
)
|
|
1251
|
+
totals.estimated_inline_tool_transcript_tokens += (
|
|
1252
|
+
breakdown.inline_tool_transcript_tokens
|
|
1253
|
+
)
|
|
1254
|
+
totals.estimated_memory_summary_tokens += breakdown.memory_summary_tokens
|
|
1255
|
+
totals.estimated_pins_tokens += breakdown.pins_tokens
|
|
1256
|
+
totals.estimated_total_request_tokens += breakdown.total_tokens
|
|
1257
|
+
tool_budget = breakdown.tool_schema_budget
|
|
1258
|
+
if tool_budget is not None:
|
|
1259
|
+
totals.tool_schema_budget_reported_calls += 1
|
|
1260
|
+
totals.tool_schema_budget_overage_tokens += tool_budget.over_budget_tokens
|
|
1261
|
+
if tool_budget.over_budget:
|
|
1262
|
+
totals.tool_schema_budget_exceeded_calls += 1
|
|
1263
|
+
if tool_budget.largest_tools:
|
|
1264
|
+
totals.tool_schema_largest_tool_tokens = max(
|
|
1265
|
+
totals.tool_schema_largest_tool_tokens,
|
|
1266
|
+
tool_budget.largest_tools[0].token_estimate,
|
|
1267
|
+
)
|
|
1268
|
+
self.calls += 1
|
|
1269
|
+
|
|
1270
|
+
def records(self) -> list[UsageRecord]:
|
|
1271
|
+
with self._lock:
|
|
1272
|
+
return list(self._records)
|
|
1273
|
+
|
|
1274
|
+
def cache_efficiency_summary(self) -> dict[str, Any]:
|
|
1275
|
+
with self._lock:
|
|
1276
|
+
reported = [
|
|
1277
|
+
(position, record)
|
|
1278
|
+
for position, record in enumerate(self._records, start=1)
|
|
1279
|
+
if record.cached_prompt_tokens is not None
|
|
1280
|
+
or record.uncached_prompt_tokens is not None
|
|
1281
|
+
]
|
|
1282
|
+
cached = sum(max(0, record.cached_prompt_tokens or 0) for _, record in reported)
|
|
1283
|
+
uncached = sum(max(0, record.uncached_prompt_tokens or 0) for _, record in reported)
|
|
1284
|
+
denominator = cached + uncached
|
|
1285
|
+
largest = max(
|
|
1286
|
+
reported,
|
|
1287
|
+
key=lambda item: max(0, item[1].uncached_prompt_tokens or 0),
|
|
1288
|
+
default=None,
|
|
1289
|
+
)
|
|
1290
|
+
largest_payload: dict[str, Any] | None = None
|
|
1291
|
+
if largest is not None:
|
|
1292
|
+
position, record = largest
|
|
1293
|
+
largest_payload = {
|
|
1294
|
+
"call_position": position,
|
|
1295
|
+
"uncached_prompt_tokens": max(
|
|
1296
|
+
0,
|
|
1297
|
+
record.uncached_prompt_tokens or 0,
|
|
1298
|
+
),
|
|
1299
|
+
"role": record.role,
|
|
1300
|
+
"requested_model": record.requested_model,
|
|
1301
|
+
"timestamp": record.timestamp,
|
|
1302
|
+
}
|
|
1303
|
+
return {
|
|
1304
|
+
"reported_calls": len(reported),
|
|
1305
|
+
"cached_prompt_tokens": cached,
|
|
1306
|
+
"uncached_prompt_tokens": uncached,
|
|
1307
|
+
"hit_ratio": cached / denominator if denominator > 0 else None,
|
|
1308
|
+
"largest_uncached_call": largest_payload,
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
def merge_records(self, records: Iterable[UsageRecord]) -> int:
|
|
1312
|
+
merged = 0
|
|
1313
|
+
for record in records:
|
|
1314
|
+
self.add_record(record)
|
|
1315
|
+
merged += 1
|
|
1316
|
+
return merged
|
|
1317
|
+
|
|
1318
|
+
def add_event_payload(self, payload: dict[str, Any]) -> None:
|
|
1319
|
+
if str(payload.get("event_type") or "") != "llm_usage":
|
|
1320
|
+
return
|
|
1321
|
+
record = UsageRecord(
|
|
1322
|
+
usage_schema_version=max(1, _safe_int(payload.get("usage_schema_version")) or 1),
|
|
1323
|
+
timestamp=str(payload.get("timestamp") or ""),
|
|
1324
|
+
role=str(payload.get("role") or "main"),
|
|
1325
|
+
requested_model=str(payload.get("requested_model") or "unknown-model"),
|
|
1326
|
+
response_model=(
|
|
1327
|
+
str(payload.get("response_model"))
|
|
1328
|
+
if payload.get("response_model") is not None
|
|
1329
|
+
else None
|
|
1330
|
+
),
|
|
1331
|
+
prompt_tokens=max(0, _safe_int(payload.get("prompt_tokens")) or 0),
|
|
1332
|
+
completion_tokens=max(0, _safe_int(payload.get("completion_tokens")) or 0),
|
|
1333
|
+
total_tokens=max(0, _safe_int(payload.get("total_tokens")) or 0),
|
|
1334
|
+
input_cost_per_token=(
|
|
1335
|
+
_safe_float(payload.get("input_cost_per_token"))
|
|
1336
|
+
if payload.get("input_cost_per_token") is not None
|
|
1337
|
+
else None
|
|
1338
|
+
),
|
|
1339
|
+
output_cost_per_token=(
|
|
1340
|
+
_safe_float(payload.get("output_cost_per_token"))
|
|
1341
|
+
if payload.get("output_cost_per_token") is not None
|
|
1342
|
+
else None
|
|
1343
|
+
),
|
|
1344
|
+
cache_read_input_cost_per_token=(
|
|
1345
|
+
_safe_float(payload.get("cache_read_input_cost_per_token"))
|
|
1346
|
+
if payload.get("cache_read_input_cost_per_token") is not None
|
|
1347
|
+
else None
|
|
1348
|
+
),
|
|
1349
|
+
cache_creation_input_cost_per_token=(
|
|
1350
|
+
_safe_float(payload.get("cache_creation_input_cost_per_token"))
|
|
1351
|
+
if payload.get("cache_creation_input_cost_per_token") is not None
|
|
1352
|
+
else None
|
|
1353
|
+
),
|
|
1354
|
+
cache_creation_5m_input_cost_per_token=(
|
|
1355
|
+
_safe_float(payload.get("cache_creation_5m_input_cost_per_token"))
|
|
1356
|
+
if payload.get("cache_creation_5m_input_cost_per_token") is not None
|
|
1357
|
+
else None
|
|
1358
|
+
),
|
|
1359
|
+
cache_creation_1h_input_cost_per_token=(
|
|
1360
|
+
_safe_float(payload.get("cache_creation_1h_input_cost_per_token"))
|
|
1361
|
+
if payload.get("cache_creation_1h_input_cost_per_token") is not None
|
|
1362
|
+
else None
|
|
1363
|
+
),
|
|
1364
|
+
reasoning_output_cost_per_token=(
|
|
1365
|
+
_safe_float(payload.get("reasoning_output_cost_per_token"))
|
|
1366
|
+
if payload.get("reasoning_output_cost_per_token") is not None
|
|
1367
|
+
else None
|
|
1368
|
+
),
|
|
1369
|
+
cost_usd=(
|
|
1370
|
+
_safe_float(payload.get("cost_usd"))
|
|
1371
|
+
if payload.get("cost_usd") is not None
|
|
1372
|
+
else None
|
|
1373
|
+
),
|
|
1374
|
+
billing_mode=str(payload.get("billing_mode") or BillingMode.METERED_API.value),
|
|
1375
|
+
cost_source=str(payload.get("cost_source") or CostSource.UNKNOWN.value),
|
|
1376
|
+
provider_cost_usd=(
|
|
1377
|
+
_safe_float(payload.get("provider_cost_usd"))
|
|
1378
|
+
if payload.get("provider_cost_usd") is not None
|
|
1379
|
+
else None
|
|
1380
|
+
),
|
|
1381
|
+
usage_source=str(payload.get("usage_source") or "estimate"),
|
|
1382
|
+
usage_source_detail=str(
|
|
1383
|
+
payload.get("usage_source_detail")
|
|
1384
|
+
or (
|
|
1385
|
+
UsageSource.PROVIDER_RESPONSE.value
|
|
1386
|
+
if str(payload.get("usage_source") or "") == "api"
|
|
1387
|
+
else UsageSource.LOCAL_ESTIMATE.value
|
|
1388
|
+
)
|
|
1389
|
+
),
|
|
1390
|
+
usage_confidence=str(
|
|
1391
|
+
payload.get("usage_confidence")
|
|
1392
|
+
or (
|
|
1393
|
+
UsageConfidence.REPORTED.value
|
|
1394
|
+
if str(payload.get("usage_source") or "") == "api"
|
|
1395
|
+
else UsageConfidence.ESTIMATED.value
|
|
1396
|
+
)
|
|
1397
|
+
),
|
|
1398
|
+
output_includes_reasoning=_safe_bool(payload.get("output_includes_reasoning", True)),
|
|
1399
|
+
provider_key=_safe_label(payload.get("provider_key")),
|
|
1400
|
+
protocol=_safe_label(payload.get("protocol")),
|
|
1401
|
+
base_url_host=_safe_label(payload.get("base_url_host")),
|
|
1402
|
+
operation=_safe_label(payload.get("operation")),
|
|
1403
|
+
request_mode=_safe_label(payload.get("request_mode")),
|
|
1404
|
+
cache_strategy=_safe_label(payload.get("cache_strategy")),
|
|
1405
|
+
request_plan=_safe_request_plan_payload(payload.get("request_plan")),
|
|
1406
|
+
request_has_media=_safe_bool(payload.get("request_has_media")),
|
|
1407
|
+
cached_prompt_tokens=_safe_int(payload.get("cached_prompt_tokens")),
|
|
1408
|
+
uncached_prompt_tokens=_safe_int(payload.get("uncached_prompt_tokens")),
|
|
1409
|
+
input_tokens_uncached=_safe_int(payload.get("input_tokens_uncached")),
|
|
1410
|
+
input_tokens_uncached_derived=bool(payload.get("input_tokens_uncached_derived")),
|
|
1411
|
+
cache_read_input_tokens=(
|
|
1412
|
+
_safe_int(payload.get("cache_read_input_tokens"))
|
|
1413
|
+
if payload.get("cache_read_input_tokens") is not None
|
|
1414
|
+
else _safe_int(payload.get("cached_prompt_tokens"))
|
|
1415
|
+
),
|
|
1416
|
+
cache_creation_input_tokens=_safe_int(payload.get("cache_creation_input_tokens")),
|
|
1417
|
+
cache_creation_5m_input_tokens=_safe_int(payload.get("cache_creation_5m_input_tokens")),
|
|
1418
|
+
cache_creation_1h_input_tokens=_safe_int(payload.get("cache_creation_1h_input_tokens")),
|
|
1419
|
+
reasoning_tokens=_safe_int(payload.get("reasoning_tokens")),
|
|
1420
|
+
raw_provider_usage=(
|
|
1421
|
+
copy.deepcopy(payload.get("raw_provider_usage"))
|
|
1422
|
+
if isinstance(payload.get("raw_provider_usage"), dict)
|
|
1423
|
+
else None
|
|
1424
|
+
),
|
|
1425
|
+
cache_cost_pricing_missing=_safe_bool(payload.get("cache_cost_pricing_missing")),
|
|
1426
|
+
request_token_estimate=RequestTokenBreakdown.from_payload(
|
|
1427
|
+
payload.get("request_token_estimate")
|
|
1428
|
+
),
|
|
1429
|
+
prompt_estimate_tokens=_safe_int(payload.get("prompt_estimate_tokens")),
|
|
1430
|
+
prompt_estimate_error_tokens=_safe_int(payload.get("prompt_estimate_error_tokens")),
|
|
1431
|
+
prompt_estimate_error_ratio=(
|
|
1432
|
+
_safe_float(payload.get("prompt_estimate_error_ratio"))
|
|
1433
|
+
if payload.get("prompt_estimate_error_ratio") is not None
|
|
1434
|
+
else None
|
|
1435
|
+
),
|
|
1436
|
+
raw_api_prompt_tokens=_safe_int(payload.get("raw_api_prompt_tokens")),
|
|
1437
|
+
raw_api_completion_tokens=_safe_int(payload.get("raw_api_completion_tokens")),
|
|
1438
|
+
raw_api_total_tokens=_safe_int(payload.get("raw_api_total_tokens")),
|
|
1439
|
+
usage_correction_reason=(
|
|
1440
|
+
str(payload.get("usage_correction_reason"))
|
|
1441
|
+
if payload.get("usage_correction_reason") is not None
|
|
1442
|
+
else None
|
|
1443
|
+
),
|
|
1444
|
+
)
|
|
1445
|
+
self.add_record(record)
|
|
1446
|
+
|
|
1447
|
+
def by_model_rows(self) -> list[dict[str, Any]]:
|
|
1448
|
+
with self._lock:
|
|
1449
|
+
rows: list[dict[str, Any]] = []
|
|
1450
|
+
for model_name in sorted(self._by_model):
|
|
1451
|
+
totals = self._by_model[model_name]
|
|
1452
|
+
rows.append(
|
|
1453
|
+
{
|
|
1454
|
+
"model": model_name,
|
|
1455
|
+
"prompt_tokens": totals.prompt_tokens,
|
|
1456
|
+
"completion_tokens": totals.completion_tokens,
|
|
1457
|
+
"total_tokens": totals.total_tokens,
|
|
1458
|
+
"cost_usd": (
|
|
1459
|
+
totals.known_cost_usd if totals.known_cost_calls > 0 else None
|
|
1460
|
+
),
|
|
1461
|
+
"known_cost_calls": totals.known_cost_calls,
|
|
1462
|
+
"unknown_cost_count": totals.unknown_cost_count,
|
|
1463
|
+
"provider_reported_cost_calls": totals.provider_reported_cost_calls,
|
|
1464
|
+
"catalog_estimated_cost_calls": totals.catalog_estimated_cost_calls,
|
|
1465
|
+
"subscription_calls": totals.subscription_calls,
|
|
1466
|
+
"included_calls": totals.included_calls,
|
|
1467
|
+
"local_calls": totals.local_calls,
|
|
1468
|
+
"api_usage_calls": totals.api_usage_calls,
|
|
1469
|
+
"estimate_usage_calls": totals.estimate_usage_calls,
|
|
1470
|
+
"authoritative_usage_calls": totals.authoritative_usage_calls,
|
|
1471
|
+
"reported_usage_calls": totals.reported_usage_calls,
|
|
1472
|
+
"corrected_usage_calls": totals.corrected_usage_calls,
|
|
1473
|
+
"prompt_estimate_calibration_calls": (
|
|
1474
|
+
totals.prompt_estimate_calibration_calls
|
|
1475
|
+
),
|
|
1476
|
+
"prompt_estimate_error_ratio_avg": (
|
|
1477
|
+
totals.prompt_estimate_error_ratio_sum
|
|
1478
|
+
/ totals.prompt_estimate_calibration_calls
|
|
1479
|
+
if totals.prompt_estimate_calibration_calls > 0
|
|
1480
|
+
else None
|
|
1481
|
+
),
|
|
1482
|
+
"prompt_estimate_error_ratio_max": (
|
|
1483
|
+
totals.prompt_estimate_error_ratio_max
|
|
1484
|
+
if totals.prompt_estimate_calibration_calls > 0
|
|
1485
|
+
else None
|
|
1486
|
+
),
|
|
1487
|
+
"prompt_estimate_underestimate_calls": (
|
|
1488
|
+
totals.prompt_estimate_underestimate_calls
|
|
1489
|
+
),
|
|
1490
|
+
"prompt_estimate_overestimate_calls": (
|
|
1491
|
+
totals.prompt_estimate_overestimate_calls
|
|
1492
|
+
),
|
|
1493
|
+
"cached_prompt_tokens": totals.cached_prompt_tokens,
|
|
1494
|
+
"uncached_prompt_tokens": totals.uncached_prompt_tokens,
|
|
1495
|
+
"input_tokens_uncached": totals.input_tokens_uncached,
|
|
1496
|
+
"cache_read_input_tokens": totals.cache_read_input_tokens,
|
|
1497
|
+
"cache_creation_input_tokens": totals.cache_creation_input_tokens,
|
|
1498
|
+
"cache_creation_5m_input_tokens": (totals.cache_creation_5m_input_tokens),
|
|
1499
|
+
"cache_creation_1h_input_tokens": (totals.cache_creation_1h_input_tokens),
|
|
1500
|
+
"input_tokens_uncached_reported_calls": (
|
|
1501
|
+
totals.input_tokens_uncached_reported_calls
|
|
1502
|
+
),
|
|
1503
|
+
"input_tokens_uncached_derived_calls": (
|
|
1504
|
+
totals.input_tokens_uncached_derived_calls
|
|
1505
|
+
),
|
|
1506
|
+
"cache_read_reported_calls": totals.cache_read_reported_calls,
|
|
1507
|
+
"cache_creation_reported_calls": totals.cache_creation_reported_calls,
|
|
1508
|
+
"reasoning_tokens": totals.reasoning_tokens,
|
|
1509
|
+
"cache_cost_pricing_missing_calls": (
|
|
1510
|
+
totals.cache_cost_pricing_missing_calls
|
|
1511
|
+
),
|
|
1512
|
+
"request_token_estimate": {
|
|
1513
|
+
"bootstrap_prompt_tokens": totals.estimated_bootstrap_prompt_tokens,
|
|
1514
|
+
"tool_schema_tokens": totals.estimated_tool_schema_tokens,
|
|
1515
|
+
"live_conversation_history_tokens": (
|
|
1516
|
+
totals.estimated_live_conversation_history_tokens
|
|
1517
|
+
),
|
|
1518
|
+
"inline_tool_transcript_tokens": (
|
|
1519
|
+
totals.estimated_inline_tool_transcript_tokens
|
|
1520
|
+
),
|
|
1521
|
+
"memory_summary_tokens": totals.estimated_memory_summary_tokens,
|
|
1522
|
+
"pins_tokens": totals.estimated_pins_tokens,
|
|
1523
|
+
"total_tokens": totals.estimated_total_request_tokens,
|
|
1524
|
+
"tool_schema_budget_reported_calls": (
|
|
1525
|
+
totals.tool_schema_budget_reported_calls
|
|
1526
|
+
),
|
|
1527
|
+
"tool_schema_budget_exceeded_calls": (
|
|
1528
|
+
totals.tool_schema_budget_exceeded_calls
|
|
1529
|
+
),
|
|
1530
|
+
"tool_schema_budget_overage_tokens": (
|
|
1531
|
+
totals.tool_schema_budget_overage_tokens
|
|
1532
|
+
),
|
|
1533
|
+
"tool_schema_largest_tool_tokens": (
|
|
1534
|
+
totals.tool_schema_largest_tool_tokens
|
|
1535
|
+
),
|
|
1536
|
+
},
|
|
1537
|
+
}
|
|
1538
|
+
)
|
|
1539
|
+
return rows
|
|
1540
|
+
|
|
1541
|
+
def totals(self) -> dict[str, Any]:
|
|
1542
|
+
with self._lock:
|
|
1543
|
+
prompt = sum(v.prompt_tokens for v in self._by_model.values())
|
|
1544
|
+
completion = sum(v.completion_tokens for v in self._by_model.values())
|
|
1545
|
+
total = sum(v.total_tokens for v in self._by_model.values())
|
|
1546
|
+
known_cost = sum(v.known_cost_usd for v in self._by_model.values())
|
|
1547
|
+
known_cost_calls = sum(v.known_cost_calls for v in self._by_model.values())
|
|
1548
|
+
unknown_cost_calls = sum(v.unknown_cost_count for v in self._by_model.values())
|
|
1549
|
+
provider_reported_cost_calls = sum(
|
|
1550
|
+
v.provider_reported_cost_calls for v in self._by_model.values()
|
|
1551
|
+
)
|
|
1552
|
+
catalog_estimated_cost_calls = sum(
|
|
1553
|
+
v.catalog_estimated_cost_calls for v in self._by_model.values()
|
|
1554
|
+
)
|
|
1555
|
+
subscription_calls = sum(v.subscription_calls for v in self._by_model.values())
|
|
1556
|
+
included_calls = sum(v.included_calls for v in self._by_model.values())
|
|
1557
|
+
local_calls = sum(v.local_calls for v in self._by_model.values())
|
|
1558
|
+
api_usage_calls = sum(v.api_usage_calls for v in self._by_model.values())
|
|
1559
|
+
estimate_usage_calls = sum(v.estimate_usage_calls for v in self._by_model.values())
|
|
1560
|
+
authoritative_usage_calls = sum(
|
|
1561
|
+
v.authoritative_usage_calls for v in self._by_model.values()
|
|
1562
|
+
)
|
|
1563
|
+
reported_usage_calls = sum(v.reported_usage_calls for v in self._by_model.values())
|
|
1564
|
+
corrected_usage_calls = sum(v.corrected_usage_calls for v in self._by_model.values())
|
|
1565
|
+
cached_prompt_tokens = sum(v.cached_prompt_tokens for v in self._by_model.values())
|
|
1566
|
+
uncached_prompt_tokens = sum(v.uncached_prompt_tokens for v in self._by_model.values())
|
|
1567
|
+
input_tokens_uncached = sum(v.input_tokens_uncached for v in self._by_model.values())
|
|
1568
|
+
cache_read_input_tokens = sum(
|
|
1569
|
+
v.cache_read_input_tokens for v in self._by_model.values()
|
|
1570
|
+
)
|
|
1571
|
+
cache_creation_input_tokens = sum(
|
|
1572
|
+
v.cache_creation_input_tokens for v in self._by_model.values()
|
|
1573
|
+
)
|
|
1574
|
+
cache_creation_5m_input_tokens = sum(
|
|
1575
|
+
v.cache_creation_5m_input_tokens for v in self._by_model.values()
|
|
1576
|
+
)
|
|
1577
|
+
cache_creation_1h_input_tokens = sum(
|
|
1578
|
+
v.cache_creation_1h_input_tokens for v in self._by_model.values()
|
|
1579
|
+
)
|
|
1580
|
+
input_tokens_uncached_reported_calls = sum(
|
|
1581
|
+
v.input_tokens_uncached_reported_calls for v in self._by_model.values()
|
|
1582
|
+
)
|
|
1583
|
+
input_tokens_uncached_derived_calls = sum(
|
|
1584
|
+
v.input_tokens_uncached_derived_calls for v in self._by_model.values()
|
|
1585
|
+
)
|
|
1586
|
+
cache_read_reported_calls = sum(
|
|
1587
|
+
v.cache_read_reported_calls for v in self._by_model.values()
|
|
1588
|
+
)
|
|
1589
|
+
cache_creation_reported_calls = sum(
|
|
1590
|
+
v.cache_creation_reported_calls for v in self._by_model.values()
|
|
1591
|
+
)
|
|
1592
|
+
reasoning_tokens = sum(v.reasoning_tokens for v in self._by_model.values())
|
|
1593
|
+
cache_cost_pricing_missing_calls = sum(
|
|
1594
|
+
v.cache_cost_pricing_missing_calls for v in self._by_model.values()
|
|
1595
|
+
)
|
|
1596
|
+
prompt_estimate_calibration_calls = sum(
|
|
1597
|
+
v.prompt_estimate_calibration_calls for v in self._by_model.values()
|
|
1598
|
+
)
|
|
1599
|
+
prompt_estimate_error_ratio_sum = sum(
|
|
1600
|
+
v.prompt_estimate_error_ratio_sum for v in self._by_model.values()
|
|
1601
|
+
)
|
|
1602
|
+
prompt_estimate_error_ratio_max = max(
|
|
1603
|
+
(v.prompt_estimate_error_ratio_max for v in self._by_model.values()),
|
|
1604
|
+
default=0.0,
|
|
1605
|
+
)
|
|
1606
|
+
prompt_estimate_underestimate_calls = sum(
|
|
1607
|
+
v.prompt_estimate_underestimate_calls for v in self._by_model.values()
|
|
1608
|
+
)
|
|
1609
|
+
prompt_estimate_overestimate_calls = sum(
|
|
1610
|
+
v.prompt_estimate_overestimate_calls for v in self._by_model.values()
|
|
1611
|
+
)
|
|
1612
|
+
estimated_bootstrap_prompt_tokens = sum(
|
|
1613
|
+
v.estimated_bootstrap_prompt_tokens for v in self._by_model.values()
|
|
1614
|
+
)
|
|
1615
|
+
estimated_tool_schema_tokens = sum(
|
|
1616
|
+
v.estimated_tool_schema_tokens for v in self._by_model.values()
|
|
1617
|
+
)
|
|
1618
|
+
estimated_live_conversation_history_tokens = sum(
|
|
1619
|
+
v.estimated_live_conversation_history_tokens for v in self._by_model.values()
|
|
1620
|
+
)
|
|
1621
|
+
estimated_inline_tool_transcript_tokens = sum(
|
|
1622
|
+
v.estimated_inline_tool_transcript_tokens for v in self._by_model.values()
|
|
1623
|
+
)
|
|
1624
|
+
estimated_memory_summary_tokens = sum(
|
|
1625
|
+
v.estimated_memory_summary_tokens for v in self._by_model.values()
|
|
1626
|
+
)
|
|
1627
|
+
estimated_pins_tokens = sum(v.estimated_pins_tokens for v in self._by_model.values())
|
|
1628
|
+
estimated_total_request_tokens = sum(
|
|
1629
|
+
v.estimated_total_request_tokens for v in self._by_model.values()
|
|
1630
|
+
)
|
|
1631
|
+
tool_schema_budget_reported_calls = sum(
|
|
1632
|
+
v.tool_schema_budget_reported_calls for v in self._by_model.values()
|
|
1633
|
+
)
|
|
1634
|
+
tool_schema_budget_exceeded_calls = sum(
|
|
1635
|
+
v.tool_schema_budget_exceeded_calls for v in self._by_model.values()
|
|
1636
|
+
)
|
|
1637
|
+
tool_schema_budget_overage_tokens = sum(
|
|
1638
|
+
v.tool_schema_budget_overage_tokens for v in self._by_model.values()
|
|
1639
|
+
)
|
|
1640
|
+
tool_schema_largest_tool_tokens = max(
|
|
1641
|
+
(v.tool_schema_largest_tool_tokens for v in self._by_model.values()),
|
|
1642
|
+
default=0,
|
|
1643
|
+
)
|
|
1644
|
+
return {
|
|
1645
|
+
"prompt_tokens": prompt,
|
|
1646
|
+
"completion_tokens": completion,
|
|
1647
|
+
"total_tokens": total,
|
|
1648
|
+
"cost_usd": known_cost if known_cost_calls > 0 else None,
|
|
1649
|
+
"known_cost_usd": known_cost,
|
|
1650
|
+
"known_cost_calls": known_cost_calls,
|
|
1651
|
+
"unknown_cost_calls": unknown_cost_calls,
|
|
1652
|
+
"provider_reported_cost_calls": provider_reported_cost_calls,
|
|
1653
|
+
"catalog_estimated_cost_calls": catalog_estimated_cost_calls,
|
|
1654
|
+
"subscription_calls": subscription_calls,
|
|
1655
|
+
"included_calls": included_calls,
|
|
1656
|
+
"local_calls": local_calls,
|
|
1657
|
+
"api_usage_calls": api_usage_calls,
|
|
1658
|
+
"estimate_usage_calls": estimate_usage_calls,
|
|
1659
|
+
"authoritative_usage_calls": authoritative_usage_calls,
|
|
1660
|
+
"reported_usage_calls": reported_usage_calls,
|
|
1661
|
+
"corrected_usage_calls": corrected_usage_calls,
|
|
1662
|
+
"prompt_estimate_calibration_calls": prompt_estimate_calibration_calls,
|
|
1663
|
+
"prompt_estimate_error_ratio_avg": (
|
|
1664
|
+
prompt_estimate_error_ratio_sum / prompt_estimate_calibration_calls
|
|
1665
|
+
if prompt_estimate_calibration_calls > 0
|
|
1666
|
+
else None
|
|
1667
|
+
),
|
|
1668
|
+
"prompt_estimate_error_ratio_max": (
|
|
1669
|
+
prompt_estimate_error_ratio_max
|
|
1670
|
+
if prompt_estimate_calibration_calls > 0
|
|
1671
|
+
else None
|
|
1672
|
+
),
|
|
1673
|
+
"prompt_estimate_underestimate_calls": prompt_estimate_underestimate_calls,
|
|
1674
|
+
"prompt_estimate_overestimate_calls": prompt_estimate_overestimate_calls,
|
|
1675
|
+
"cached_prompt_tokens": cached_prompt_tokens,
|
|
1676
|
+
"uncached_prompt_tokens": uncached_prompt_tokens,
|
|
1677
|
+
"input_tokens_uncached": input_tokens_uncached,
|
|
1678
|
+
"cache_read_input_tokens": cache_read_input_tokens,
|
|
1679
|
+
"cache_creation_input_tokens": cache_creation_input_tokens,
|
|
1680
|
+
"cache_creation_5m_input_tokens": cache_creation_5m_input_tokens,
|
|
1681
|
+
"cache_creation_1h_input_tokens": cache_creation_1h_input_tokens,
|
|
1682
|
+
"input_tokens_uncached_reported_calls": input_tokens_uncached_reported_calls,
|
|
1683
|
+
"input_tokens_uncached_derived_calls": input_tokens_uncached_derived_calls,
|
|
1684
|
+
"cache_read_reported_calls": cache_read_reported_calls,
|
|
1685
|
+
"cache_creation_reported_calls": cache_creation_reported_calls,
|
|
1686
|
+
"reasoning_tokens": reasoning_tokens,
|
|
1687
|
+
"cache_cost_pricing_missing_calls": cache_cost_pricing_missing_calls,
|
|
1688
|
+
"cache_efficiency": self.cache_efficiency_summary(),
|
|
1689
|
+
"request_token_estimate": {
|
|
1690
|
+
"bootstrap_prompt_tokens": estimated_bootstrap_prompt_tokens,
|
|
1691
|
+
"tool_schema_tokens": estimated_tool_schema_tokens,
|
|
1692
|
+
"live_conversation_history_tokens": (
|
|
1693
|
+
estimated_live_conversation_history_tokens
|
|
1694
|
+
),
|
|
1695
|
+
"inline_tool_transcript_tokens": estimated_inline_tool_transcript_tokens,
|
|
1696
|
+
"memory_summary_tokens": estimated_memory_summary_tokens,
|
|
1697
|
+
"pins_tokens": estimated_pins_tokens,
|
|
1698
|
+
"total_tokens": estimated_total_request_tokens,
|
|
1699
|
+
"tool_schema_budget_reported_calls": tool_schema_budget_reported_calls,
|
|
1700
|
+
"tool_schema_budget_exceeded_calls": tool_schema_budget_exceeded_calls,
|
|
1701
|
+
"tool_schema_budget_overage_tokens": tool_schema_budget_overage_tokens,
|
|
1702
|
+
"tool_schema_largest_tool_tokens": tool_schema_largest_tool_tokens,
|
|
1703
|
+
},
|
|
1704
|
+
"calls": self.calls,
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
def recent_calibration_snapshot(
|
|
1708
|
+
self,
|
|
1709
|
+
*,
|
|
1710
|
+
requested_model: str | None = None,
|
|
1711
|
+
provider_key: str | None = None,
|
|
1712
|
+
protocol: str | None = None,
|
|
1713
|
+
base_url_host: str | None = None,
|
|
1714
|
+
operation: str | None = None,
|
|
1715
|
+
request_mode: str | None = None,
|
|
1716
|
+
cache_strategy: str | None = None,
|
|
1717
|
+
limit: int = 20,
|
|
1718
|
+
) -> dict[str, Any]:
|
|
1719
|
+
model_key = str(requested_model or "").strip()
|
|
1720
|
+
provider_key_filter = str(provider_key or "").strip()
|
|
1721
|
+
protocol_filter = str(protocol or "").strip()
|
|
1722
|
+
base_url_host_filter = str(base_url_host or "").strip()
|
|
1723
|
+
operation_filter = str(operation or "").strip()
|
|
1724
|
+
request_mode_filter = str(request_mode or "").strip()
|
|
1725
|
+
cache_strategy_filter = str(cache_strategy or "").strip()
|
|
1726
|
+
max_items = max(1, int(limit or 20))
|
|
1727
|
+
with self._lock:
|
|
1728
|
+
records = [
|
|
1729
|
+
record
|
|
1730
|
+
for record in self._records
|
|
1731
|
+
if _record_matches_calibration_filters(
|
|
1732
|
+
record,
|
|
1733
|
+
requested_model=model_key,
|
|
1734
|
+
provider_key=provider_key_filter,
|
|
1735
|
+
protocol=protocol_filter,
|
|
1736
|
+
base_url_host_filter=base_url_host_filter,
|
|
1737
|
+
operation=operation_filter,
|
|
1738
|
+
request_mode=request_mode_filter,
|
|
1739
|
+
cache_strategy=cache_strategy_filter,
|
|
1740
|
+
)
|
|
1741
|
+
][-max_items:]
|
|
1742
|
+
snapshot = self._calibration_snapshot_from_records(records)
|
|
1743
|
+
snapshot["group_filter"] = {
|
|
1744
|
+
"requested_model": model_key,
|
|
1745
|
+
"provider_key": provider_key_filter,
|
|
1746
|
+
"protocol": protocol_filter,
|
|
1747
|
+
"base_url_host": base_url_host_filter,
|
|
1748
|
+
"operation": operation_filter,
|
|
1749
|
+
"request_mode": request_mode_filter,
|
|
1750
|
+
"cache_strategy": cache_strategy_filter,
|
|
1751
|
+
}
|
|
1752
|
+
return snapshot
|
|
1753
|
+
|
|
1754
|
+
def calibration_group_rows(self, *, limit: int = 200) -> list[dict[str, Any]]:
|
|
1755
|
+
max_items = max(1, int(limit or 200))
|
|
1756
|
+
with self._lock:
|
|
1757
|
+
records = self._records[-max_items:]
|
|
1758
|
+
groups: dict[tuple[str, str, str, str, str, str, str], list[UsageRecord]] = {}
|
|
1759
|
+
for record in records:
|
|
1760
|
+
groups.setdefault(_calibration_group_key(record), []).append(record)
|
|
1761
|
+
rows: list[dict[str, Any]] = []
|
|
1762
|
+
for key, group_records in sorted(groups.items()):
|
|
1763
|
+
snapshot = self._calibration_snapshot_from_records(group_records)
|
|
1764
|
+
rows.append(
|
|
1765
|
+
{
|
|
1766
|
+
"requested_model": key[0],
|
|
1767
|
+
"provider_key": key[1],
|
|
1768
|
+
"protocol": key[2],
|
|
1769
|
+
"base_url_host": key[3],
|
|
1770
|
+
"operation": key[4],
|
|
1771
|
+
"request_mode": key[5],
|
|
1772
|
+
"cache_strategy": key[6],
|
|
1773
|
+
**snapshot,
|
|
1774
|
+
}
|
|
1775
|
+
)
|
|
1776
|
+
return rows
|
|
1777
|
+
|
|
1778
|
+
def _calibration_snapshot_from_records(
|
|
1779
|
+
self,
|
|
1780
|
+
records: list[UsageRecord],
|
|
1781
|
+
) -> dict[str, Any]:
|
|
1782
|
+
media_known_records = [record for record in records if record.usage_schema_version >= 5]
|
|
1783
|
+
calibration_records = [
|
|
1784
|
+
record for record in media_known_records if not record.request_has_media
|
|
1785
|
+
]
|
|
1786
|
+
raw_ratios = [
|
|
1787
|
+
float(record.prompt_estimate_error_ratio)
|
|
1788
|
+
for record in calibration_records
|
|
1789
|
+
if record.prompt_estimate_error_ratio is not None
|
|
1790
|
+
and math.isfinite(float(record.prompt_estimate_error_ratio))
|
|
1791
|
+
and float(record.prompt_estimate_error_ratio) > 0
|
|
1792
|
+
]
|
|
1793
|
+
ratios = _robust_calibration_ratios(raw_ratios)
|
|
1794
|
+
api_prompt_tokens = sum(max(0, record.prompt_tokens) for record in records)
|
|
1795
|
+
cache_read_tokens = sum(max(0, record.cache_read_input_tokens or 0) for record in records)
|
|
1796
|
+
cache_creation_tokens = sum(
|
|
1797
|
+
max(0, record.cache_creation_input_tokens or 0) for record in records
|
|
1798
|
+
)
|
|
1799
|
+
underestimate_calls = sum(
|
|
1800
|
+
1 for record in records if (record.prompt_estimate_error_tokens or 0) > 0
|
|
1801
|
+
)
|
|
1802
|
+
overestimate_calls = sum(
|
|
1803
|
+
1 for record in records if (record.prompt_estimate_error_tokens or 0) < 0
|
|
1804
|
+
)
|
|
1805
|
+
ratios_sorted = sorted(ratios)
|
|
1806
|
+
p90 = None
|
|
1807
|
+
if ratios_sorted:
|
|
1808
|
+
p90_index = min(
|
|
1809
|
+
len(ratios_sorted) - 1,
|
|
1810
|
+
max(0, math.ceil(len(ratios_sorted) * 0.9) - 1),
|
|
1811
|
+
)
|
|
1812
|
+
p90 = ratios_sorted[p90_index]
|
|
1813
|
+
return {
|
|
1814
|
+
"records": len(records),
|
|
1815
|
+
"calibration_calls": len(raw_ratios),
|
|
1816
|
+
"calibration_usable_calls": len(ratios),
|
|
1817
|
+
"calibration_outlier_calls": max(0, len(raw_ratios) - len(ratios)),
|
|
1818
|
+
"calibration_excluded_unknown_media_calls": (len(records) - len(media_known_records)),
|
|
1819
|
+
"calibration_excluded_media_calls": (
|
|
1820
|
+
len(media_known_records) - len(calibration_records)
|
|
1821
|
+
),
|
|
1822
|
+
"calibration_stable": len(ratios) >= 3,
|
|
1823
|
+
"prompt_estimate_error_ratio_avg": (sum(ratios) / len(ratios) if ratios else None),
|
|
1824
|
+
"prompt_estimate_error_ratio_p90": p90,
|
|
1825
|
+
"prompt_estimate_error_ratio_max": max(ratios) if ratios else None,
|
|
1826
|
+
"prompt_estimate_error_ratio_raw_max": (max(raw_ratios) if raw_ratios else None),
|
|
1827
|
+
"prompt_estimate_underestimate_calls": underestimate_calls,
|
|
1828
|
+
"prompt_estimate_overestimate_calls": overestimate_calls,
|
|
1829
|
+
"cache_read_input_tokens": cache_read_tokens,
|
|
1830
|
+
"cache_creation_input_tokens": cache_creation_tokens,
|
|
1831
|
+
"cache_hit_ratio": (
|
|
1832
|
+
cache_read_tokens / api_prompt_tokens if api_prompt_tokens > 0 else None
|
|
1833
|
+
),
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
|
|
1837
|
+
def aggregate_usage_from_session_logs(paths: list[Path]) -> UsageSummary:
|
|
1838
|
+
summary = UsageSummary()
|
|
1839
|
+
for path in paths:
|
|
1840
|
+
if not path.exists() or not path.is_file():
|
|
1841
|
+
continue
|
|
1842
|
+
for event in read_session_events(path):
|
|
1843
|
+
if str(event.get("type") or "") != "llm_usage":
|
|
1844
|
+
continue
|
|
1845
|
+
payload = event.get("payload")
|
|
1846
|
+
if isinstance(payload, dict):
|
|
1847
|
+
summary.add_event_payload(payload)
|
|
1848
|
+
return summary
|
|
1849
|
+
|
|
1850
|
+
|
|
1851
|
+
def compute_context_left(
|
|
1852
|
+
*,
|
|
1853
|
+
messages: list[dict[str, Any]],
|
|
1854
|
+
model_name: str,
|
|
1855
|
+
registry: ModelRegistry,
|
|
1856
|
+
tool_list: list[dict[str, Any]] | None = None,
|
|
1857
|
+
pinned_prefix_len: int = 0,
|
|
1858
|
+
safety_margin_tokens: int = 512,
|
|
1859
|
+
startup_baseline_tokens: int = 0,
|
|
1860
|
+
prompt_estimate_multiplier: float | None = None,
|
|
1861
|
+
request_measurement: RequestContextMeasurement | None = None,
|
|
1862
|
+
) -> ContextLeft:
|
|
1863
|
+
meta = registry.get(model_name)
|
|
1864
|
+
context_window = meta.context_window_tokens
|
|
1865
|
+
effective_input_budget = compute_input_budget(meta, safety_margin=safety_margin_tokens)
|
|
1866
|
+
baseline = max(0, int(startup_baseline_tokens or 0))
|
|
1867
|
+
request_estimate = estimate_request_token_breakdown(
|
|
1868
|
+
messages=messages,
|
|
1869
|
+
tool_list=tool_list,
|
|
1870
|
+
pinned_prefix_len=pinned_prefix_len,
|
|
1871
|
+
)
|
|
1872
|
+
local_used = request_estimate.total_tokens
|
|
1873
|
+
used = local_used
|
|
1874
|
+
token_count_source = UsageSource.LOCAL_ESTIMATE.value
|
|
1875
|
+
token_count_confidence = UsageConfidence.ESTIMATED.value
|
|
1876
|
+
multiplier = 1.0
|
|
1877
|
+
projection_kind = (
|
|
1878
|
+
request_measurement.projection_kind(messages=messages, tool_list=tool_list)
|
|
1879
|
+
if request_measurement is not None
|
|
1880
|
+
else None
|
|
1881
|
+
)
|
|
1882
|
+
anchor_token_count_source: str | None = None
|
|
1883
|
+
anchor_token_count_confidence: str | None = None
|
|
1884
|
+
provider_projection_applied = False
|
|
1885
|
+
if request_measurement is not None and projection_kind is not None:
|
|
1886
|
+
anchor_token_count_source = request_measurement.source
|
|
1887
|
+
anchor_token_count_confidence = request_measurement.confidence
|
|
1888
|
+
if projection_kind == "exact_request":
|
|
1889
|
+
used = request_measurement.input_tokens
|
|
1890
|
+
token_count_source = request_measurement.source
|
|
1891
|
+
token_count_confidence = request_measurement.confidence
|
|
1892
|
+
else:
|
|
1893
|
+
scale = request_measurement.input_tokens / max(
|
|
1894
|
+
1,
|
|
1895
|
+
request_measurement.anchor_estimate_tokens,
|
|
1896
|
+
)
|
|
1897
|
+
persistent_delta = max(
|
|
1898
|
+
0,
|
|
1899
|
+
local_used - request_measurement.persistent_anchor_estimate_tokens,
|
|
1900
|
+
)
|
|
1901
|
+
used = max(
|
|
1902
|
+
0,
|
|
1903
|
+
request_measurement.input_tokens + math.ceil(persistent_delta * scale),
|
|
1904
|
+
)
|
|
1905
|
+
if request_measurement.source == UsageSource.LOCAL_ESTIMATE.value:
|
|
1906
|
+
token_count_source = UsageSource.LOCAL_ESTIMATE.value
|
|
1907
|
+
token_count_confidence = UsageConfidence.ESTIMATED.value
|
|
1908
|
+
else:
|
|
1909
|
+
token_count_source = UsageSource.MIXED.value
|
|
1910
|
+
token_count_confidence = UsageConfidence.ESTIMATED.value
|
|
1911
|
+
provider_projection_applied = True
|
|
1912
|
+
elif isinstance(prompt_estimate_multiplier, int | float):
|
|
1913
|
+
# Calibration is allowed to make the universal fallback more
|
|
1914
|
+
# conservative, never more optimistic. The caller supplies a
|
|
1915
|
+
# provider/protocol/operation-scoped robust percentile, so do not cap a
|
|
1916
|
+
# measured tokenizer gap at an arbitrary universal ratio.
|
|
1917
|
+
multiplier = max(1.0, float(prompt_estimate_multiplier))
|
|
1918
|
+
used = math.ceil(used * multiplier)
|
|
1919
|
+
baseline = math.ceil(baseline * multiplier)
|
|
1920
|
+
context_remaining = max(0, context_window - used) if context_window > 0 else None
|
|
1921
|
+
context_percent = (
|
|
1922
|
+
(context_remaining / context_window) * 100.0
|
|
1923
|
+
if (context_remaining is not None and context_window > 0)
|
|
1924
|
+
else None
|
|
1925
|
+
)
|
|
1926
|
+
effective_remaining = (
|
|
1927
|
+
max(0, effective_input_budget - used) if effective_input_budget > 0 else None
|
|
1928
|
+
)
|
|
1929
|
+
effective_percent = (
|
|
1930
|
+
(effective_remaining / effective_input_budget) * 100.0
|
|
1931
|
+
if (effective_remaining is not None and effective_input_budget > 0)
|
|
1932
|
+
else None
|
|
1933
|
+
)
|
|
1934
|
+
# Conversation-growth metrics intentionally remain on the local estimator.
|
|
1935
|
+
# Provider counts include tokenizer framing and ephemeral request wrappers;
|
|
1936
|
+
# attributing that overhead to conversation growth makes the dynamic gauge
|
|
1937
|
+
# jump even when no persistent context changed.
|
|
1938
|
+
dynamic_metric_used = local_used if projection_kind is not None else used
|
|
1939
|
+
dynamic_budget = max(0, effective_input_budget - baseline)
|
|
1940
|
+
dynamic_used = max(0, dynamic_metric_used - baseline)
|
|
1941
|
+
dynamic_remaining = max(0, dynamic_budget - dynamic_used)
|
|
1942
|
+
dynamic_percent = (dynamic_remaining / dynamic_budget) * 100.0 if dynamic_budget > 0 else 0.0
|
|
1943
|
+
return ContextLeft(
|
|
1944
|
+
model_name=model_name,
|
|
1945
|
+
max_input_tokens=context_window,
|
|
1946
|
+
used_input_tokens=used,
|
|
1947
|
+
remaining_tokens=context_remaining,
|
|
1948
|
+
percent_left=context_percent,
|
|
1949
|
+
source=meta.source,
|
|
1950
|
+
context_window_tokens=context_window,
|
|
1951
|
+
context_window_remaining_tokens=context_remaining,
|
|
1952
|
+
context_window_percent_left=context_percent,
|
|
1953
|
+
effective_input_budget=effective_input_budget,
|
|
1954
|
+
effective_remaining_tokens=effective_remaining,
|
|
1955
|
+
effective_percent_left=effective_percent,
|
|
1956
|
+
startup_baseline_tokens=baseline,
|
|
1957
|
+
dynamic_context_budget_tokens=dynamic_budget,
|
|
1958
|
+
dynamic_context_used_tokens=dynamic_used,
|
|
1959
|
+
dynamic_context_remaining_tokens=dynamic_remaining,
|
|
1960
|
+
dynamic_context_percent_left=dynamic_percent,
|
|
1961
|
+
token_count_source=token_count_source,
|
|
1962
|
+
token_count_confidence=token_count_confidence,
|
|
1963
|
+
local_request_estimate_tokens=local_used,
|
|
1964
|
+
anchor_token_count_source=anchor_token_count_source,
|
|
1965
|
+
anchor_token_count_confidence=anchor_token_count_confidence,
|
|
1966
|
+
provider_projection_applied=provider_projection_applied,
|
|
1967
|
+
capacity_provider_key=meta.provider_key,
|
|
1968
|
+
context_window_source=meta.field_sources.get("context_window_tokens", meta.source),
|
|
1969
|
+
max_output_tokens=meta.max_output_tokens,
|
|
1970
|
+
max_output_source=meta.field_sources.get("max_output_tokens", meta.source),
|
|
1971
|
+
safety_margin_tokens=max(0, int(safety_margin_tokens)),
|
|
1972
|
+
)
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
def format_usd(cost: float | None, style: str = "table") -> str:
|
|
1976
|
+
if cost is None:
|
|
1977
|
+
return "n/a"
|
|
1978
|
+
decimals = 3 if style == "hud" else 4
|
|
1979
|
+
return f"${cost:.{decimals}f}"
|
|
1980
|
+
|
|
1981
|
+
|
|
1982
|
+
def format_cost(cost: float | None) -> str:
|
|
1983
|
+
# Backward-compatible alias for existing call sites.
|
|
1984
|
+
return format_usd(cost, style="table")
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
def format_context_percent(ctx: ContextLeft) -> str:
|
|
1988
|
+
if ctx.percent_left is None:
|
|
1989
|
+
return "n/a"
|
|
1990
|
+
return f"{ctx.percent_left:.1f}%"
|