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,525 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import errno
|
|
4
|
+
import logging
|
|
5
|
+
import random
|
|
6
|
+
import socket
|
|
7
|
+
import ssl
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Callable, Mapping
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import TypeVar
|
|
13
|
+
from urllib.parse import urlsplit
|
|
14
|
+
|
|
15
|
+
from ..failure_category import is_provider_throttling_error, provider_unavailable_retry_reason
|
|
16
|
+
from ..provider_url import known_provider_key_from_base_url
|
|
17
|
+
from .types import LLMStreamNoProgressError
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
DEFAULT_PROVIDER_CONCURRENCY_CAPS: dict[str, int] = {"qwen": 4}
|
|
22
|
+
DEFAULT_PROVIDER_RETRY_MAX_RETRIES = 1
|
|
23
|
+
DEFAULT_PROVIDER_RETRY_BASE_DELAY_SECONDS = 10.0
|
|
24
|
+
DEFAULT_PROVIDER_RETRY_MAX_DELAY_SECONDS = 120.0
|
|
25
|
+
|
|
26
|
+
_LOGGER = logging.getLogger(__name__)
|
|
27
|
+
_JITTER_RATIO = 0.25
|
|
28
|
+
_QWEN_CANONICAL_KEY = "qwen"
|
|
29
|
+
_QWEN_PROVIDER_ALIASES = frozenset(
|
|
30
|
+
{
|
|
31
|
+
"aliyun",
|
|
32
|
+
"aliyuncs",
|
|
33
|
+
"dashscope",
|
|
34
|
+
"dashscopechat",
|
|
35
|
+
"qwen",
|
|
36
|
+
"qwen2",
|
|
37
|
+
"qwen25",
|
|
38
|
+
"qwen3",
|
|
39
|
+
"qwen35",
|
|
40
|
+
"qwenmax",
|
|
41
|
+
"tongyi",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
_GENERIC_HOST_PARTS = frozenset(
|
|
45
|
+
{
|
|
46
|
+
"api",
|
|
47
|
+
"ai",
|
|
48
|
+
"chat",
|
|
49
|
+
"coding",
|
|
50
|
+
"compatible",
|
|
51
|
+
"com",
|
|
52
|
+
"cn",
|
|
53
|
+
"intl",
|
|
54
|
+
"mode",
|
|
55
|
+
"v1",
|
|
56
|
+
"v1beta",
|
|
57
|
+
"www",
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
_KNOWN_TRANSPORT_PROVIDER_KEYS = frozenset(
|
|
61
|
+
{
|
|
62
|
+
"qwen",
|
|
63
|
+
"openrouter",
|
|
64
|
+
"openai",
|
|
65
|
+
"azure",
|
|
66
|
+
"deepseek",
|
|
67
|
+
"gemini",
|
|
68
|
+
"mistral",
|
|
69
|
+
"nvidia",
|
|
70
|
+
"zai_coding_plan",
|
|
71
|
+
"xai",
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
_SEMAPHORE_LOCK = threading.Lock()
|
|
75
|
+
_SEMAPHORES: dict[tuple[str, int], threading.Semaphore] = {}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class ProviderRetrySettings:
|
|
80
|
+
max_retries: int = DEFAULT_PROVIDER_RETRY_MAX_RETRIES
|
|
81
|
+
base_delay_seconds: float = DEFAULT_PROVIDER_RETRY_BASE_DELAY_SECONDS
|
|
82
|
+
max_delay_seconds: float = DEFAULT_PROVIDER_RETRY_MAX_DELAY_SECONDS
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def canonical_provider_key(provider_key: str | None) -> str | None:
|
|
86
|
+
normalized = _normalize_provider_key(provider_key)
|
|
87
|
+
if not normalized:
|
|
88
|
+
return None
|
|
89
|
+
tokens = _provider_tokens(normalized)
|
|
90
|
+
if tokens & _QWEN_PROVIDER_ALIASES:
|
|
91
|
+
return _QWEN_CANONICAL_KEY
|
|
92
|
+
return normalized
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def best_effort_provider_key(*, base_url: str | None, model: str | None) -> str | None:
|
|
96
|
+
url_key = _provider_key_from_base_url(base_url)
|
|
97
|
+
if url_key in _KNOWN_TRANSPORT_PROVIDER_KEYS:
|
|
98
|
+
return url_key
|
|
99
|
+
model_key = _provider_key_from_model(model)
|
|
100
|
+
if model_key is not None:
|
|
101
|
+
return model_key
|
|
102
|
+
return url_key
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def resolve_provider_retry_settings(cfg: object | None) -> ProviderRetrySettings:
|
|
106
|
+
return ProviderRetrySettings(
|
|
107
|
+
max_retries=_coerce_non_negative_int(
|
|
108
|
+
getattr(cfg, "provider_retry_max_retries", DEFAULT_PROVIDER_RETRY_MAX_RETRIES),
|
|
109
|
+
default=DEFAULT_PROVIDER_RETRY_MAX_RETRIES,
|
|
110
|
+
),
|
|
111
|
+
base_delay_seconds=_coerce_positive_float(
|
|
112
|
+
getattr(
|
|
113
|
+
cfg,
|
|
114
|
+
"provider_retry_base_delay_seconds",
|
|
115
|
+
DEFAULT_PROVIDER_RETRY_BASE_DELAY_SECONDS,
|
|
116
|
+
),
|
|
117
|
+
default=DEFAULT_PROVIDER_RETRY_BASE_DELAY_SECONDS,
|
|
118
|
+
),
|
|
119
|
+
max_delay_seconds=_coerce_positive_float(
|
|
120
|
+
getattr(
|
|
121
|
+
cfg,
|
|
122
|
+
"provider_retry_max_delay_seconds",
|
|
123
|
+
DEFAULT_PROVIDER_RETRY_MAX_DELAY_SECONDS,
|
|
124
|
+
),
|
|
125
|
+
default=DEFAULT_PROVIDER_RETRY_MAX_DELAY_SECONDS,
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def resolve_provider_concurrency_cap(
|
|
131
|
+
provider_concurrency_caps: Mapping[str, int | None] | None,
|
|
132
|
+
provider_key: str | None,
|
|
133
|
+
) -> int | None:
|
|
134
|
+
canonical_key = canonical_provider_key(provider_key)
|
|
135
|
+
if canonical_key is None:
|
|
136
|
+
return None
|
|
137
|
+
caps = provider_concurrency_caps or DEFAULT_PROVIDER_CONCURRENCY_CAPS
|
|
138
|
+
exact_found, exact_cap = _lookup_cap(caps, canonical_key)
|
|
139
|
+
if exact_found:
|
|
140
|
+
return exact_cap
|
|
141
|
+
|
|
142
|
+
alias_caps: list[int] = []
|
|
143
|
+
for raw_key, raw_cap in caps.items():
|
|
144
|
+
if canonical_provider_key(str(raw_key)) != canonical_key:
|
|
145
|
+
continue
|
|
146
|
+
cap = _coerce_cap(raw_cap)
|
|
147
|
+
if cap is not None:
|
|
148
|
+
alias_caps.append(cap)
|
|
149
|
+
if alias_caps:
|
|
150
|
+
return min(alias_caps)
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def run_provider_limited_call(
|
|
155
|
+
*,
|
|
156
|
+
call: Callable[[], T],
|
|
157
|
+
provider_key: str | None,
|
|
158
|
+
provider_concurrency_caps: Mapping[str, int | None] | None = None,
|
|
159
|
+
retry_settings: ProviderRetrySettings | None = None,
|
|
160
|
+
operation: str,
|
|
161
|
+
sleep_fn: Callable[[float], None] | None = None,
|
|
162
|
+
random_fn: Callable[[], float] | None = None,
|
|
163
|
+
on_retry: Callable[[int, str, float], None] | None = None,
|
|
164
|
+
on_retry_event: Callable[[dict[str, object]], None] | None = None,
|
|
165
|
+
retry_deadline_allows: Callable[[float], bool] | None = None,
|
|
166
|
+
retry_wall_clock_cap_seconds: float | None = None,
|
|
167
|
+
clock_fn: Callable[[], float] | None = None,
|
|
168
|
+
) -> T:
|
|
169
|
+
settings = retry_settings or ProviderRetrySettings()
|
|
170
|
+
canonical_key = canonical_provider_key(provider_key)
|
|
171
|
+
cap = resolve_provider_concurrency_cap(provider_concurrency_caps, canonical_key)
|
|
172
|
+
semaphore = _semaphore_for(canonical_key, cap) if canonical_key and cap else None
|
|
173
|
+
sleep = sleep_fn or time.sleep
|
|
174
|
+
jitter = random_fn or random.random
|
|
175
|
+
clock = clock_fn or time.monotonic
|
|
176
|
+
retry_started_at = clock()
|
|
177
|
+
retries_used = 0
|
|
178
|
+
|
|
179
|
+
while True:
|
|
180
|
+
if semaphore is not None:
|
|
181
|
+
semaphore.acquire()
|
|
182
|
+
try:
|
|
183
|
+
return call()
|
|
184
|
+
except Exception as exc:
|
|
185
|
+
retry_reason = _provider_retry_reason(exc)
|
|
186
|
+
if (
|
|
187
|
+
retry_reason is None
|
|
188
|
+
or _is_provider_call_non_retryable(exc)
|
|
189
|
+
or retries_used >= settings.max_retries
|
|
190
|
+
):
|
|
191
|
+
raise
|
|
192
|
+
wait_seconds = _retry_delay_seconds(settings, retries_used, jitter)
|
|
193
|
+
if _retry_wall_clock_cap_blocks(
|
|
194
|
+
retry_reason=retry_reason,
|
|
195
|
+
retry_started_at=retry_started_at,
|
|
196
|
+
wait_seconds=wait_seconds,
|
|
197
|
+
cap_seconds=retry_wall_clock_cap_seconds,
|
|
198
|
+
clock=clock,
|
|
199
|
+
):
|
|
200
|
+
_LOGGER.info(
|
|
201
|
+
"provider_retry_wall_clock_cap_blocked",
|
|
202
|
+
extra={
|
|
203
|
+
"operation": operation,
|
|
204
|
+
"provider_key": canonical_key,
|
|
205
|
+
"retry_attempt": retries_used + 1,
|
|
206
|
+
"wait_seconds": wait_seconds,
|
|
207
|
+
"retry_reason": retry_reason,
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
raise
|
|
211
|
+
if retry_deadline_allows is not None and not retry_deadline_allows(wait_seconds):
|
|
212
|
+
_LOGGER.info(
|
|
213
|
+
"provider_retry_deadline_blocked",
|
|
214
|
+
extra={
|
|
215
|
+
"operation": operation,
|
|
216
|
+
"provider_key": canonical_key,
|
|
217
|
+
"retry_attempt": retries_used + 1,
|
|
218
|
+
"wait_seconds": wait_seconds,
|
|
219
|
+
"retry_reason": retry_reason,
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
raise
|
|
223
|
+
if on_retry is not None:
|
|
224
|
+
try:
|
|
225
|
+
on_retry(retries_used + 1, retry_reason, wait_seconds)
|
|
226
|
+
except Exception: # noqa: BLE001 - observers must not change retry behavior.
|
|
227
|
+
_LOGGER.debug("provider_retry_observer_failed", exc_info=True)
|
|
228
|
+
if on_retry_event is not None:
|
|
229
|
+
try:
|
|
230
|
+
on_retry_event(
|
|
231
|
+
{
|
|
232
|
+
"provider": canonical_key or "unknown",
|
|
233
|
+
"attempt": retries_used + 2,
|
|
234
|
+
"reason": retry_reason,
|
|
235
|
+
"elapsed_ms": max(
|
|
236
|
+
0,
|
|
237
|
+
int((clock() - retry_started_at) * 1000),
|
|
238
|
+
),
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
except Exception: # noqa: BLE001 - observers cannot change retries.
|
|
242
|
+
_LOGGER.debug("provider_retry_event_observer_failed", exc_info=True)
|
|
243
|
+
_LOGGER.info(
|
|
244
|
+
f"{retry_reason}, retrying",
|
|
245
|
+
extra={
|
|
246
|
+
"operation": operation,
|
|
247
|
+
"provider_key": canonical_key,
|
|
248
|
+
"retry_attempt": retries_used + 1,
|
|
249
|
+
"wait_seconds": wait_seconds,
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
finally:
|
|
253
|
+
if semaphore is not None:
|
|
254
|
+
semaphore.release()
|
|
255
|
+
retries_used += 1
|
|
256
|
+
sleep(wait_seconds)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _retry_wall_clock_cap_blocks(
|
|
260
|
+
*,
|
|
261
|
+
retry_reason: str,
|
|
262
|
+
retry_started_at: float,
|
|
263
|
+
wait_seconds: float,
|
|
264
|
+
cap_seconds: float | None,
|
|
265
|
+
clock: Callable[[], float],
|
|
266
|
+
) -> bool:
|
|
267
|
+
if (
|
|
268
|
+
retry_reason not in {"provider_unavailable", "provider_stream_truncated"}
|
|
269
|
+
or cap_seconds is None
|
|
270
|
+
):
|
|
271
|
+
return False
|
|
272
|
+
try:
|
|
273
|
+
cap = float(cap_seconds)
|
|
274
|
+
except (TypeError, ValueError):
|
|
275
|
+
return False
|
|
276
|
+
if cap <= 0:
|
|
277
|
+
return True
|
|
278
|
+
elapsed = max(0.0, float(clock()) - float(retry_started_at))
|
|
279
|
+
return elapsed + max(0.0, float(wait_seconds)) > cap
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _provider_retry_reason(exc: Exception) -> str | None:
|
|
283
|
+
if isinstance(exc, LLMStreamNoProgressError):
|
|
284
|
+
return "provider_no_progress"
|
|
285
|
+
if is_provider_throttling_error(exc):
|
|
286
|
+
return "provider_throttled"
|
|
287
|
+
transport_reason = _transient_transport_retry_reason(exc)
|
|
288
|
+
if transport_reason is not None:
|
|
289
|
+
return transport_reason
|
|
290
|
+
return provider_unavailable_retry_reason(exc)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
_TRANSIENT_TRANSPORT_ERRNOS = {
|
|
294
|
+
value
|
|
295
|
+
for value in (
|
|
296
|
+
getattr(socket, "EAI_AGAIN", None),
|
|
297
|
+
getattr(socket, "EAI_FAIL", None),
|
|
298
|
+
getattr(socket, "EAI_NONAME", None),
|
|
299
|
+
getattr(errno, "ECONNABORTED", None),
|
|
300
|
+
getattr(errno, "ECONNREFUSED", None),
|
|
301
|
+
getattr(errno, "ECONNRESET", None),
|
|
302
|
+
getattr(errno, "EHOSTUNREACH", None),
|
|
303
|
+
getattr(errno, "ENETUNREACH", None),
|
|
304
|
+
getattr(errno, "ETIMEDOUT", None),
|
|
305
|
+
)
|
|
306
|
+
if value is not None
|
|
307
|
+
}
|
|
308
|
+
_TRANSIENT_TRANSPORT_MARKERS = (
|
|
309
|
+
"connection aborted",
|
|
310
|
+
"connection refused",
|
|
311
|
+
"connection reset",
|
|
312
|
+
"getaddrinfo failed",
|
|
313
|
+
"handshake operation timed out",
|
|
314
|
+
"name or service not known",
|
|
315
|
+
"name resolution",
|
|
316
|
+
"network is unreachable",
|
|
317
|
+
"nodename nor servname",
|
|
318
|
+
"temporary failure in name resolution",
|
|
319
|
+
"timed out",
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _transient_transport_retry_reason(exc: BaseException) -> str | None:
|
|
324
|
+
for item in _iter_exception_chain(exc):
|
|
325
|
+
if isinstance(item, (socket.timeout, TimeoutError)):
|
|
326
|
+
return "provider_unavailable"
|
|
327
|
+
if isinstance(item, ssl.SSLError) and _has_transient_transport_marker(item):
|
|
328
|
+
return "provider_unavailable"
|
|
329
|
+
if isinstance(
|
|
330
|
+
item,
|
|
331
|
+
(ConnectionAbortedError, ConnectionRefusedError, ConnectionResetError),
|
|
332
|
+
):
|
|
333
|
+
return "provider_unavailable"
|
|
334
|
+
if isinstance(item, OSError):
|
|
335
|
+
if getattr(item, "errno", None) in _TRANSIENT_TRANSPORT_ERRNOS:
|
|
336
|
+
return "provider_unavailable"
|
|
337
|
+
if _has_transient_transport_marker(item):
|
|
338
|
+
return "provider_unavailable"
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _iter_exception_chain(exc: BaseException) -> list[BaseException]:
|
|
343
|
+
chain: list[BaseException] = []
|
|
344
|
+
seen: set[int] = set()
|
|
345
|
+
current: BaseException | None = exc
|
|
346
|
+
while current is not None and id(current) not in seen:
|
|
347
|
+
seen.add(id(current))
|
|
348
|
+
chain.append(current)
|
|
349
|
+
cause = current.__cause__
|
|
350
|
+
context = current.__context__
|
|
351
|
+
current = cause if cause is not None else context
|
|
352
|
+
return chain
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _has_transient_transport_marker(exc: BaseException) -> bool:
|
|
356
|
+
message = str(exc or "").casefold()
|
|
357
|
+
return any(marker in message for marker in _TRANSIENT_TRANSPORT_MARKERS)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
PROVIDER_NON_RETRYABLE_ATTR = "_provider_call_non_retryable"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def mark_provider_call_non_retryable(exc: BaseException) -> None:
|
|
364
|
+
"""Tag an exception so :func:`run_provider_limited_call` will not retry it.
|
|
365
|
+
|
|
366
|
+
Some failures match the retryable heuristics by message (e.g. an httpx read
|
|
367
|
+
timeout contains "timed out") yet an immediate retry with the same budget is
|
|
368
|
+
pointless — it will fail the same way and only double the dead air. A caller
|
|
369
|
+
that knows a failure is not worth retrying marks it here so the retry loop
|
|
370
|
+
skips it while still retrying genuinely transient failures.
|
|
371
|
+
"""
|
|
372
|
+
try:
|
|
373
|
+
setattr(exc, PROVIDER_NON_RETRYABLE_ATTR, True)
|
|
374
|
+
except Exception: # noqa: BLE001 - never let tagging crash the caller
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _is_provider_call_non_retryable(exc: BaseException) -> bool:
|
|
379
|
+
return bool(getattr(exc, PROVIDER_NON_RETRYABLE_ATTR, False))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def single_retry_is_worthwhile(exc: BaseException) -> bool:
|
|
383
|
+
"""Whether one more attempt at the same call could plausibly succeed.
|
|
384
|
+
|
|
385
|
+
Only transport-class failures qualify: timeouts, resets, refused
|
|
386
|
+
connections, and transient SSL/OS errors. A rejection — bad credentials,
|
|
387
|
+
exhausted quota, a malformed request — fails identically on the second
|
|
388
|
+
attempt and only spends another round trip. Failures the provider layer has
|
|
389
|
+
already given up on are never retried again on top of that.
|
|
390
|
+
|
|
391
|
+
Exposed for callers outside this module that run their own bounded retry,
|
|
392
|
+
so the transient classification stays defined in exactly one place.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
if _is_provider_call_non_retryable(exc):
|
|
396
|
+
return False
|
|
397
|
+
return _transient_transport_retry_reason(exc) is not None
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def reset_provider_limit_state_for_tests() -> None:
|
|
401
|
+
with _SEMAPHORE_LOCK:
|
|
402
|
+
_SEMAPHORES.clear()
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def qwen_provider_aliases() -> tuple[str, ...]:
|
|
406
|
+
return tuple(sorted(_QWEN_PROVIDER_ALIASES))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _semaphore_for(provider_key: str, cap: int) -> threading.Semaphore:
|
|
410
|
+
key = (provider_key, cap)
|
|
411
|
+
with _SEMAPHORE_LOCK:
|
|
412
|
+
semaphore = _SEMAPHORES.get(key)
|
|
413
|
+
if semaphore is None:
|
|
414
|
+
semaphore = threading.Semaphore(cap)
|
|
415
|
+
_SEMAPHORES[key] = semaphore
|
|
416
|
+
return semaphore
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _retry_delay_seconds(
|
|
420
|
+
settings: ProviderRetrySettings,
|
|
421
|
+
retry_index: int,
|
|
422
|
+
random_fn: Callable[[], float],
|
|
423
|
+
) -> float:
|
|
424
|
+
base = max(settings.base_delay_seconds, 0.0)
|
|
425
|
+
max_delay = max(settings.max_delay_seconds, base)
|
|
426
|
+
raw_delay = min(max_delay, base * (2**retry_index))
|
|
427
|
+
jitter_sample = min(max(float(random_fn()), 0.0), 1.0)
|
|
428
|
+
jitter_offset = (jitter_sample - 0.5) * 2.0 * _JITTER_RATIO * raw_delay
|
|
429
|
+
return max(0.0, min(max_delay, raw_delay + jitter_offset))
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _lookup_cap(
|
|
433
|
+
caps: Mapping[str, int | None],
|
|
434
|
+
key: str,
|
|
435
|
+
) -> tuple[bool, int | None]:
|
|
436
|
+
for raw_key, raw_cap in caps.items():
|
|
437
|
+
if _normalize_provider_key(str(raw_key)) == key:
|
|
438
|
+
return True, _coerce_cap(raw_cap)
|
|
439
|
+
return False, None
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _coerce_cap(raw: int | None) -> int | None:
|
|
443
|
+
try:
|
|
444
|
+
cap = int(raw if raw is not None else 0)
|
|
445
|
+
except (TypeError, ValueError):
|
|
446
|
+
return None
|
|
447
|
+
return cap if cap > 0 else None
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _coerce_non_negative_int(raw: object, *, default: int) -> int:
|
|
451
|
+
try:
|
|
452
|
+
value = int(raw)
|
|
453
|
+
except (TypeError, ValueError):
|
|
454
|
+
return default
|
|
455
|
+
return value if value >= 0 else default
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _coerce_positive_float(raw: object, *, default: float) -> float:
|
|
459
|
+
try:
|
|
460
|
+
value = float(raw)
|
|
461
|
+
except (TypeError, ValueError):
|
|
462
|
+
return default
|
|
463
|
+
return value if value > 0 else default
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _provider_key_from_model(model: str | None) -> str | None:
|
|
467
|
+
raw_model = str(model or "").strip()
|
|
468
|
+
normalized = _normalize_provider_key(raw_model)
|
|
469
|
+
if not normalized:
|
|
470
|
+
return None
|
|
471
|
+
canonical = canonical_provider_key(normalized)
|
|
472
|
+
if canonical is not None and canonical != normalized:
|
|
473
|
+
return canonical
|
|
474
|
+
if "/" in raw_model:
|
|
475
|
+
prefix = raw_model.split("/", 1)[0]
|
|
476
|
+
return canonical_provider_key(prefix) or _normalize_provider_key(prefix)
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _provider_key_from_base_url(base_url: str | None) -> str | None:
|
|
481
|
+
raw = str(base_url or "").strip()
|
|
482
|
+
if not raw:
|
|
483
|
+
return None
|
|
484
|
+
try:
|
|
485
|
+
parts = urlsplit(raw)
|
|
486
|
+
hostname = (parts.hostname or "").rstrip(".").casefold()
|
|
487
|
+
except ValueError:
|
|
488
|
+
return None
|
|
489
|
+
if not hostname:
|
|
490
|
+
return None
|
|
491
|
+
known_provider = known_provider_key_from_base_url(raw)
|
|
492
|
+
if known_provider:
|
|
493
|
+
return known_provider
|
|
494
|
+
canonical = canonical_provider_key(hostname)
|
|
495
|
+
if canonical is not None and canonical != hostname:
|
|
496
|
+
return canonical
|
|
497
|
+
host_parts = [part for part in hostname.replace("-", ".").split(".") if part]
|
|
498
|
+
for part in host_parts:
|
|
499
|
+
canonical_part = canonical_provider_key(part)
|
|
500
|
+
if canonical_part is not None and canonical_part != part:
|
|
501
|
+
return canonical_part
|
|
502
|
+
for part in reversed(host_parts):
|
|
503
|
+
if part not in _GENERIC_HOST_PARTS:
|
|
504
|
+
return canonical_provider_key(part) or part
|
|
505
|
+
return hostname
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _normalize_provider_key(provider_key: str | None) -> str | None:
|
|
509
|
+
normalized = str(provider_key or "").strip().casefold()
|
|
510
|
+
if not normalized:
|
|
511
|
+
return None
|
|
512
|
+
return "".join(char if char.isalnum() else "_" for char in normalized).strip("_") or None
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _provider_tokens(provider_key: str) -> set[str]:
|
|
516
|
+
ordered_tokens = [
|
|
517
|
+
token
|
|
518
|
+
for token in provider_key.replace("-", "_").replace("/", "_").replace(".", "_").split("_")
|
|
519
|
+
if token
|
|
520
|
+
]
|
|
521
|
+
tokens = set(ordered_tokens)
|
|
522
|
+
compact = "".join(ordered_tokens)
|
|
523
|
+
if compact:
|
|
524
|
+
tokens.add(compact)
|
|
525
|
+
return tokens
|