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,985 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
|
|
9
|
+
from .protocols import (
|
|
10
|
+
ANTHROPIC_MESSAGES_PROTOCOL,
|
|
11
|
+
GEMINI_GENERATE_CONTENT_PROTOCOL,
|
|
12
|
+
GEMINI_INTERACTIONS_PROTOCOL,
|
|
13
|
+
OPENAI_COMPAT_PROTOCOL,
|
|
14
|
+
OPENAI_RESPONSES_PROTOCOL,
|
|
15
|
+
ProviderProtocolCapabilities,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..profile_presets import ProfilePreset
|
|
20
|
+
from ..profiles import ProfileSpec
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
CACHE_STRATEGY_INHERIT = "inherit"
|
|
24
|
+
CACHE_STRATEGY_NONE = "none"
|
|
25
|
+
CACHE_STRATEGY_IMPLICIT_PROVIDER = "implicit_provider"
|
|
26
|
+
CACHE_STRATEGY_OPENAI_PROMPT_CACHE = "openai_prompt_cache"
|
|
27
|
+
CACHE_STRATEGY_MISTRAL_PROMPT_CACHE_KEY = "mistral_prompt_cache_key"
|
|
28
|
+
CACHE_STRATEGY_ANTHROPIC_CACHE_CONTROL = "anthropic_cache_control"
|
|
29
|
+
CACHE_STRATEGY_GEMINI_EXPLICIT_CACHED_CONTENT = "gemini_explicit_cached_content"
|
|
30
|
+
CACHE_STRATEGY_GEMINI_IMPLICIT = "gemini_implicit"
|
|
31
|
+
CACHE_STRATEGY_OPENROUTER_STICKY_SESSION = "openrouter_sticky_session"
|
|
32
|
+
CACHE_STRATEGY_XAI_CONVERSATION_HEADER = "xai_conversation_header"
|
|
33
|
+
CACHE_STRATEGY_QWEN_CACHE_CONTROL_BLOCKS = "qwen_cache_control_blocks"
|
|
34
|
+
|
|
35
|
+
KNOWN_CACHE_STRATEGIES: frozenset[str] = frozenset(
|
|
36
|
+
{
|
|
37
|
+
CACHE_STRATEGY_NONE,
|
|
38
|
+
CACHE_STRATEGY_IMPLICIT_PROVIDER,
|
|
39
|
+
CACHE_STRATEGY_OPENAI_PROMPT_CACHE,
|
|
40
|
+
CACHE_STRATEGY_MISTRAL_PROMPT_CACHE_KEY,
|
|
41
|
+
CACHE_STRATEGY_ANTHROPIC_CACHE_CONTROL,
|
|
42
|
+
CACHE_STRATEGY_GEMINI_EXPLICIT_CACHED_CONTENT,
|
|
43
|
+
CACHE_STRATEGY_GEMINI_IMPLICIT,
|
|
44
|
+
CACHE_STRATEGY_OPENROUTER_STICKY_SESSION,
|
|
45
|
+
CACHE_STRATEGY_XAI_CONVERSATION_HEADER,
|
|
46
|
+
CACHE_STRATEGY_QWEN_CACHE_CONTROL_BLOCKS,
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
PROFILE_CACHE_STRATEGIES: frozenset[str] = frozenset(
|
|
51
|
+
{CACHE_STRATEGY_INHERIT, *KNOWN_CACHE_STRATEGIES}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
CACHE_USAGE_SCHEMA_NONE = "none"
|
|
55
|
+
CACHE_USAGE_SCHEMA_OPENAI = "openai"
|
|
56
|
+
CACHE_USAGE_SCHEMA_ANTHROPIC = "anthropic"
|
|
57
|
+
CACHE_USAGE_SCHEMA_GEMINI = "gemini"
|
|
58
|
+
CACHE_USAGE_SCHEMA_PROVIDER = "provider"
|
|
59
|
+
|
|
60
|
+
KNOWN_CACHE_USAGE_SCHEMAS: frozenset[str] = frozenset(
|
|
61
|
+
{
|
|
62
|
+
CACHE_USAGE_SCHEMA_NONE,
|
|
63
|
+
CACHE_USAGE_SCHEMA_OPENAI,
|
|
64
|
+
CACHE_USAGE_SCHEMA_ANTHROPIC,
|
|
65
|
+
CACHE_USAGE_SCHEMA_GEMINI,
|
|
66
|
+
CACHE_USAGE_SCHEMA_PROVIDER,
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
PROMPT_CACHE_KEY_FIELD = "prompt_cache_key"
|
|
71
|
+
PROMPT_CACHE_RETENTION_FIELD = "prompt_cache_retention"
|
|
72
|
+
CACHE_CONTROL_FIELD = "cache_control"
|
|
73
|
+
CACHED_CONTENT_FIELD = "cached_content"
|
|
74
|
+
OPENROUTER_SESSION_ID_FIELD = "session_id"
|
|
75
|
+
OPENROUTER_SESSION_ID_HEADER_FIELD = "x-session-id"
|
|
76
|
+
XAI_CONVERSATION_ID_HEADER_FIELD = "x-grok-conv-id"
|
|
77
|
+
|
|
78
|
+
KNOWN_CACHE_REQUEST_FIELDS: frozenset[str] = frozenset(
|
|
79
|
+
{
|
|
80
|
+
PROMPT_CACHE_KEY_FIELD,
|
|
81
|
+
PROMPT_CACHE_RETENTION_FIELD,
|
|
82
|
+
CACHE_CONTROL_FIELD,
|
|
83
|
+
CACHED_CONTENT_FIELD,
|
|
84
|
+
OPENROUTER_SESSION_ID_FIELD,
|
|
85
|
+
OPENROUTER_SESSION_ID_HEADER_FIELD,
|
|
86
|
+
XAI_CONVERSATION_ID_HEADER_FIELD,
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class CacheCapabilitySpec:
|
|
93
|
+
"""Declarative cache capability metadata for profiles and presets.
|
|
94
|
+
|
|
95
|
+
``None`` fields mean "inherit from the lower-precedence capability source".
|
|
96
|
+
Persisted user profiles should store this object under ``cache_capability``.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
strategy: str = CACHE_STRATEGY_INHERIT
|
|
100
|
+
enabled: bool | None = None
|
|
101
|
+
supports_prompt_cache_key: bool | None = None
|
|
102
|
+
supports_prompt_cache_retention: bool | None = None
|
|
103
|
+
supports_cache_control: bool | None = None
|
|
104
|
+
supports_explicit_cached_content: bool | None = None
|
|
105
|
+
reports_cache_read_tokens: bool | None = None
|
|
106
|
+
reports_cache_write_tokens: bool | None = None
|
|
107
|
+
emits_request_fields: bool | None = None
|
|
108
|
+
request_fields: tuple[str, ...] = field(default_factory=tuple)
|
|
109
|
+
usage_schema: str | None = None
|
|
110
|
+
min_cacheable_tokens: int | None = None
|
|
111
|
+
notes: tuple[str, ...] = field(default_factory=tuple)
|
|
112
|
+
source: str = "profile"
|
|
113
|
+
model_overrides: tuple[tuple[str, CacheCapabilitySpec], ...] = ()
|
|
114
|
+
model_family_overrides: tuple[tuple[str, CacheCapabilitySpec], ...] = ()
|
|
115
|
+
endpoint_overrides: tuple[tuple[str, CacheCapabilitySpec], ...] = ()
|
|
116
|
+
|
|
117
|
+
def __post_init__(self) -> None:
|
|
118
|
+
strategy = normalize_cache_strategy(
|
|
119
|
+
self.strategy,
|
|
120
|
+
allow_inherit=True,
|
|
121
|
+
field_name=f"{self.source} cache strategy",
|
|
122
|
+
)
|
|
123
|
+
usage_schema = (
|
|
124
|
+
normalize_cache_usage_schema(
|
|
125
|
+
self.usage_schema,
|
|
126
|
+
field_name=f"{self.source} cache usage_schema",
|
|
127
|
+
)
|
|
128
|
+
if self.usage_schema is not None
|
|
129
|
+
else None
|
|
130
|
+
)
|
|
131
|
+
min_tokens = self.min_cacheable_tokens
|
|
132
|
+
if min_tokens is not None:
|
|
133
|
+
try:
|
|
134
|
+
min_tokens = int(min_tokens)
|
|
135
|
+
except (TypeError, ValueError):
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f"{self.source} cache min_cacheable_tokens must be an integer."
|
|
138
|
+
) from None
|
|
139
|
+
if min_tokens < 0:
|
|
140
|
+
raise ValueError(f"{self.source} cache min_cacheable_tokens must be non-negative.")
|
|
141
|
+
object.__setattr__(self, "strategy", strategy)
|
|
142
|
+
object.__setattr__(self, "usage_schema", usage_schema)
|
|
143
|
+
object.__setattr__(self, "min_cacheable_tokens", min_tokens)
|
|
144
|
+
object.__setattr__(
|
|
145
|
+
self,
|
|
146
|
+
"request_fields",
|
|
147
|
+
_request_field_tuple(
|
|
148
|
+
self.request_fields,
|
|
149
|
+
field_name=f"{self.source} cache request_fields",
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
object.__setattr__(
|
|
153
|
+
self,
|
|
154
|
+
"notes",
|
|
155
|
+
tuple(note for note in (str(item).strip() for item in self.notes) if note),
|
|
156
|
+
)
|
|
157
|
+
object.__setattr__(self, "source", str(self.source or "profile").strip() or "profile")
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def from_mapping(
|
|
161
|
+
cls,
|
|
162
|
+
value: Mapping[str, Any] | None,
|
|
163
|
+
*,
|
|
164
|
+
source: str = "profile",
|
|
165
|
+
) -> CacheCapabilitySpec | None:
|
|
166
|
+
if value is None:
|
|
167
|
+
return None
|
|
168
|
+
if not isinstance(value, Mapping):
|
|
169
|
+
raise ValueError(f"{source} cache_capability must be an object.")
|
|
170
|
+
strategy = value.get("strategy", value.get("cache_strategy", CACHE_STRATEGY_INHERIT))
|
|
171
|
+
return cls(
|
|
172
|
+
strategy=str(strategy or CACHE_STRATEGY_INHERIT).strip(),
|
|
173
|
+
enabled=_optional_bool(value.get("enabled"), field_name=f"{source} cache enabled"),
|
|
174
|
+
supports_prompt_cache_key=_optional_bool(
|
|
175
|
+
value.get("supports_prompt_cache_key"),
|
|
176
|
+
field_name=f"{source} cache supports_prompt_cache_key",
|
|
177
|
+
),
|
|
178
|
+
supports_prompt_cache_retention=_optional_bool(
|
|
179
|
+
value.get("supports_prompt_cache_retention"),
|
|
180
|
+
field_name=f"{source} cache supports_prompt_cache_retention",
|
|
181
|
+
),
|
|
182
|
+
supports_cache_control=_optional_bool(
|
|
183
|
+
value.get("supports_cache_control"),
|
|
184
|
+
field_name=f"{source} cache supports_cache_control",
|
|
185
|
+
),
|
|
186
|
+
supports_explicit_cached_content=_optional_bool(
|
|
187
|
+
value.get("supports_explicit_cached_content"),
|
|
188
|
+
field_name=f"{source} cache supports_explicit_cached_content",
|
|
189
|
+
),
|
|
190
|
+
reports_cache_read_tokens=_optional_bool(
|
|
191
|
+
value.get("reports_cache_read_tokens"),
|
|
192
|
+
field_name=f"{source} cache reports_cache_read_tokens",
|
|
193
|
+
),
|
|
194
|
+
reports_cache_write_tokens=_optional_bool(
|
|
195
|
+
value.get("reports_cache_write_tokens"),
|
|
196
|
+
field_name=f"{source} cache reports_cache_write_tokens",
|
|
197
|
+
),
|
|
198
|
+
emits_request_fields=_optional_bool(
|
|
199
|
+
value.get(
|
|
200
|
+
"emits_request_fields",
|
|
201
|
+
value.get("emit_request_fields", value.get("request_fields_enabled")),
|
|
202
|
+
),
|
|
203
|
+
field_name=f"{source} cache emits_request_fields",
|
|
204
|
+
),
|
|
205
|
+
request_fields=_request_field_tuple(
|
|
206
|
+
value.get("request_fields"),
|
|
207
|
+
field_name=f"{source} cache request_fields",
|
|
208
|
+
),
|
|
209
|
+
usage_schema=(
|
|
210
|
+
str(value.get("usage_schema")).strip()
|
|
211
|
+
if value.get("usage_schema") is not None
|
|
212
|
+
else None
|
|
213
|
+
),
|
|
214
|
+
min_cacheable_tokens=_optional_int(
|
|
215
|
+
value.get("min_cacheable_tokens"),
|
|
216
|
+
field_name=f"{source} cache min_cacheable_tokens",
|
|
217
|
+
),
|
|
218
|
+
notes=_string_tuple(value.get("notes"), field_name=f"{source} cache notes"),
|
|
219
|
+
source=source,
|
|
220
|
+
model_overrides=_scoped_specs(
|
|
221
|
+
value.get("models"),
|
|
222
|
+
source=f"{source} cache models",
|
|
223
|
+
),
|
|
224
|
+
model_family_overrides=_scoped_specs(
|
|
225
|
+
value.get("model_families"),
|
|
226
|
+
source=f"{source} cache model_families",
|
|
227
|
+
),
|
|
228
|
+
endpoint_overrides=_scoped_specs(
|
|
229
|
+
value.get("endpoints"),
|
|
230
|
+
source=f"{source} cache endpoints",
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def to_dict(self) -> dict[str, Any]:
|
|
235
|
+
data: dict[str, Any] = {}
|
|
236
|
+
if self.strategy != CACHE_STRATEGY_INHERIT:
|
|
237
|
+
data["strategy"] = self.strategy
|
|
238
|
+
for key in (
|
|
239
|
+
"enabled",
|
|
240
|
+
"supports_prompt_cache_key",
|
|
241
|
+
"supports_prompt_cache_retention",
|
|
242
|
+
"supports_cache_control",
|
|
243
|
+
"supports_explicit_cached_content",
|
|
244
|
+
"reports_cache_read_tokens",
|
|
245
|
+
"reports_cache_write_tokens",
|
|
246
|
+
"emits_request_fields",
|
|
247
|
+
):
|
|
248
|
+
value = getattr(self, key)
|
|
249
|
+
if value is not None:
|
|
250
|
+
data[key] = bool(value)
|
|
251
|
+
if self.request_fields:
|
|
252
|
+
data["request_fields"] = list(self.request_fields)
|
|
253
|
+
if self.usage_schema is not None:
|
|
254
|
+
data["usage_schema"] = self.usage_schema
|
|
255
|
+
if self.min_cacheable_tokens is not None:
|
|
256
|
+
data["min_cacheable_tokens"] = self.min_cacheable_tokens
|
|
257
|
+
if self.notes:
|
|
258
|
+
data["notes"] = list(self.notes)
|
|
259
|
+
if self.model_overrides:
|
|
260
|
+
data["models"] = {
|
|
261
|
+
key: spec.to_dict() for key, spec in self.model_overrides if spec.has_values()
|
|
262
|
+
}
|
|
263
|
+
if self.model_family_overrides:
|
|
264
|
+
data["model_families"] = {
|
|
265
|
+
key: spec.to_dict()
|
|
266
|
+
for key, spec in self.model_family_overrides
|
|
267
|
+
if spec.has_values()
|
|
268
|
+
}
|
|
269
|
+
if self.endpoint_overrides:
|
|
270
|
+
data["endpoints"] = {
|
|
271
|
+
key: spec.to_dict() for key, spec in self.endpoint_overrides if spec.has_values()
|
|
272
|
+
}
|
|
273
|
+
return data
|
|
274
|
+
|
|
275
|
+
def has_values(self) -> bool:
|
|
276
|
+
return bool(self.to_dict())
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@dataclass(frozen=True)
|
|
280
|
+
class EffectiveCacheCapability:
|
|
281
|
+
provider_key: str
|
|
282
|
+
protocol: str
|
|
283
|
+
model: str
|
|
284
|
+
strategy: str = CACHE_STRATEGY_NONE
|
|
285
|
+
enabled: bool = False
|
|
286
|
+
source: str = "default"
|
|
287
|
+
supports_prompt_cache_key: bool = False
|
|
288
|
+
supports_prompt_cache_retention: bool = False
|
|
289
|
+
supports_cache_control: bool = False
|
|
290
|
+
supports_explicit_cached_content: bool = False
|
|
291
|
+
reports_cache_read_tokens: bool = False
|
|
292
|
+
reports_cache_write_tokens: bool = False
|
|
293
|
+
emits_request_fields: bool = False
|
|
294
|
+
usage_schema: str = CACHE_USAGE_SCHEMA_NONE
|
|
295
|
+
min_cacheable_tokens: int | None = None
|
|
296
|
+
emitted_fields: tuple[str, ...] = ()
|
|
297
|
+
trusted_usage_fields: tuple[str, ...] = ()
|
|
298
|
+
notes: tuple[str, ...] = ()
|
|
299
|
+
warnings: tuple[str, ...] = ()
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def status(self) -> str:
|
|
303
|
+
if self.enabled and self.emitted_fields:
|
|
304
|
+
return "enabled"
|
|
305
|
+
if self.enabled:
|
|
306
|
+
return "available"
|
|
307
|
+
if self.strategy == CACHE_STRATEGY_NONE:
|
|
308
|
+
return "disabled"
|
|
309
|
+
return "unsupported"
|
|
310
|
+
|
|
311
|
+
def to_dict(self) -> dict[str, Any]:
|
|
312
|
+
payload: dict[str, Any] = {
|
|
313
|
+
"status": self.status,
|
|
314
|
+
"strategy": self.strategy,
|
|
315
|
+
"source": self.source,
|
|
316
|
+
"provider_key": self.provider_key,
|
|
317
|
+
"protocol": self.protocol,
|
|
318
|
+
"model": self.model,
|
|
319
|
+
"emitted_fields": list(self.emitted_fields),
|
|
320
|
+
"trusted_usage_fields": list(self.trusted_usage_fields),
|
|
321
|
+
"usage_schema": self.usage_schema,
|
|
322
|
+
"emits_request_fields": self.emits_request_fields,
|
|
323
|
+
}
|
|
324
|
+
if self.min_cacheable_tokens is not None:
|
|
325
|
+
payload["min_cacheable_tokens"] = self.min_cacheable_tokens
|
|
326
|
+
if self.notes:
|
|
327
|
+
payload["notes"] = list(self.notes)
|
|
328
|
+
if self.warnings:
|
|
329
|
+
payload["warnings"] = list(self.warnings)
|
|
330
|
+
return payload
|
|
331
|
+
|
|
332
|
+
def summary(self) -> str:
|
|
333
|
+
fields = ", ".join(self.emitted_fields) if self.emitted_fields else "none"
|
|
334
|
+
usage = ", ".join(self.trusted_usage_fields) if self.trusted_usage_fields else "none"
|
|
335
|
+
return (
|
|
336
|
+
f"{self.status}; strategy={self.strategy}; source={self.source}; "
|
|
337
|
+
f"emits={fields}; usage={usage}"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@dataclass(frozen=True)
|
|
342
|
+
class _CacheCapabilityState:
|
|
343
|
+
strategy: str
|
|
344
|
+
enabled: bool
|
|
345
|
+
supports_prompt_cache_key: bool
|
|
346
|
+
supports_prompt_cache_retention: bool
|
|
347
|
+
supports_cache_control: bool
|
|
348
|
+
supports_explicit_cached_content: bool
|
|
349
|
+
reports_cache_read_tokens: bool
|
|
350
|
+
reports_cache_write_tokens: bool
|
|
351
|
+
emits_request_fields: bool
|
|
352
|
+
request_fields: tuple[str, ...]
|
|
353
|
+
usage_schema: str
|
|
354
|
+
min_cacheable_tokens: int | None
|
|
355
|
+
source: str
|
|
356
|
+
notes: tuple[str, ...]
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def resolve_effective_cache_capability(
|
|
360
|
+
*,
|
|
361
|
+
provider_key: str,
|
|
362
|
+
protocol: str,
|
|
363
|
+
model: str,
|
|
364
|
+
base_url: str | None = None,
|
|
365
|
+
transport_capabilities: ProviderProtocolCapabilities | None,
|
|
366
|
+
preset_cache_capability: CacheCapabilitySpec | Mapping[str, Any] | None = None,
|
|
367
|
+
profile_cache_capability: CacheCapabilitySpec | Mapping[str, Any] | None = None,
|
|
368
|
+
runtime_disabled_fields: tuple[str, ...] = (),
|
|
369
|
+
) -> EffectiveCacheCapability:
|
|
370
|
+
"""Resolve cache behavior from transport, preset, and profile metadata.
|
|
371
|
+
|
|
372
|
+
The resolver deliberately separates "declared support" from "transport projection".
|
|
373
|
+
User/profile overrides may opt into provider features, but emitted request fields are
|
|
374
|
+
still clamped to fields the active client implementation knows how to project.
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
normalized_provider = _safe_text(provider_key).lower()
|
|
378
|
+
normalized_protocol = _safe_text(protocol).lower()
|
|
379
|
+
normalized_model = _safe_text(model)
|
|
380
|
+
|
|
381
|
+
state = _state_from_transport_capabilities(transport_capabilities)
|
|
382
|
+
preset_spec = _coerce_spec(preset_cache_capability, source="preset")
|
|
383
|
+
profile_spec = _coerce_spec(profile_cache_capability, source="profile")
|
|
384
|
+
if preset_spec is not None:
|
|
385
|
+
state = _apply_spec(state, preset_spec, origin="preset")
|
|
386
|
+
state = _apply_scoped_specs(
|
|
387
|
+
state,
|
|
388
|
+
preset_spec,
|
|
389
|
+
model=normalized_model,
|
|
390
|
+
base_url=base_url,
|
|
391
|
+
origin="preset",
|
|
392
|
+
)
|
|
393
|
+
if profile_spec is not None:
|
|
394
|
+
state = _apply_spec(state, profile_spec, origin="profile")
|
|
395
|
+
state = _apply_scoped_specs(
|
|
396
|
+
state,
|
|
397
|
+
profile_spec,
|
|
398
|
+
model=normalized_model,
|
|
399
|
+
base_url=base_url,
|
|
400
|
+
origin="profile",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
warnings: list[str] = []
|
|
404
|
+
allowed_fields, projection_warnings = _projected_fields(
|
|
405
|
+
state=state,
|
|
406
|
+
protocol=normalized_protocol,
|
|
407
|
+
)
|
|
408
|
+
warnings.extend(projection_warnings)
|
|
409
|
+
disabled_fields = _request_field_tuple(
|
|
410
|
+
runtime_disabled_fields,
|
|
411
|
+
field_name="runtime disabled cache fields",
|
|
412
|
+
)
|
|
413
|
+
if disabled_fields:
|
|
414
|
+
disabled = set(disabled_fields)
|
|
415
|
+
allowed_fields = tuple(field for field in allowed_fields if field not in disabled)
|
|
416
|
+
warnings.append(
|
|
417
|
+
"Session-local cache capability downgrade disabled rejected field(s): "
|
|
418
|
+
+ ", ".join(disabled_fields)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
strategy = normalize_cache_strategy(state.strategy, allow_inherit=False)
|
|
422
|
+
enabled = bool(state.enabled) and strategy != CACHE_STRATEGY_NONE
|
|
423
|
+
if (
|
|
424
|
+
enabled
|
|
425
|
+
and state.emits_request_fields
|
|
426
|
+
and not _strategy_has_effective_surface(strategy, allowed_fields)
|
|
427
|
+
):
|
|
428
|
+
enabled = False
|
|
429
|
+
warnings.append(
|
|
430
|
+
f"Cache strategy {strategy!r} has no safe emitted field for protocol "
|
|
431
|
+
f"{normalized_protocol!r}."
|
|
432
|
+
)
|
|
433
|
+
if not enabled and strategy == CACHE_STRATEGY_NONE:
|
|
434
|
+
allowed_fields = ()
|
|
435
|
+
|
|
436
|
+
trusted_usage_fields = _trusted_usage_fields(
|
|
437
|
+
read=state.reports_cache_read_tokens,
|
|
438
|
+
write=state.reports_cache_write_tokens,
|
|
439
|
+
)
|
|
440
|
+
usage_schema = state.usage_schema
|
|
441
|
+
if not trusted_usage_fields and usage_schema != CACHE_USAGE_SCHEMA_NONE:
|
|
442
|
+
usage_schema = CACHE_USAGE_SCHEMA_NONE
|
|
443
|
+
|
|
444
|
+
return EffectiveCacheCapability(
|
|
445
|
+
provider_key=normalized_provider,
|
|
446
|
+
protocol=normalized_protocol,
|
|
447
|
+
model=normalized_model,
|
|
448
|
+
strategy=strategy if enabled or strategy != CACHE_STRATEGY_INHERIT else CACHE_STRATEGY_NONE,
|
|
449
|
+
enabled=enabled,
|
|
450
|
+
source=state.source,
|
|
451
|
+
supports_prompt_cache_key=PROMPT_CACHE_KEY_FIELD in allowed_fields,
|
|
452
|
+
supports_prompt_cache_retention=PROMPT_CACHE_RETENTION_FIELD in allowed_fields,
|
|
453
|
+
supports_cache_control=CACHE_CONTROL_FIELD in allowed_fields,
|
|
454
|
+
supports_explicit_cached_content=CACHED_CONTENT_FIELD in allowed_fields,
|
|
455
|
+
reports_cache_read_tokens=state.reports_cache_read_tokens,
|
|
456
|
+
reports_cache_write_tokens=state.reports_cache_write_tokens,
|
|
457
|
+
emits_request_fields=state.emits_request_fields,
|
|
458
|
+
usage_schema=usage_schema,
|
|
459
|
+
min_cacheable_tokens=state.min_cacheable_tokens,
|
|
460
|
+
emitted_fields=allowed_fields if enabled else (),
|
|
461
|
+
trusted_usage_fields=trusted_usage_fields,
|
|
462
|
+
notes=state.notes,
|
|
463
|
+
warnings=tuple(_dedupe(warnings)),
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def profile_cache_capability_from_preset(
|
|
468
|
+
preset: ProfilePreset | None,
|
|
469
|
+
) -> CacheCapabilitySpec | None:
|
|
470
|
+
if preset is None:
|
|
471
|
+
return None
|
|
472
|
+
return getattr(preset, "cache_capability", None)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def profile_cache_capability(profile: ProfileSpec | None) -> CacheCapabilitySpec | None:
|
|
476
|
+
if profile is None:
|
|
477
|
+
return None
|
|
478
|
+
return getattr(profile, "cache_capability", None)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def cache_capability_diagnostic_rows(
|
|
482
|
+
capability: EffectiveCacheCapability,
|
|
483
|
+
) -> tuple[tuple[str, str], ...]:
|
|
484
|
+
emitted_fields = ", ".join(capability.emitted_fields) if capability.emitted_fields else "none"
|
|
485
|
+
usage_fields = (
|
|
486
|
+
", ".join(capability.trusted_usage_fields) if capability.trusted_usage_fields else "none"
|
|
487
|
+
)
|
|
488
|
+
warnings = "; ".join(capability.warnings) if capability.warnings else "none"
|
|
489
|
+
return (
|
|
490
|
+
("cache_status", capability.status),
|
|
491
|
+
("cache_strategy", capability.strategy),
|
|
492
|
+
("cache_capability_source", capability.source),
|
|
493
|
+
("cache_emitted_fields", emitted_fields),
|
|
494
|
+
("cache_usage_fields", usage_fields),
|
|
495
|
+
("cache_usage_schema", capability.usage_schema),
|
|
496
|
+
("cache_warnings", warnings),
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def normalize_cache_strategy(
|
|
501
|
+
value: str | None,
|
|
502
|
+
*,
|
|
503
|
+
allow_inherit: bool = False,
|
|
504
|
+
field_name: str = "cache strategy",
|
|
505
|
+
) -> str:
|
|
506
|
+
normalized = str(value or "").strip().lower()
|
|
507
|
+
if not normalized:
|
|
508
|
+
normalized = CACHE_STRATEGY_INHERIT if allow_inherit else CACHE_STRATEGY_NONE
|
|
509
|
+
allowed = PROFILE_CACHE_STRATEGIES if allow_inherit else KNOWN_CACHE_STRATEGIES
|
|
510
|
+
if normalized not in allowed:
|
|
511
|
+
allowed_text = ", ".join(sorted(allowed))
|
|
512
|
+
raise ValueError(f"{field_name} must be one of: {allowed_text}")
|
|
513
|
+
if normalized == CACHE_STRATEGY_INHERIT and not allow_inherit:
|
|
514
|
+
return CACHE_STRATEGY_NONE
|
|
515
|
+
return normalized
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def normalize_cache_usage_schema(
|
|
519
|
+
value: str | None,
|
|
520
|
+
*,
|
|
521
|
+
field_name: str = "cache usage_schema",
|
|
522
|
+
) -> str:
|
|
523
|
+
normalized = str(value or "").strip().lower()
|
|
524
|
+
if not normalized:
|
|
525
|
+
normalized = CACHE_USAGE_SCHEMA_NONE
|
|
526
|
+
if normalized not in KNOWN_CACHE_USAGE_SCHEMAS:
|
|
527
|
+
allowed_text = ", ".join(sorted(KNOWN_CACHE_USAGE_SCHEMAS))
|
|
528
|
+
raise ValueError(f"{field_name} must be one of: {allowed_text}")
|
|
529
|
+
return normalized
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _coerce_spec(
|
|
533
|
+
value: CacheCapabilitySpec | Mapping[str, Any] | None,
|
|
534
|
+
*,
|
|
535
|
+
source: str,
|
|
536
|
+
) -> CacheCapabilitySpec | None:
|
|
537
|
+
if value is None:
|
|
538
|
+
return None
|
|
539
|
+
if isinstance(value, CacheCapabilitySpec):
|
|
540
|
+
if value.source == source:
|
|
541
|
+
return value
|
|
542
|
+
return CacheCapabilitySpec(
|
|
543
|
+
strategy=value.strategy,
|
|
544
|
+
enabled=value.enabled,
|
|
545
|
+
supports_prompt_cache_key=value.supports_prompt_cache_key,
|
|
546
|
+
supports_prompt_cache_retention=value.supports_prompt_cache_retention,
|
|
547
|
+
supports_cache_control=value.supports_cache_control,
|
|
548
|
+
supports_explicit_cached_content=value.supports_explicit_cached_content,
|
|
549
|
+
reports_cache_read_tokens=value.reports_cache_read_tokens,
|
|
550
|
+
reports_cache_write_tokens=value.reports_cache_write_tokens,
|
|
551
|
+
emits_request_fields=value.emits_request_fields,
|
|
552
|
+
request_fields=value.request_fields,
|
|
553
|
+
usage_schema=value.usage_schema,
|
|
554
|
+
min_cacheable_tokens=value.min_cacheable_tokens,
|
|
555
|
+
notes=value.notes,
|
|
556
|
+
source=source,
|
|
557
|
+
model_overrides=value.model_overrides,
|
|
558
|
+
model_family_overrides=value.model_family_overrides,
|
|
559
|
+
endpoint_overrides=value.endpoint_overrides,
|
|
560
|
+
)
|
|
561
|
+
return CacheCapabilitySpec.from_mapping(value, source=source)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _scoped_specs(value: Any, *, source: str) -> tuple[tuple[str, CacheCapabilitySpec], ...]:
|
|
565
|
+
if value is None:
|
|
566
|
+
return ()
|
|
567
|
+
if not isinstance(value, Mapping):
|
|
568
|
+
raise ValueError(f"{source} must be an object.")
|
|
569
|
+
specs: list[tuple[str, CacheCapabilitySpec]] = []
|
|
570
|
+
for key, raw_spec in value.items():
|
|
571
|
+
label = str(key or "").strip()
|
|
572
|
+
if not label:
|
|
573
|
+
continue
|
|
574
|
+
spec = CacheCapabilitySpec.from_mapping(raw_spec, source=f"{source}[{label!r}]")
|
|
575
|
+
if spec is not None:
|
|
576
|
+
specs.append((label, spec))
|
|
577
|
+
return tuple(specs)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _apply_scoped_specs(
|
|
581
|
+
state: _CacheCapabilityState,
|
|
582
|
+
spec: CacheCapabilitySpec,
|
|
583
|
+
*,
|
|
584
|
+
model: str,
|
|
585
|
+
base_url: str | None,
|
|
586
|
+
origin: str,
|
|
587
|
+
) -> _CacheCapabilityState:
|
|
588
|
+
resolved = state
|
|
589
|
+
for key, scoped_spec in spec.endpoint_overrides:
|
|
590
|
+
if _endpoint_rule_matches(key, base_url):
|
|
591
|
+
resolved = _apply_spec(resolved, scoped_spec, origin=origin)
|
|
592
|
+
for key, scoped_spec in spec.model_family_overrides:
|
|
593
|
+
if _model_family_rule_matches(key, model):
|
|
594
|
+
resolved = _apply_spec(resolved, scoped_spec, origin=origin)
|
|
595
|
+
for key, scoped_spec in spec.model_overrides:
|
|
596
|
+
if _model_rule_matches(key, model):
|
|
597
|
+
resolved = _apply_spec(resolved, scoped_spec, origin=origin)
|
|
598
|
+
return resolved
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _endpoint_rule_matches(rule: str, base_url: str | None) -> bool:
|
|
602
|
+
normalized_rule = _normalize_endpoint_rule(rule)
|
|
603
|
+
if not normalized_rule:
|
|
604
|
+
return False
|
|
605
|
+
normalized_url = _normalize_endpoint_rule(base_url)
|
|
606
|
+
if normalized_url == normalized_rule:
|
|
607
|
+
return True
|
|
608
|
+
return _endpoint_host(base_url) == normalized_rule
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _normalize_endpoint_rule(value: str | None) -> str:
|
|
612
|
+
text = str(value or "").strip().rstrip("/").lower()
|
|
613
|
+
if not text:
|
|
614
|
+
return ""
|
|
615
|
+
host = _endpoint_host(text)
|
|
616
|
+
if "://" not in text and "/" not in text and host:
|
|
617
|
+
return host
|
|
618
|
+
return text
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _endpoint_host(value: str | None) -> str:
|
|
622
|
+
text = str(value or "").strip()
|
|
623
|
+
if not text:
|
|
624
|
+
return ""
|
|
625
|
+
try:
|
|
626
|
+
parsed = urlsplit(text if "://" in text else f"https://{text}")
|
|
627
|
+
except ValueError:
|
|
628
|
+
return ""
|
|
629
|
+
return (parsed.hostname or "").rstrip(".").lower()
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _model_rule_matches(rule: str, model: str) -> bool:
|
|
633
|
+
normalized_rule = str(rule or "").strip().casefold()
|
|
634
|
+
normalized_model = str(model or "").strip().casefold()
|
|
635
|
+
if not normalized_rule or not normalized_model:
|
|
636
|
+
return False
|
|
637
|
+
return normalized_model == normalized_rule or fnmatch.fnmatchcase(
|
|
638
|
+
normalized_model,
|
|
639
|
+
normalized_rule,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _model_family_rule_matches(rule: str, model: str) -> bool:
|
|
644
|
+
normalized_rule = str(rule or "").strip().casefold().rstrip("*")
|
|
645
|
+
normalized_model = str(model or "").strip().casefold()
|
|
646
|
+
if not normalized_rule or not normalized_model:
|
|
647
|
+
return False
|
|
648
|
+
return normalized_model.startswith(normalized_rule)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _state_from_transport_capabilities(
|
|
652
|
+
capabilities: ProviderProtocolCapabilities | None,
|
|
653
|
+
) -> _CacheCapabilityState:
|
|
654
|
+
if capabilities is None:
|
|
655
|
+
return _CacheCapabilityState(
|
|
656
|
+
strategy=CACHE_STRATEGY_NONE,
|
|
657
|
+
enabled=False,
|
|
658
|
+
supports_prompt_cache_key=False,
|
|
659
|
+
supports_prompt_cache_retention=False,
|
|
660
|
+
supports_cache_control=False,
|
|
661
|
+
supports_explicit_cached_content=False,
|
|
662
|
+
reports_cache_read_tokens=False,
|
|
663
|
+
reports_cache_write_tokens=False,
|
|
664
|
+
emits_request_fields=False,
|
|
665
|
+
request_fields=(),
|
|
666
|
+
usage_schema=CACHE_USAGE_SCHEMA_NONE,
|
|
667
|
+
min_cacheable_tokens=None,
|
|
668
|
+
source="default",
|
|
669
|
+
notes=("No bundled cache capability metadata for this provider/protocol.",),
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
strategy = normalize_cache_strategy(capabilities.cache_strategy, allow_inherit=False)
|
|
673
|
+
enabled = strategy != CACHE_STRATEGY_NONE
|
|
674
|
+
return _CacheCapabilityState(
|
|
675
|
+
strategy=strategy,
|
|
676
|
+
enabled=enabled,
|
|
677
|
+
supports_prompt_cache_key=bool(capabilities.supports_prompt_cache_key),
|
|
678
|
+
supports_prompt_cache_retention=bool(capabilities.supports_prompt_cache_retention),
|
|
679
|
+
supports_cache_control=bool(capabilities.supports_cache_control),
|
|
680
|
+
supports_explicit_cached_content=bool(capabilities.supports_explicit_cached_content),
|
|
681
|
+
reports_cache_read_tokens=bool(capabilities.reports_cache_read_tokens),
|
|
682
|
+
reports_cache_write_tokens=bool(capabilities.reports_cache_write_tokens),
|
|
683
|
+
emits_request_fields=True,
|
|
684
|
+
request_fields=(),
|
|
685
|
+
usage_schema=_usage_schema_for_strategy(strategy),
|
|
686
|
+
min_cacheable_tokens=_min_tokens_for_strategy(strategy),
|
|
687
|
+
source="protocol",
|
|
688
|
+
notes=(),
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _apply_spec(
|
|
693
|
+
state: _CacheCapabilityState,
|
|
694
|
+
spec: CacheCapabilitySpec,
|
|
695
|
+
*,
|
|
696
|
+
origin: str,
|
|
697
|
+
) -> _CacheCapabilityState:
|
|
698
|
+
if spec.enabled is False:
|
|
699
|
+
return _CacheCapabilityState(
|
|
700
|
+
strategy=CACHE_STRATEGY_NONE,
|
|
701
|
+
enabled=False,
|
|
702
|
+
supports_prompt_cache_key=False,
|
|
703
|
+
supports_prompt_cache_retention=False,
|
|
704
|
+
supports_cache_control=False,
|
|
705
|
+
supports_explicit_cached_content=False,
|
|
706
|
+
reports_cache_read_tokens=False,
|
|
707
|
+
reports_cache_write_tokens=False,
|
|
708
|
+
emits_request_fields=False,
|
|
709
|
+
request_fields=(),
|
|
710
|
+
usage_schema=CACHE_USAGE_SCHEMA_NONE,
|
|
711
|
+
min_cacheable_tokens=spec.min_cacheable_tokens,
|
|
712
|
+
source=spec.source,
|
|
713
|
+
notes=_dedupe((*state.notes, *spec.notes)),
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
strategy = state.strategy
|
|
717
|
+
if spec.strategy != CACHE_STRATEGY_INHERIT:
|
|
718
|
+
strategy = spec.strategy
|
|
719
|
+
enabled = state.enabled if spec.enabled is None else bool(spec.enabled)
|
|
720
|
+
if strategy == CACHE_STRATEGY_NONE:
|
|
721
|
+
enabled = False
|
|
722
|
+
emits_request_fields = state.emits_request_fields
|
|
723
|
+
if spec.emits_request_fields is not None:
|
|
724
|
+
emits_request_fields = bool(spec.emits_request_fields)
|
|
725
|
+
elif origin == "profile" and spec.enabled is True:
|
|
726
|
+
# A profile override is an explicit advanced user opt-in, including scoped
|
|
727
|
+
# models/model_families/endpoints overrides, which inherit the profile
|
|
728
|
+
# origin. Preset metadata can stay diagnostic-only by setting
|
|
729
|
+
# emits_request_fields=False.
|
|
730
|
+
emits_request_fields = True
|
|
731
|
+
|
|
732
|
+
return _CacheCapabilityState(
|
|
733
|
+
strategy=strategy,
|
|
734
|
+
enabled=enabled,
|
|
735
|
+
supports_prompt_cache_key=_merge_bool(
|
|
736
|
+
state.supports_prompt_cache_key,
|
|
737
|
+
spec.supports_prompt_cache_key,
|
|
738
|
+
),
|
|
739
|
+
supports_prompt_cache_retention=_merge_bool(
|
|
740
|
+
state.supports_prompt_cache_retention,
|
|
741
|
+
spec.supports_prompt_cache_retention,
|
|
742
|
+
),
|
|
743
|
+
supports_cache_control=_merge_bool(
|
|
744
|
+
state.supports_cache_control,
|
|
745
|
+
spec.supports_cache_control,
|
|
746
|
+
),
|
|
747
|
+
supports_explicit_cached_content=_merge_bool(
|
|
748
|
+
state.supports_explicit_cached_content,
|
|
749
|
+
spec.supports_explicit_cached_content,
|
|
750
|
+
),
|
|
751
|
+
reports_cache_read_tokens=_merge_bool(
|
|
752
|
+
state.reports_cache_read_tokens,
|
|
753
|
+
spec.reports_cache_read_tokens,
|
|
754
|
+
),
|
|
755
|
+
reports_cache_write_tokens=_merge_bool(
|
|
756
|
+
state.reports_cache_write_tokens,
|
|
757
|
+
spec.reports_cache_write_tokens,
|
|
758
|
+
),
|
|
759
|
+
emits_request_fields=emits_request_fields,
|
|
760
|
+
request_fields=_dedupe((*state.request_fields, *spec.request_fields)),
|
|
761
|
+
usage_schema=spec.usage_schema
|
|
762
|
+
or state.usage_schema
|
|
763
|
+
or _usage_schema_for_strategy(strategy),
|
|
764
|
+
min_cacheable_tokens=(
|
|
765
|
+
spec.min_cacheable_tokens
|
|
766
|
+
if spec.min_cacheable_tokens is not None
|
|
767
|
+
else state.min_cacheable_tokens
|
|
768
|
+
),
|
|
769
|
+
source=spec.source if spec.has_values() else state.source,
|
|
770
|
+
notes=_dedupe((*state.notes, *spec.notes)),
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _projected_fields(
|
|
775
|
+
*,
|
|
776
|
+
state: _CacheCapabilityState,
|
|
777
|
+
protocol: str,
|
|
778
|
+
) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
|
779
|
+
physical_fields = _transport_projection_fields(protocol)
|
|
780
|
+
if not state.emits_request_fields:
|
|
781
|
+
return (), ()
|
|
782
|
+
wanted: list[str] = []
|
|
783
|
+
if state.supports_prompt_cache_key:
|
|
784
|
+
wanted.append(PROMPT_CACHE_KEY_FIELD)
|
|
785
|
+
if state.supports_prompt_cache_retention:
|
|
786
|
+
wanted.append(PROMPT_CACHE_RETENTION_FIELD)
|
|
787
|
+
if state.supports_cache_control:
|
|
788
|
+
wanted.append(CACHE_CONTROL_FIELD)
|
|
789
|
+
if state.supports_explicit_cached_content:
|
|
790
|
+
wanted.append(CACHED_CONTENT_FIELD)
|
|
791
|
+
wanted.extend(state.request_fields)
|
|
792
|
+
|
|
793
|
+
strategy_fields = _strategy_projection_fields(state.strategy)
|
|
794
|
+
wanted = [field for field in wanted if field in strategy_fields]
|
|
795
|
+
fields = tuple(field for field in wanted if field in physical_fields)
|
|
796
|
+
dropped = [field for field in wanted if field not in physical_fields]
|
|
797
|
+
warnings = tuple(
|
|
798
|
+
f"Dropped cache field {field!r}; protocol {protocol!r} cannot project it."
|
|
799
|
+
for field in dropped
|
|
800
|
+
)
|
|
801
|
+
return fields, warnings
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _transport_projection_fields(protocol: str) -> tuple[str, ...]:
|
|
805
|
+
normalized = str(protocol or "").strip().lower()
|
|
806
|
+
if normalized in {OPENAI_COMPAT_PROTOCOL, OPENAI_RESPONSES_PROTOCOL}:
|
|
807
|
+
fields = [PROMPT_CACHE_KEY_FIELD, PROMPT_CACHE_RETENTION_FIELD]
|
|
808
|
+
if normalized == OPENAI_COMPAT_PROTOCOL:
|
|
809
|
+
fields.extend(
|
|
810
|
+
(
|
|
811
|
+
CACHE_CONTROL_FIELD,
|
|
812
|
+
OPENROUTER_SESSION_ID_FIELD,
|
|
813
|
+
OPENROUTER_SESSION_ID_HEADER_FIELD,
|
|
814
|
+
XAI_CONVERSATION_ID_HEADER_FIELD,
|
|
815
|
+
)
|
|
816
|
+
)
|
|
817
|
+
return tuple(fields)
|
|
818
|
+
if normalized == ANTHROPIC_MESSAGES_PROTOCOL:
|
|
819
|
+
return (CACHE_CONTROL_FIELD,)
|
|
820
|
+
if normalized == GEMINI_GENERATE_CONTENT_PROTOCOL:
|
|
821
|
+
return (CACHED_CONTENT_FIELD,)
|
|
822
|
+
if normalized == GEMINI_INTERACTIONS_PROTOCOL:
|
|
823
|
+
return ()
|
|
824
|
+
return ()
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _strategy_projection_fields(strategy: str) -> tuple[str, ...]:
|
|
828
|
+
normalized = str(strategy or "").strip().lower()
|
|
829
|
+
if normalized == CACHE_STRATEGY_IMPLICIT_PROVIDER:
|
|
830
|
+
return (PROMPT_CACHE_KEY_FIELD,)
|
|
831
|
+
if normalized in {
|
|
832
|
+
CACHE_STRATEGY_OPENAI_PROMPT_CACHE,
|
|
833
|
+
}:
|
|
834
|
+
return (PROMPT_CACHE_KEY_FIELD, PROMPT_CACHE_RETENTION_FIELD)
|
|
835
|
+
if normalized == CACHE_STRATEGY_MISTRAL_PROMPT_CACHE_KEY:
|
|
836
|
+
return (PROMPT_CACHE_KEY_FIELD,)
|
|
837
|
+
if normalized == CACHE_STRATEGY_OPENROUTER_STICKY_SESSION:
|
|
838
|
+
return (OPENROUTER_SESSION_ID_FIELD, OPENROUTER_SESSION_ID_HEADER_FIELD)
|
|
839
|
+
if normalized == CACHE_STRATEGY_XAI_CONVERSATION_HEADER:
|
|
840
|
+
return (XAI_CONVERSATION_ID_HEADER_FIELD, PROMPT_CACHE_KEY_FIELD)
|
|
841
|
+
if normalized == CACHE_STRATEGY_ANTHROPIC_CACHE_CONTROL:
|
|
842
|
+
return (CACHE_CONTROL_FIELD,)
|
|
843
|
+
if normalized == CACHE_STRATEGY_GEMINI_EXPLICIT_CACHED_CONTENT:
|
|
844
|
+
return (CACHED_CONTENT_FIELD,)
|
|
845
|
+
if normalized == CACHE_STRATEGY_QWEN_CACHE_CONTROL_BLOCKS:
|
|
846
|
+
return (CACHE_CONTROL_FIELD,)
|
|
847
|
+
return ()
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def _strategy_has_effective_surface(strategy: str, projected_fields: tuple[str, ...]) -> bool:
|
|
851
|
+
if strategy in {
|
|
852
|
+
CACHE_STRATEGY_IMPLICIT_PROVIDER,
|
|
853
|
+
CACHE_STRATEGY_GEMINI_IMPLICIT,
|
|
854
|
+
CACHE_STRATEGY_OPENROUTER_STICKY_SESSION,
|
|
855
|
+
CACHE_STRATEGY_XAI_CONVERSATION_HEADER,
|
|
856
|
+
CACHE_STRATEGY_QWEN_CACHE_CONTROL_BLOCKS,
|
|
857
|
+
}:
|
|
858
|
+
return True
|
|
859
|
+
if strategy == CACHE_STRATEGY_NONE:
|
|
860
|
+
return False
|
|
861
|
+
return bool(projected_fields)
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _usage_schema_for_strategy(strategy: str) -> str:
|
|
865
|
+
if strategy in {
|
|
866
|
+
CACHE_STRATEGY_OPENAI_PROMPT_CACHE,
|
|
867
|
+
CACHE_STRATEGY_MISTRAL_PROMPT_CACHE_KEY,
|
|
868
|
+
}:
|
|
869
|
+
return CACHE_USAGE_SCHEMA_OPENAI
|
|
870
|
+
if strategy == CACHE_STRATEGY_ANTHROPIC_CACHE_CONTROL:
|
|
871
|
+
return CACHE_USAGE_SCHEMA_ANTHROPIC
|
|
872
|
+
if strategy in {
|
|
873
|
+
CACHE_STRATEGY_GEMINI_EXPLICIT_CACHED_CONTENT,
|
|
874
|
+
CACHE_STRATEGY_GEMINI_IMPLICIT,
|
|
875
|
+
}:
|
|
876
|
+
return CACHE_USAGE_SCHEMA_GEMINI
|
|
877
|
+
if strategy in {
|
|
878
|
+
CACHE_STRATEGY_IMPLICIT_PROVIDER,
|
|
879
|
+
CACHE_STRATEGY_OPENROUTER_STICKY_SESSION,
|
|
880
|
+
CACHE_STRATEGY_XAI_CONVERSATION_HEADER,
|
|
881
|
+
CACHE_STRATEGY_QWEN_CACHE_CONTROL_BLOCKS,
|
|
882
|
+
}:
|
|
883
|
+
return CACHE_USAGE_SCHEMA_PROVIDER
|
|
884
|
+
return CACHE_USAGE_SCHEMA_NONE
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _min_tokens_for_strategy(strategy: str) -> int | None:
|
|
888
|
+
if strategy in {
|
|
889
|
+
CACHE_STRATEGY_OPENAI_PROMPT_CACHE,
|
|
890
|
+
CACHE_STRATEGY_MISTRAL_PROMPT_CACHE_KEY,
|
|
891
|
+
CACHE_STRATEGY_QWEN_CACHE_CONTROL_BLOCKS,
|
|
892
|
+
}:
|
|
893
|
+
return 1024
|
|
894
|
+
if strategy == CACHE_STRATEGY_GEMINI_EXPLICIT_CACHED_CONTENT:
|
|
895
|
+
return 4096
|
|
896
|
+
return None
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _trusted_usage_fields(*, read: bool, write: bool) -> tuple[str, ...]:
|
|
900
|
+
fields: list[str] = []
|
|
901
|
+
if read:
|
|
902
|
+
fields.append("cache_read_input_tokens")
|
|
903
|
+
if write:
|
|
904
|
+
fields.extend(
|
|
905
|
+
[
|
|
906
|
+
"cache_creation_input_tokens",
|
|
907
|
+
"cache_creation_5m_input_tokens",
|
|
908
|
+
"cache_creation_1h_input_tokens",
|
|
909
|
+
]
|
|
910
|
+
)
|
|
911
|
+
return tuple(fields)
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _merge_bool(current: bool, override: bool | None) -> bool:
|
|
915
|
+
return current if override is None else bool(override)
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _optional_bool(value: Any, *, field_name: str) -> bool | None:
|
|
919
|
+
if value is None:
|
|
920
|
+
return None
|
|
921
|
+
if isinstance(value, bool):
|
|
922
|
+
return value
|
|
923
|
+
normalized = str(value).strip().lower()
|
|
924
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
925
|
+
return True
|
|
926
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
927
|
+
return False
|
|
928
|
+
raise ValueError(f"{field_name} must be true or false.")
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _optional_int(value: Any, *, field_name: str) -> int | None:
|
|
932
|
+
if value is None or str(value).strip() == "":
|
|
933
|
+
return None
|
|
934
|
+
try:
|
|
935
|
+
number = int(value)
|
|
936
|
+
except (TypeError, ValueError):
|
|
937
|
+
raise ValueError(f"{field_name} must be an integer.") from None
|
|
938
|
+
if number < 0:
|
|
939
|
+
raise ValueError(f"{field_name} must be non-negative.")
|
|
940
|
+
return number
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _string_tuple(value: Any, *, field_name: str) -> tuple[str, ...]:
|
|
944
|
+
if value is None:
|
|
945
|
+
return ()
|
|
946
|
+
if isinstance(value, str):
|
|
947
|
+
text = value.strip()
|
|
948
|
+
return (text,) if text else ()
|
|
949
|
+
if not isinstance(value, (list, tuple)):
|
|
950
|
+
raise ValueError(f"{field_name} must be a string or list of strings.")
|
|
951
|
+
items: list[str] = []
|
|
952
|
+
for item in value:
|
|
953
|
+
text = str(item).strip()
|
|
954
|
+
if text:
|
|
955
|
+
items.append(text)
|
|
956
|
+
return tuple(items)
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def _request_field_tuple(value: Any, *, field_name: str) -> tuple[str, ...]:
|
|
960
|
+
fields = _string_tuple(value, field_name=field_name)
|
|
961
|
+
unknown = [field for field in fields if field not in KNOWN_CACHE_REQUEST_FIELDS]
|
|
962
|
+
if unknown:
|
|
963
|
+
allowed_text = ", ".join(sorted(KNOWN_CACHE_REQUEST_FIELDS))
|
|
964
|
+
unknown_text = ", ".join(unknown)
|
|
965
|
+
raise ValueError(
|
|
966
|
+
f"{field_name} contains unknown field(s): {unknown_text}. "
|
|
967
|
+
f"Allowed fields: {allowed_text}."
|
|
968
|
+
)
|
|
969
|
+
return _dedupe(fields)
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def _safe_text(value: Any) -> str:
|
|
973
|
+
return str(value or "").strip()
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _dedupe(values: tuple[str, ...]) -> tuple[str, ...]:
|
|
977
|
+
result: list[str] = []
|
|
978
|
+
seen: set[str] = set()
|
|
979
|
+
for value in values:
|
|
980
|
+
text = str(value or "").strip()
|
|
981
|
+
if not text or text in seen:
|
|
982
|
+
continue
|
|
983
|
+
seen.add(text)
|
|
984
|
+
result.append(text)
|
|
985
|
+
return tuple(result)
|