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,244 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from collections.abc import Iterator, Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..request_estimation import estimate_message_tokens
|
|
9
|
+
from .cache_capabilities import CACHE_CONTROL_FIELD
|
|
10
|
+
|
|
11
|
+
_TEXT_BLOCK_TYPES = {"text", "input_text", "output_text"}
|
|
12
|
+
_DEFAULT_CACHE_CONTROL_MIN_TOKENS = 0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class CacheControlApplication:
|
|
17
|
+
messages: list[dict[str, Any]]
|
|
18
|
+
applied: bool
|
|
19
|
+
eligible: bool
|
|
20
|
+
prefix_message_count: int
|
|
21
|
+
prefix_estimated_tokens: int
|
|
22
|
+
min_tokens: int
|
|
23
|
+
reason: str
|
|
24
|
+
|
|
25
|
+
def policy_metadata(self) -> dict[str, Any]:
|
|
26
|
+
return {
|
|
27
|
+
"eligible": self.eligible,
|
|
28
|
+
"used": self.applied,
|
|
29
|
+
"cacheable_prefix_estimated_tokens": self.prefix_estimated_tokens,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def apply_openai_compatible_cache_control_breakpoint(
|
|
34
|
+
messages: list[dict[str, Any]],
|
|
35
|
+
*,
|
|
36
|
+
cache_policy: Mapping[str, Any] | None,
|
|
37
|
+
) -> CacheControlApplication:
|
|
38
|
+
copied = copy.deepcopy([message for message in messages if isinstance(message, dict)])
|
|
39
|
+
min_tokens = _cache_control_min_tokens(cache_policy)
|
|
40
|
+
prefix_count = cacheable_prefix_message_count(copied)
|
|
41
|
+
prefix_tokens = estimate_message_tokens(copied[:prefix_count])
|
|
42
|
+
if CACHE_CONTROL_FIELD not in _policy_emitted_fields(cache_policy):
|
|
43
|
+
return CacheControlApplication(
|
|
44
|
+
messages=copied,
|
|
45
|
+
applied=False,
|
|
46
|
+
eligible=False,
|
|
47
|
+
prefix_message_count=prefix_count,
|
|
48
|
+
prefix_estimated_tokens=prefix_tokens,
|
|
49
|
+
min_tokens=min_tokens,
|
|
50
|
+
reason="cache_control_not_emitted",
|
|
51
|
+
)
|
|
52
|
+
if count_cache_control_blocks(copied) > 0:
|
|
53
|
+
return CacheControlApplication(
|
|
54
|
+
messages=copied,
|
|
55
|
+
applied=True,
|
|
56
|
+
eligible=True,
|
|
57
|
+
prefix_message_count=prefix_count,
|
|
58
|
+
prefix_estimated_tokens=prefix_tokens,
|
|
59
|
+
min_tokens=min_tokens,
|
|
60
|
+
reason="existing_cache_control_block",
|
|
61
|
+
)
|
|
62
|
+
if prefix_count <= 0:
|
|
63
|
+
return CacheControlApplication(
|
|
64
|
+
messages=copied,
|
|
65
|
+
applied=False,
|
|
66
|
+
eligible=False,
|
|
67
|
+
prefix_message_count=prefix_count,
|
|
68
|
+
prefix_estimated_tokens=prefix_tokens,
|
|
69
|
+
min_tokens=min_tokens,
|
|
70
|
+
reason="no_cacheable_prefix",
|
|
71
|
+
)
|
|
72
|
+
if prefix_tokens < min_tokens:
|
|
73
|
+
return CacheControlApplication(
|
|
74
|
+
messages=copied,
|
|
75
|
+
applied=False,
|
|
76
|
+
eligible=False,
|
|
77
|
+
prefix_message_count=prefix_count,
|
|
78
|
+
prefix_estimated_tokens=prefix_tokens,
|
|
79
|
+
min_tokens=min_tokens,
|
|
80
|
+
reason="prefix_below_min_tokens",
|
|
81
|
+
)
|
|
82
|
+
for index in range(prefix_count - 1, -1, -1):
|
|
83
|
+
if _add_cache_control_to_message(copied[index]):
|
|
84
|
+
return CacheControlApplication(
|
|
85
|
+
messages=copied,
|
|
86
|
+
applied=True,
|
|
87
|
+
eligible=True,
|
|
88
|
+
prefix_message_count=prefix_count,
|
|
89
|
+
prefix_estimated_tokens=prefix_tokens,
|
|
90
|
+
min_tokens=min_tokens,
|
|
91
|
+
reason="cache_control_block_added",
|
|
92
|
+
)
|
|
93
|
+
return CacheControlApplication(
|
|
94
|
+
messages=copied,
|
|
95
|
+
applied=False,
|
|
96
|
+
eligible=False,
|
|
97
|
+
prefix_message_count=prefix_count,
|
|
98
|
+
prefix_estimated_tokens=prefix_tokens,
|
|
99
|
+
min_tokens=min_tokens,
|
|
100
|
+
reason="no_text_block_in_prefix",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cacheable_prefix_message_count(messages: list[dict[str, Any]]) -> int:
|
|
105
|
+
for index in range(len(messages) - 1, -1, -1):
|
|
106
|
+
if str(messages[index].get("role") or "").strip().lower() == "user":
|
|
107
|
+
return max(0, index)
|
|
108
|
+
return max(0, len(messages) - 1)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _is_request_payload(value: Mapping[str, Any]) -> bool:
|
|
112
|
+
return isinstance(value.get("messages"), (list, tuple))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _iter_block_carriers(blocks: Any) -> Iterator[Mapping[str, Any]]:
|
|
116
|
+
if not isinstance(blocks, (list, tuple)):
|
|
117
|
+
return
|
|
118
|
+
for block in blocks:
|
|
119
|
+
if not isinstance(block, Mapping):
|
|
120
|
+
continue
|
|
121
|
+
yield block
|
|
122
|
+
yield from _iter_block_carriers(block.get("content"))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _iter_cache_control_carriers(
|
|
126
|
+
value: Any,
|
|
127
|
+
*,
|
|
128
|
+
skip_root: bool = False,
|
|
129
|
+
) -> Iterator[Mapping[str, Any]]:
|
|
130
|
+
# Only positions where providers accept cache_control: the payload top
|
|
131
|
+
# level, messages and their content blocks, system blocks, and the top
|
|
132
|
+
# level of each tools[] entry. Never nested tool input schemas or other
|
|
133
|
+
# structured data, where a field merely named "cache_control" is data.
|
|
134
|
+
if isinstance(value, Mapping):
|
|
135
|
+
if _is_request_payload(value):
|
|
136
|
+
if not skip_root:
|
|
137
|
+
yield value
|
|
138
|
+
yield from _iter_block_carriers(value.get("messages"))
|
|
139
|
+
system = value.get("system")
|
|
140
|
+
if isinstance(system, (list, tuple)):
|
|
141
|
+
yield from _iter_block_carriers(system)
|
|
142
|
+
tools = value.get("tools")
|
|
143
|
+
if isinstance(tools, (list, tuple)):
|
|
144
|
+
for tool in tools:
|
|
145
|
+
if isinstance(tool, Mapping):
|
|
146
|
+
yield tool
|
|
147
|
+
return
|
|
148
|
+
if not skip_root:
|
|
149
|
+
yield value
|
|
150
|
+
yield from _iter_block_carriers(value.get("content"))
|
|
151
|
+
return
|
|
152
|
+
if isinstance(value, (list, tuple)):
|
|
153
|
+
yield from _iter_block_carriers(value)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def count_cache_control_blocks(value: Any) -> int:
|
|
157
|
+
return sum(
|
|
158
|
+
1 for carrier in _iter_cache_control_carriers(value) if CACHE_CONTROL_FIELD in carrier
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def count_explicit_cache_control_blocks(value: Any) -> int:
|
|
163
|
+
return sum(
|
|
164
|
+
1
|
|
165
|
+
for carrier in _iter_cache_control_carriers(value, skip_root=True)
|
|
166
|
+
if CACHE_CONTROL_FIELD in carrier
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def explicit_cache_control_payloads(value: Any) -> list[dict[str, Any]]:
|
|
171
|
+
payloads: list[dict[str, Any]] = []
|
|
172
|
+
for carrier in _iter_cache_control_carriers(value, skip_root=True):
|
|
173
|
+
cache_control = carrier.get(CACHE_CONTROL_FIELD)
|
|
174
|
+
if isinstance(cache_control, Mapping):
|
|
175
|
+
payloads.append(copy.deepcopy(dict(cache_control)))
|
|
176
|
+
return payloads
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def strip_cache_control_blocks(value: Any) -> bool:
|
|
180
|
+
stripped = False
|
|
181
|
+
for carrier in _iter_cache_control_carriers(value):
|
|
182
|
+
if isinstance(carrier, dict) and CACHE_CONTROL_FIELD in carrier:
|
|
183
|
+
carrier.pop(CACHE_CONTROL_FIELD, None)
|
|
184
|
+
stripped = True
|
|
185
|
+
return stripped
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _policy_emitted_fields(policy: Mapping[str, Any] | None) -> tuple[str, ...]:
|
|
189
|
+
if not isinstance(policy, Mapping):
|
|
190
|
+
return ()
|
|
191
|
+
raw = policy.get("emitted_fields")
|
|
192
|
+
if not isinstance(raw, (list, tuple)):
|
|
193
|
+
return ()
|
|
194
|
+
return tuple(str(item).strip() for item in raw if str(item).strip())
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _cache_control_min_tokens(policy: Mapping[str, Any] | None) -> int:
|
|
198
|
+
if not isinstance(policy, Mapping):
|
|
199
|
+
return _DEFAULT_CACHE_CONTROL_MIN_TOKENS
|
|
200
|
+
for key in ("min_tokens", "min_cacheable_tokens"):
|
|
201
|
+
try:
|
|
202
|
+
value = int(policy.get(key))
|
|
203
|
+
except (TypeError, ValueError):
|
|
204
|
+
continue
|
|
205
|
+
if value >= 0:
|
|
206
|
+
return value
|
|
207
|
+
return _DEFAULT_CACHE_CONTROL_MIN_TOKENS
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _add_cache_control_to_message(message: dict[str, Any]) -> bool:
|
|
211
|
+
role = str(message.get("role") or "").strip().lower()
|
|
212
|
+
if role == "tool":
|
|
213
|
+
return False
|
|
214
|
+
content = message.get("content")
|
|
215
|
+
if isinstance(content, list):
|
|
216
|
+
for index in range(len(content) - 1, -1, -1):
|
|
217
|
+
item = content[index]
|
|
218
|
+
if not _is_text_block(item):
|
|
219
|
+
continue
|
|
220
|
+
copied_item = dict(item)
|
|
221
|
+
copied_item[CACHE_CONTROL_FIELD] = {"type": "ephemeral"}
|
|
222
|
+
content[index] = copied_item
|
|
223
|
+
return True
|
|
224
|
+
return False
|
|
225
|
+
if isinstance(content, str) and content.strip():
|
|
226
|
+
message["content"] = [
|
|
227
|
+
{
|
|
228
|
+
"type": "text",
|
|
229
|
+
"text": content,
|
|
230
|
+
CACHE_CONTROL_FIELD: {"type": "ephemeral"},
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
return True
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _is_text_block(value: Any) -> bool:
|
|
238
|
+
if not isinstance(value, Mapping) or CACHE_CONTROL_FIELD in value:
|
|
239
|
+
return False
|
|
240
|
+
block_type = str(value.get("type") or "text").strip().lower()
|
|
241
|
+
if block_type not in _TEXT_BLOCK_TYPES:
|
|
242
|
+
return False
|
|
243
|
+
text = value.get("text")
|
|
244
|
+
return isinstance(text, str) and bool(text.strip())
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from ..config import (
|
|
13
|
+
resolve_anthropic_prompt_cache_enabled,
|
|
14
|
+
resolve_anthropic_prompt_cache_ttl,
|
|
15
|
+
resolve_prompt_cache_mode,
|
|
16
|
+
)
|
|
17
|
+
from .cache_capabilities import (
|
|
18
|
+
CACHE_CONTROL_FIELD,
|
|
19
|
+
CACHED_CONTENT_FIELD,
|
|
20
|
+
OPENROUTER_SESSION_ID_FIELD,
|
|
21
|
+
OPENROUTER_SESSION_ID_HEADER_FIELD,
|
|
22
|
+
PROMPT_CACHE_KEY_FIELD,
|
|
23
|
+
PROMPT_CACHE_RETENTION_FIELD,
|
|
24
|
+
XAI_CONVERSATION_ID_HEADER_FIELD,
|
|
25
|
+
EffectiveCacheCapability,
|
|
26
|
+
resolve_effective_cache_capability,
|
|
27
|
+
)
|
|
28
|
+
from .protocols import ProviderProtocolCapabilities
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from ..config import AppConfig
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_CACHE_KEY_LABEL_RE = re.compile(r"[^a-z0-9_.:-]+")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ResolvedPromptCachePolicy:
|
|
39
|
+
prompt_cache_key: str | None = None
|
|
40
|
+
prompt_cache_retention: str | None = None
|
|
41
|
+
anthropic_cache_control_enabled: bool = False
|
|
42
|
+
anthropic_cache_control_ttl: str = "5m"
|
|
43
|
+
gemini_explicit_cached_content_enabled: bool = False
|
|
44
|
+
gemini_cached_content_ttl: str | None = None
|
|
45
|
+
mode: str = "manual"
|
|
46
|
+
strategy: str = "none"
|
|
47
|
+
capability_source: str = "default"
|
|
48
|
+
allowed_fields: tuple[str, ...] = ()
|
|
49
|
+
emitted_fields: tuple[str, ...] = ()
|
|
50
|
+
trusted_usage_fields: tuple[str, ...] = ()
|
|
51
|
+
usage_schema: str = "none"
|
|
52
|
+
min_cacheable_tokens: int | None = None
|
|
53
|
+
request_field_values: tuple[tuple[str, str], ...] = ()
|
|
54
|
+
warnings: tuple[str, ...] = ()
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def status(self) -> str:
|
|
58
|
+
if self.mode == "off":
|
|
59
|
+
return "disabled"
|
|
60
|
+
if self.emitted_fields:
|
|
61
|
+
return "enabled"
|
|
62
|
+
if self.strategy == "none":
|
|
63
|
+
return "unsupported"
|
|
64
|
+
if not self.allowed_fields and self.warnings:
|
|
65
|
+
return "unsupported"
|
|
66
|
+
return "available"
|
|
67
|
+
|
|
68
|
+
def telemetry_metadata(self) -> dict[str, Any]:
|
|
69
|
+
metadata: dict[str, Any] = {
|
|
70
|
+
"status": self.status,
|
|
71
|
+
"strategy": self.strategy,
|
|
72
|
+
"mode": "automatic" if self.mode == "auto" else self.mode,
|
|
73
|
+
"enabled": self.status == "enabled",
|
|
74
|
+
"capability_source": self.capability_source,
|
|
75
|
+
"source": self.capability_source,
|
|
76
|
+
"allowed_fields": list(self.allowed_fields),
|
|
77
|
+
"emitted_fields": list(self.emitted_fields),
|
|
78
|
+
"trusted_usage_fields": list(self.trusted_usage_fields),
|
|
79
|
+
"usage_schema": self.usage_schema,
|
|
80
|
+
"emits_request_fields": bool(self.allowed_fields),
|
|
81
|
+
}
|
|
82
|
+
if self.prompt_cache_retention:
|
|
83
|
+
metadata["retention"] = self.prompt_cache_retention
|
|
84
|
+
if self.prompt_cache_key and PROMPT_CACHE_KEY_FIELD in self.emitted_fields:
|
|
85
|
+
metadata["prompt_cache_key_hash"] = hashlib.sha256(
|
|
86
|
+
self.prompt_cache_key.encode("utf-8")
|
|
87
|
+
).hexdigest()
|
|
88
|
+
if self.anthropic_cache_control_enabled:
|
|
89
|
+
metadata["ttl"] = self.anthropic_cache_control_ttl
|
|
90
|
+
elif self.gemini_explicit_cached_content_enabled and self.gemini_cached_content_ttl:
|
|
91
|
+
metadata["ttl"] = self.gemini_cached_content_ttl
|
|
92
|
+
if self.min_cacheable_tokens is not None:
|
|
93
|
+
metadata["min_tokens"] = self.min_cacheable_tokens
|
|
94
|
+
if self.warnings:
|
|
95
|
+
metadata["warnings"] = list(self.warnings)
|
|
96
|
+
return metadata
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def build_prompt_cache_namespace(
|
|
100
|
+
*,
|
|
101
|
+
workspace_root: str | os.PathLike[str] | None = None,
|
|
102
|
+
role: str | None = None,
|
|
103
|
+
profile_name: str | None = None,
|
|
104
|
+
session_id: str | None = None,
|
|
105
|
+
) -> str | None:
|
|
106
|
+
parts: list[tuple[str, str]] = []
|
|
107
|
+
if workspace_root is not None:
|
|
108
|
+
parts.append(("workspace", _normalize_workspace_root(workspace_root)))
|
|
109
|
+
normalized_role = str(role or "").strip().lower()
|
|
110
|
+
if normalized_role:
|
|
111
|
+
parts.append(("role", normalized_role))
|
|
112
|
+
normalized_profile = str(profile_name or "").strip().lower()
|
|
113
|
+
if normalized_profile:
|
|
114
|
+
parts.append(("profile", normalized_profile))
|
|
115
|
+
normalized_session = str(session_id or "").strip().lower()
|
|
116
|
+
if normalized_session:
|
|
117
|
+
parts.append(("session", normalized_session))
|
|
118
|
+
if not parts:
|
|
119
|
+
return None
|
|
120
|
+
return json.dumps(parts, ensure_ascii=False, separators=(",", ":"))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def derive_prompt_cache_stream_key(
|
|
124
|
+
*,
|
|
125
|
+
session_id: str,
|
|
126
|
+
parent_session_id: str | None = None,
|
|
127
|
+
) -> str | None:
|
|
128
|
+
"""Return one stable cache-affinity key for a parent or its child stream."""
|
|
129
|
+
normalized_parent = str(parent_session_id or "").strip()
|
|
130
|
+
if normalized_parent:
|
|
131
|
+
digest = hashlib.blake2s(
|
|
132
|
+
f"{normalized_parent}:children".encode(),
|
|
133
|
+
digest_size=12,
|
|
134
|
+
).hexdigest()
|
|
135
|
+
return f"alysis:children:{digest}"
|
|
136
|
+
normalized_session = str(session_id or "").strip()
|
|
137
|
+
return normalized_session or None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def resolve_prompt_cache_policy(
|
|
141
|
+
*,
|
|
142
|
+
cfg: AppConfig | None,
|
|
143
|
+
capabilities: ProviderProtocolCapabilities | None,
|
|
144
|
+
provider_key: str,
|
|
145
|
+
protocol: str,
|
|
146
|
+
model: str,
|
|
147
|
+
prompt_cache_key: str | None,
|
|
148
|
+
prompt_cache_retention: str | None,
|
|
149
|
+
prompt_cache_namespace: str | None = None,
|
|
150
|
+
cache_capability: EffectiveCacheCapability | None = None,
|
|
151
|
+
) -> ResolvedPromptCachePolicy:
|
|
152
|
+
mode = resolve_prompt_cache_mode(cfg)
|
|
153
|
+
ttl = resolve_anthropic_prompt_cache_ttl(cfg)
|
|
154
|
+
effective_capability = cache_capability or resolve_effective_cache_capability(
|
|
155
|
+
provider_key=provider_key,
|
|
156
|
+
protocol=protocol,
|
|
157
|
+
model=model,
|
|
158
|
+
transport_capabilities=capabilities,
|
|
159
|
+
)
|
|
160
|
+
if mode == "off" or not effective_capability.enabled:
|
|
161
|
+
return ResolvedPromptCachePolicy(
|
|
162
|
+
mode=mode,
|
|
163
|
+
anthropic_cache_control_ttl=ttl,
|
|
164
|
+
strategy=effective_capability.strategy,
|
|
165
|
+
capability_source=effective_capability.source,
|
|
166
|
+
allowed_fields=effective_capability.emitted_fields,
|
|
167
|
+
emitted_fields=(),
|
|
168
|
+
trusted_usage_fields=effective_capability.trusted_usage_fields,
|
|
169
|
+
usage_schema=effective_capability.usage_schema,
|
|
170
|
+
min_cacheable_tokens=effective_capability.min_cacheable_tokens,
|
|
171
|
+
warnings=effective_capability.warnings,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
strategy = effective_capability.strategy
|
|
175
|
+
cache_config = getattr(cfg, "cache", None)
|
|
176
|
+
prompt_cache_key_enabled = cache_config is None or bool(
|
|
177
|
+
getattr(cache_config, "prompt_cache_key_enabled", True)
|
|
178
|
+
)
|
|
179
|
+
explicit_key = _clean_optional(prompt_cache_key) if prompt_cache_key_enabled else None
|
|
180
|
+
explicit_retention = _clean_optional(prompt_cache_retention)
|
|
181
|
+
effective_key = explicit_key
|
|
182
|
+
needs_affinity_key = any(
|
|
183
|
+
field
|
|
184
|
+
in {
|
|
185
|
+
PROMPT_CACHE_KEY_FIELD,
|
|
186
|
+
OPENROUTER_SESSION_ID_FIELD,
|
|
187
|
+
OPENROUTER_SESSION_ID_HEADER_FIELD,
|
|
188
|
+
XAI_CONVERSATION_ID_HEADER_FIELD,
|
|
189
|
+
}
|
|
190
|
+
for field in effective_capability.emitted_fields
|
|
191
|
+
)
|
|
192
|
+
if prompt_cache_key_enabled and mode == "auto" and effective_key is None and needs_affinity_key:
|
|
193
|
+
effective_key = _auto_prompt_cache_key(
|
|
194
|
+
provider_key=provider_key,
|
|
195
|
+
protocol=protocol,
|
|
196
|
+
model=model,
|
|
197
|
+
namespace=prompt_cache_namespace,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
anthropic_enabled = False
|
|
201
|
+
if effective_capability.supports_cache_control:
|
|
202
|
+
anthropic_enabled = mode == "auto" or resolve_anthropic_prompt_cache_enabled(cfg)
|
|
203
|
+
gemini_explicit_enabled = (
|
|
204
|
+
effective_capability.supports_explicit_cached_content and mode == "auto"
|
|
205
|
+
)
|
|
206
|
+
warnings = effective_capability.warnings
|
|
207
|
+
gemini_ttl: str | None = None
|
|
208
|
+
if gemini_explicit_enabled:
|
|
209
|
+
gemini_ttl, gemini_ttl_fallback = _google_duration_from_retention(explicit_retention)
|
|
210
|
+
if gemini_ttl_fallback:
|
|
211
|
+
warnings = (
|
|
212
|
+
*warnings,
|
|
213
|
+
f"gemini_cached_content_ttl_fallback_{gemini_ttl}_for_unparseable_retention",
|
|
214
|
+
)
|
|
215
|
+
request_field_values = _policy_request_field_values(
|
|
216
|
+
emitted_fields=effective_capability.emitted_fields,
|
|
217
|
+
prompt_cache_key=(
|
|
218
|
+
effective_key
|
|
219
|
+
if effective_capability.supports_prompt_cache_key and effective_key
|
|
220
|
+
else None
|
|
221
|
+
),
|
|
222
|
+
prompt_cache_retention=(
|
|
223
|
+
explicit_retention
|
|
224
|
+
if effective_capability.supports_prompt_cache_retention and explicit_retention
|
|
225
|
+
else None
|
|
226
|
+
),
|
|
227
|
+
affinity_key=effective_key,
|
|
228
|
+
anthropic_cache_control_enabled=anthropic_enabled,
|
|
229
|
+
gemini_explicit_cached_content_enabled=gemini_explicit_enabled,
|
|
230
|
+
)
|
|
231
|
+
emitted_fields = _policy_emitted_fields(
|
|
232
|
+
request_field_values=request_field_values,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return ResolvedPromptCachePolicy(
|
|
236
|
+
prompt_cache_key=(
|
|
237
|
+
effective_key
|
|
238
|
+
if effective_capability.supports_prompt_cache_key and effective_key
|
|
239
|
+
else None
|
|
240
|
+
),
|
|
241
|
+
prompt_cache_retention=(
|
|
242
|
+
explicit_retention
|
|
243
|
+
if effective_capability.supports_prompt_cache_retention and explicit_retention
|
|
244
|
+
else None
|
|
245
|
+
),
|
|
246
|
+
anthropic_cache_control_enabled=anthropic_enabled,
|
|
247
|
+
anthropic_cache_control_ttl=ttl,
|
|
248
|
+
gemini_explicit_cached_content_enabled=gemini_explicit_enabled,
|
|
249
|
+
gemini_cached_content_ttl=gemini_ttl,
|
|
250
|
+
mode=mode,
|
|
251
|
+
strategy=strategy,
|
|
252
|
+
capability_source=effective_capability.source,
|
|
253
|
+
allowed_fields=effective_capability.emitted_fields,
|
|
254
|
+
emitted_fields=emitted_fields,
|
|
255
|
+
trusted_usage_fields=effective_capability.trusted_usage_fields,
|
|
256
|
+
usage_schema=effective_capability.usage_schema,
|
|
257
|
+
min_cacheable_tokens=effective_capability.min_cacheable_tokens,
|
|
258
|
+
request_field_values=request_field_values,
|
|
259
|
+
warnings=warnings,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def prompt_cache_policy_diagnostic_rows(
|
|
264
|
+
policy: ResolvedPromptCachePolicy,
|
|
265
|
+
) -> tuple[tuple[str, str], ...]:
|
|
266
|
+
allowed_fields = ", ".join(policy.allowed_fields) if policy.allowed_fields else "none"
|
|
267
|
+
emitted_fields = ", ".join(policy.emitted_fields) if policy.emitted_fields else "none"
|
|
268
|
+
usage_fields = ", ".join(policy.trusted_usage_fields) if policy.trusted_usage_fields else "none"
|
|
269
|
+
warnings = "; ".join(policy.warnings) if policy.warnings else "none"
|
|
270
|
+
return (
|
|
271
|
+
("cache_status", policy.status),
|
|
272
|
+
("cache_strategy", policy.strategy),
|
|
273
|
+
("cache_capability_source", policy.capability_source),
|
|
274
|
+
("cache_allowed_fields", allowed_fields),
|
|
275
|
+
("cache_emitted_fields", emitted_fields),
|
|
276
|
+
("cache_usage_fields", usage_fields),
|
|
277
|
+
("cache_usage_schema", policy.usage_schema),
|
|
278
|
+
("cache_warnings", warnings),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def merge_cache_policy_metadata(
|
|
283
|
+
base: Mapping[str, Any] | None,
|
|
284
|
+
active: Mapping[str, Any] | None,
|
|
285
|
+
) -> dict[str, Any] | None:
|
|
286
|
+
merged: dict[str, Any] = {}
|
|
287
|
+
if isinstance(base, Mapping):
|
|
288
|
+
merged.update(dict(base))
|
|
289
|
+
if isinstance(active, Mapping):
|
|
290
|
+
merged.update(dict(active))
|
|
291
|
+
if active.get("enabled") is True:
|
|
292
|
+
merged["status"] = "enabled"
|
|
293
|
+
merged["enabled"] = True
|
|
294
|
+
return merged or None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _policy_emitted_fields(
|
|
298
|
+
*,
|
|
299
|
+
request_field_values: tuple[tuple[str, str], ...],
|
|
300
|
+
) -> tuple[str, ...]:
|
|
301
|
+
return tuple(field for field, _value in request_field_values)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _policy_request_field_values(
|
|
305
|
+
*,
|
|
306
|
+
emitted_fields: tuple[str, ...],
|
|
307
|
+
prompt_cache_key: str | None,
|
|
308
|
+
prompt_cache_retention: str | None,
|
|
309
|
+
affinity_key: str | None,
|
|
310
|
+
anthropic_cache_control_enabled: bool,
|
|
311
|
+
gemini_explicit_cached_content_enabled: bool,
|
|
312
|
+
) -> tuple[tuple[str, str], ...]:
|
|
313
|
+
values: list[tuple[str, str]] = []
|
|
314
|
+
if prompt_cache_key and PROMPT_CACHE_KEY_FIELD in emitted_fields:
|
|
315
|
+
values.append((PROMPT_CACHE_KEY_FIELD, prompt_cache_key))
|
|
316
|
+
if prompt_cache_retention and PROMPT_CACHE_RETENTION_FIELD in emitted_fields:
|
|
317
|
+
values.append((PROMPT_CACHE_RETENTION_FIELD, prompt_cache_retention))
|
|
318
|
+
if affinity_key:
|
|
319
|
+
for field in (
|
|
320
|
+
OPENROUTER_SESSION_ID_FIELD,
|
|
321
|
+
OPENROUTER_SESSION_ID_HEADER_FIELD,
|
|
322
|
+
XAI_CONVERSATION_ID_HEADER_FIELD,
|
|
323
|
+
):
|
|
324
|
+
if field in emitted_fields:
|
|
325
|
+
values.append((field, affinity_key))
|
|
326
|
+
if anthropic_cache_control_enabled and CACHE_CONTROL_FIELD in emitted_fields:
|
|
327
|
+
values.append((CACHE_CONTROL_FIELD, "ephemeral"))
|
|
328
|
+
if gemini_explicit_cached_content_enabled and CACHED_CONTENT_FIELD in emitted_fields:
|
|
329
|
+
values.append((CACHED_CONTENT_FIELD, "enabled"))
|
|
330
|
+
return tuple(values)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _clean_optional(value: str | None) -> str | None:
|
|
334
|
+
cleaned = str(value or "").strip()
|
|
335
|
+
return cleaned or None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _normalize_workspace_root(root: str | os.PathLike[str]) -> str:
|
|
339
|
+
try:
|
|
340
|
+
return os.fspath(Path(root).expanduser().resolve(strict=False))
|
|
341
|
+
except (OSError, RuntimeError, ValueError):
|
|
342
|
+
return os.fspath(root)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _auto_prompt_cache_key(
|
|
346
|
+
*,
|
|
347
|
+
provider_key: str,
|
|
348
|
+
protocol: str,
|
|
349
|
+
model: str,
|
|
350
|
+
namespace: str | None,
|
|
351
|
+
) -> str | None:
|
|
352
|
+
normalized_namespace = str(namespace or "").strip()
|
|
353
|
+
if not normalized_namespace:
|
|
354
|
+
return None
|
|
355
|
+
provider_label = _safe_cache_label(provider_key or "provider")
|
|
356
|
+
payload = {
|
|
357
|
+
"version": 1,
|
|
358
|
+
"provider": str(provider_key or "").strip().lower(),
|
|
359
|
+
"protocol": str(protocol or "").strip().lower(),
|
|
360
|
+
"model": str(model or "").strip().lower(),
|
|
361
|
+
"namespace": normalized_namespace,
|
|
362
|
+
}
|
|
363
|
+
digest = hashlib.blake2s(
|
|
364
|
+
json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8"),
|
|
365
|
+
digest_size=10,
|
|
366
|
+
).hexdigest()
|
|
367
|
+
return f"alysis:{provider_label}:{digest}"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _safe_cache_label(value: str) -> str:
|
|
371
|
+
normalized = _CACHE_KEY_LABEL_RE.sub("-", str(value or "").strip().lower()).strip("-")
|
|
372
|
+
return normalized[:32] or "provider"
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _google_duration_from_retention(value: str | None) -> tuple[str, bool]:
|
|
376
|
+
cleaned = _clean_optional(value)
|
|
377
|
+
if cleaned is None:
|
|
378
|
+
return "3600s", False
|
|
379
|
+
normalized = cleaned.lower()
|
|
380
|
+
if normalized.endswith("s") and normalized[:-1].isdigit():
|
|
381
|
+
return normalized, False
|
|
382
|
+
if normalized.endswith("m") and normalized[:-1].isdigit():
|
|
383
|
+
return f"{int(normalized[:-1]) * 60}s", False
|
|
384
|
+
if normalized.endswith("h") and normalized[:-1].isdigit():
|
|
385
|
+
return f"{int(normalized[:-1]) * 3600}s", False
|
|
386
|
+
# Retention values from other providers (e.g. "in-memory") are not valid
|
|
387
|
+
# Google durations and would 400 every cachedContents create.
|
|
388
|
+
return "3600s", True
|