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
alysis_code/profiles.py
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from urllib.parse import urlsplit
|
|
7
|
+
|
|
8
|
+
from .llm.cache_capabilities import CacheCapabilitySpec
|
|
9
|
+
from .llm.metadata import canonicalize_extra_headers
|
|
10
|
+
from .llm.protocols import (
|
|
11
|
+
OPENAI_COMPAT_PROTOCOL,
|
|
12
|
+
SUPPORTED_LLM_PROTOCOLS,
|
|
13
|
+
normalize_reasoning_trace_adapter,
|
|
14
|
+
validate_reasoning_trace_adapter_for_protocol,
|
|
15
|
+
)
|
|
16
|
+
from .web_search_adapters import normalize_web_search_adapter
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .config import AppConfig
|
|
20
|
+
|
|
21
|
+
SUPPORTED_PROTOCOLS: frozenset[str] = SUPPORTED_LLM_PROTOCOLS
|
|
22
|
+
DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"
|
|
23
|
+
_PROFILE_NAME_RE = re.compile(r"^[a-z0-9_-]+$")
|
|
24
|
+
_REASONING_EFFORT_RE = re.compile(r"^[a-z][a-z0-9_-]{0,31}$")
|
|
25
|
+
_UNSET = object()
|
|
26
|
+
SUBSCRIPTION_SELECTION_REQUIRED_KEY = "subscription_model_selection_required"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class ProfileSpec:
|
|
31
|
+
name: str
|
|
32
|
+
protocol: str = OPENAI_COMPAT_PROTOCOL
|
|
33
|
+
base_url: str = ""
|
|
34
|
+
api_key_env: str | None = None
|
|
35
|
+
auth_provider: str | None = None
|
|
36
|
+
extra_headers: dict[str, str] = field(default_factory=dict)
|
|
37
|
+
default_model: str = ""
|
|
38
|
+
reasoning_effort: str | None = None
|
|
39
|
+
reasoning_trace_adapter: str = "auto"
|
|
40
|
+
web_search_adapter: str = "auto"
|
|
41
|
+
web_search_model: str = ""
|
|
42
|
+
notes: str = ""
|
|
43
|
+
cache_capability: CacheCapabilitySpec | None = None
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
_validate_profile_name(self.name)
|
|
47
|
+
_validate_protocol(self.protocol)
|
|
48
|
+
_validate_web_search_adapter(self.web_search_adapter)
|
|
49
|
+
normalized_reasoning_trace_adapter = validate_reasoning_trace_adapter_for_protocol(
|
|
50
|
+
protocol=self.protocol,
|
|
51
|
+
adapter=self.reasoning_trace_adapter,
|
|
52
|
+
)
|
|
53
|
+
object.__setattr__(
|
|
54
|
+
self,
|
|
55
|
+
"reasoning_trace_adapter",
|
|
56
|
+
normalized_reasoning_trace_adapter,
|
|
57
|
+
)
|
|
58
|
+
if self.base_url:
|
|
59
|
+
_validate_base_url(self.base_url)
|
|
60
|
+
if (
|
|
61
|
+
self.reasoning_effort is not None
|
|
62
|
+
and _REASONING_EFFORT_RE.fullmatch(self.reasoning_effort) is None
|
|
63
|
+
):
|
|
64
|
+
_raise_config_error("Profile reasoning_effort is not a valid provider effort id.")
|
|
65
|
+
if self.cache_capability is not None and not isinstance(
|
|
66
|
+
self.cache_capability, CacheCapabilitySpec
|
|
67
|
+
):
|
|
68
|
+
_raise_config_error("Profile cache_capability must be a JSON object.")
|
|
69
|
+
try:
|
|
70
|
+
normalized_headers = canonicalize_extra_headers(self.extra_headers)
|
|
71
|
+
except ValueError as exc:
|
|
72
|
+
_raise_config_error(str(exc))
|
|
73
|
+
object.__setattr__(self, "extra_headers", normalized_headers)
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> dict[str, Any]:
|
|
76
|
+
data: dict[str, Any] = {
|
|
77
|
+
"protocol": self.protocol,
|
|
78
|
+
"base_url": self.base_url,
|
|
79
|
+
"extra_headers": dict(sorted(self.extra_headers.items())),
|
|
80
|
+
"default_model": self.default_model,
|
|
81
|
+
"web_search_adapter": normalize_web_search_adapter(self.web_search_adapter),
|
|
82
|
+
"web_search_model": self.web_search_model,
|
|
83
|
+
"notes": self.notes,
|
|
84
|
+
}
|
|
85
|
+
if self.api_key_env:
|
|
86
|
+
data["api_key_env"] = self.api_key_env
|
|
87
|
+
if self.auth_provider:
|
|
88
|
+
data["auth_provider"] = self.auth_provider
|
|
89
|
+
if self.reasoning_effort:
|
|
90
|
+
data["reasoning_effort"] = self.reasoning_effort
|
|
91
|
+
reasoning_trace_adapter = normalize_reasoning_trace_adapter(self.reasoning_trace_adapter)
|
|
92
|
+
if reasoning_trace_adapter != "auto":
|
|
93
|
+
data["reasoning_trace_adapter"] = reasoning_trace_adapter
|
|
94
|
+
if self.cache_capability is not None and self.cache_capability.has_values():
|
|
95
|
+
data["cache_capability"] = self.cache_capability.to_dict()
|
|
96
|
+
return data
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_dict(cls, name: str, data: dict[str, Any]) -> ProfileSpec:
|
|
100
|
+
if not isinstance(data, dict):
|
|
101
|
+
_raise_config_error(f"Profile {name!r} must be a JSON object.")
|
|
102
|
+
cache_capability = _coerce_cache_capability(
|
|
103
|
+
data.get("cache_capability"),
|
|
104
|
+
source=f"Profile {name!r}",
|
|
105
|
+
)
|
|
106
|
+
return cls(
|
|
107
|
+
name=name,
|
|
108
|
+
protocol=str(data.get("protocol") or OPENAI_COMPAT_PROTOCOL).strip(),
|
|
109
|
+
base_url=str(data.get("base_url") or "").strip(),
|
|
110
|
+
api_key_env=_optional_string(data.get("api_key_env")),
|
|
111
|
+
auth_provider=_optional_string(data.get("auth_provider")),
|
|
112
|
+
extra_headers=_coerce_headers(data.get("extra_headers")),
|
|
113
|
+
default_model=str(data.get("default_model") or "").strip(),
|
|
114
|
+
reasoning_effort=_optional_string(data.get("reasoning_effort")),
|
|
115
|
+
reasoning_trace_adapter=normalize_reasoning_trace_adapter(
|
|
116
|
+
_optional_string(data.get("reasoning_trace_adapter"))
|
|
117
|
+
),
|
|
118
|
+
web_search_adapter=str(data.get("web_search_adapter") or "auto").strip(),
|
|
119
|
+
web_search_model=str(data.get("web_search_model") or "").strip(),
|
|
120
|
+
notes=str(data.get("notes") or ""),
|
|
121
|
+
cache_capability=cache_capability,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def list_profiles(cfg: AppConfig) -> list[ProfileSpec]:
|
|
126
|
+
migrate_legacy_to_profiles(cfg)
|
|
127
|
+
profiles = _profile_dict(cfg)
|
|
128
|
+
return [ProfileSpec.from_dict(name, profiles[name]) for name in sorted(profiles)]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_profile(cfg: AppConfig, name: str) -> ProfileSpec | None:
|
|
132
|
+
migrate_legacy_to_profiles(cfg)
|
|
133
|
+
profile_name = _normalize_profile_name(name)
|
|
134
|
+
profiles = _profile_dict(cfg)
|
|
135
|
+
data = profiles.get(profile_name)
|
|
136
|
+
if not isinstance(data, dict):
|
|
137
|
+
return None
|
|
138
|
+
return ProfileSpec.from_dict(profile_name, data)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_active_profile(cfg: AppConfig) -> ProfileSpec:
|
|
142
|
+
migrate_legacy_to_profiles(cfg)
|
|
143
|
+
active = str((cfg.extra_fields or {}).get("active_profile") or "").strip()
|
|
144
|
+
if not active:
|
|
145
|
+
_raise_config_error("No active profile configured.")
|
|
146
|
+
profile = get_profile(cfg, active)
|
|
147
|
+
if profile is None:
|
|
148
|
+
_raise_config_error(f"Active profile not found: {active}")
|
|
149
|
+
return profile
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def active_subscription_selection_ready(cfg: AppConfig) -> bool:
|
|
153
|
+
"""Whether the active auth profile has an explicit /config model+effort choice."""
|
|
154
|
+
|
|
155
|
+
profile = get_active_profile(cfg)
|
|
156
|
+
if not profile.auth_provider:
|
|
157
|
+
return True
|
|
158
|
+
marker = (cfg.extra_fields or {}).get(SUBSCRIPTION_SELECTION_REQUIRED_KEY)
|
|
159
|
+
if marker is True or str(marker or "").strip() == profile.auth_provider:
|
|
160
|
+
return False
|
|
161
|
+
return bool(profile.default_model and profile.reasoning_effort is not None)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def subscription_selection_supported(profile: ProfileSpec, models: Any) -> bool:
|
|
165
|
+
"""Check an explicit profile selection against a provider model catalog."""
|
|
166
|
+
|
|
167
|
+
if not profile.default_model or profile.reasoning_effort is None:
|
|
168
|
+
return False
|
|
169
|
+
selected = next(
|
|
170
|
+
(item for item in models if str(getattr(item, "id", "")) == profile.default_model),
|
|
171
|
+
None,
|
|
172
|
+
)
|
|
173
|
+
if selected is None:
|
|
174
|
+
return False
|
|
175
|
+
if profile.reasoning_effort == "auto":
|
|
176
|
+
return True
|
|
177
|
+
supported = {
|
|
178
|
+
str(getattr(item, "id", ""))
|
|
179
|
+
for item in tuple(getattr(selected, "reasoning_efforts", ()) or ())
|
|
180
|
+
}
|
|
181
|
+
return not supported or profile.reasoning_effort in supported
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def sync_active_profile_to_config(cfg: AppConfig) -> bool:
|
|
185
|
+
"""Mirror the active profile connection defaults onto legacy top-level fields."""
|
|
186
|
+
active = str((cfg.extra_fields or {}).get("active_profile") or "").strip()
|
|
187
|
+
if not active:
|
|
188
|
+
return False
|
|
189
|
+
profile = get_profile(cfg, active)
|
|
190
|
+
if profile is None:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
changed = False
|
|
194
|
+
if profile.base_url and not _same_base_url(getattr(cfg, "base_url", ""), profile.base_url):
|
|
195
|
+
cfg.base_url = profile.base_url
|
|
196
|
+
changed = True
|
|
197
|
+
desired_model = (
|
|
198
|
+
profile.default_model if profile.auth_provider else profile.default_model or None
|
|
199
|
+
)
|
|
200
|
+
if desired_model is not None and str(getattr(cfg, "model", "") or "") != desired_model:
|
|
201
|
+
cfg.model = desired_model
|
|
202
|
+
changed = True
|
|
203
|
+
if profile.reasoning_effort is not None or profile.auth_provider:
|
|
204
|
+
effective_effort = (
|
|
205
|
+
None if profile.reasoning_effort in {None, "auto"} else profile.reasoning_effort
|
|
206
|
+
)
|
|
207
|
+
effective_thinking = None if effective_effort is None else effective_effort != "none"
|
|
208
|
+
if (
|
|
209
|
+
getattr(cfg, "llm_reasoning_effort", None) == effective_effort
|
|
210
|
+
and getattr(cfg, "llm_enable_thinking", None) == effective_thinking
|
|
211
|
+
):
|
|
212
|
+
return changed
|
|
213
|
+
cfg.llm_reasoning_effort = effective_effort
|
|
214
|
+
cfg.llm_enable_thinking = effective_thinking
|
|
215
|
+
changed = True
|
|
216
|
+
return changed
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def add_profile(
|
|
220
|
+
cfg: AppConfig,
|
|
221
|
+
profile: ProfileSpec,
|
|
222
|
+
*,
|
|
223
|
+
allow_auth_profile_update: bool = False,
|
|
224
|
+
) -> None:
|
|
225
|
+
migrate_legacy_to_profiles(cfg)
|
|
226
|
+
profiles = dict(_profile_dict(cfg))
|
|
227
|
+
existing_data = profiles.get(profile.name)
|
|
228
|
+
if isinstance(existing_data, dict):
|
|
229
|
+
existing = ProfileSpec.from_dict(profile.name, existing_data)
|
|
230
|
+
replacing_auth_profile = bool(existing.auth_provider)
|
|
231
|
+
unchanged = existing.to_dict() == profile.to_dict()
|
|
232
|
+
if replacing_auth_profile and not unchanged and not allow_auth_profile_update:
|
|
233
|
+
_raise_config_error(
|
|
234
|
+
f"Profile {profile.name!r} is managed by the AI subscription "
|
|
235
|
+
f"{existing.auth_provider!r} and cannot be replaced by a generic profile edit."
|
|
236
|
+
)
|
|
237
|
+
profiles[profile.name] = profile.to_dict()
|
|
238
|
+
_set_profile_dict(cfg, profiles)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def remove_profile(cfg: AppConfig, name: str) -> None:
|
|
242
|
+
migrate_legacy_to_profiles(cfg)
|
|
243
|
+
profile_name = _normalize_profile_name(name)
|
|
244
|
+
profiles = dict(_profile_dict(cfg))
|
|
245
|
+
if profile_name not in profiles:
|
|
246
|
+
_raise_config_error(f"Profile not found: {profile_name}")
|
|
247
|
+
profiles.pop(profile_name, None)
|
|
248
|
+
_set_profile_dict(cfg, profiles)
|
|
249
|
+
active = str((cfg.extra_fields or {}).get("active_profile") or "").strip()
|
|
250
|
+
if active != profile_name:
|
|
251
|
+
return
|
|
252
|
+
if profiles:
|
|
253
|
+
set_active_profile(cfg, sorted(profiles)[0])
|
|
254
|
+
else:
|
|
255
|
+
cfg.extra_fields.pop("active_profile", None)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def set_active_profile(cfg: AppConfig, name: str) -> None:
|
|
259
|
+
profile_name = _normalize_profile_name(name)
|
|
260
|
+
profile = get_profile(cfg, profile_name)
|
|
261
|
+
if profile is None:
|
|
262
|
+
_raise_config_error(f"Profile not found: {profile_name}")
|
|
263
|
+
previous_profile = str((cfg.extra_fields or {}).get("active_profile") or "").strip()
|
|
264
|
+
if previous_profile and previous_profile != profile.name:
|
|
265
|
+
# Legacy-config hygiene: "router" is a deprecated role_models key from
|
|
266
|
+
# the removed semantic router. It stored a provider-specific model id,
|
|
267
|
+
# so never carry it into a different API profile; drop it on switch.
|
|
268
|
+
for section in ("role_models", "forge_role_models"):
|
|
269
|
+
raw_models = cfg.extra_fields.get(section)
|
|
270
|
+
if not isinstance(raw_models, dict) or "router" not in raw_models:
|
|
271
|
+
continue
|
|
272
|
+
next_models = dict(raw_models)
|
|
273
|
+
next_models.pop("router", None)
|
|
274
|
+
if next_models:
|
|
275
|
+
cfg.extra_fields[section] = next_models
|
|
276
|
+
else:
|
|
277
|
+
cfg.extra_fields.pop(section, None)
|
|
278
|
+
cfg.extra_fields["active_profile"] = profile.name
|
|
279
|
+
if profile.base_url:
|
|
280
|
+
cfg.base_url = profile.base_url
|
|
281
|
+
if profile.default_model or profile.auth_provider:
|
|
282
|
+
cfg.model = profile.default_model
|
|
283
|
+
if profile.reasoning_effort is not None or profile.auth_provider:
|
|
284
|
+
effective_effort = (
|
|
285
|
+
None if profile.reasoning_effort in {None, "auto"} else profile.reasoning_effort
|
|
286
|
+
)
|
|
287
|
+
cfg.llm_reasoning_effort = effective_effort
|
|
288
|
+
cfg.llm_enable_thinking = None if effective_effort is None else effective_effort != "none"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def update_active_profile_defaults(
|
|
292
|
+
cfg: AppConfig,
|
|
293
|
+
*,
|
|
294
|
+
base_url: str | None = None,
|
|
295
|
+
default_model: str | None = None,
|
|
296
|
+
reasoning_effort: str | None | object = _UNSET,
|
|
297
|
+
allow_subscription_selection: bool = False,
|
|
298
|
+
) -> bool:
|
|
299
|
+
if not isinstance((cfg.extra_fields or {}).get("profiles"), dict):
|
|
300
|
+
return False
|
|
301
|
+
active = str((cfg.extra_fields or {}).get("active_profile") or "").strip()
|
|
302
|
+
if not active:
|
|
303
|
+
return False
|
|
304
|
+
current = get_profile(cfg, active)
|
|
305
|
+
if current is None:
|
|
306
|
+
_raise_config_error(f"Active profile not found: {active}")
|
|
307
|
+
if current.auth_provider:
|
|
308
|
+
if base_url is not None and not _same_base_url(base_url, current.base_url):
|
|
309
|
+
_raise_config_error(
|
|
310
|
+
"Subscription endpoints are owned by their provider adapter; reconnect "
|
|
311
|
+
"through setup instead of overriding base_url."
|
|
312
|
+
)
|
|
313
|
+
selection_changes_requested = default_model is not None or reasoning_effort is not _UNSET
|
|
314
|
+
if selection_changes_requested and not allow_subscription_selection:
|
|
315
|
+
_raise_config_error(
|
|
316
|
+
"Subscription model and reasoning effort are managed in /config → Default Model."
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
next_base_url = current.base_url
|
|
320
|
+
if base_url is not None:
|
|
321
|
+
next_base_url = validate_base_url(base_url, key="base_url", allow_empty=True)
|
|
322
|
+
|
|
323
|
+
next_default_model = current.default_model
|
|
324
|
+
if default_model is not None:
|
|
325
|
+
next_default_model = str(default_model or "").strip()
|
|
326
|
+
next_reasoning_effort = current.reasoning_effort
|
|
327
|
+
if reasoning_effort is not _UNSET:
|
|
328
|
+
next_reasoning_effort = str(reasoning_effort or "auto").strip().lower() or "auto"
|
|
329
|
+
|
|
330
|
+
updated = ProfileSpec(
|
|
331
|
+
name=current.name,
|
|
332
|
+
protocol=current.protocol,
|
|
333
|
+
base_url=next_base_url,
|
|
334
|
+
api_key_env=current.api_key_env,
|
|
335
|
+
auth_provider=current.auth_provider,
|
|
336
|
+
extra_headers=dict(current.extra_headers),
|
|
337
|
+
default_model=next_default_model,
|
|
338
|
+
reasoning_effort=next_reasoning_effort,
|
|
339
|
+
reasoning_trace_adapter=current.reasoning_trace_adapter,
|
|
340
|
+
web_search_adapter=current.web_search_adapter,
|
|
341
|
+
web_search_model=current.web_search_model,
|
|
342
|
+
notes=current.notes,
|
|
343
|
+
cache_capability=current.cache_capability,
|
|
344
|
+
)
|
|
345
|
+
if updated.to_dict() == current.to_dict():
|
|
346
|
+
return False
|
|
347
|
+
add_profile(
|
|
348
|
+
cfg,
|
|
349
|
+
updated,
|
|
350
|
+
allow_auth_profile_update=bool(current.auth_provider),
|
|
351
|
+
)
|
|
352
|
+
if base_url is not None:
|
|
353
|
+
cfg.base_url = updated.base_url
|
|
354
|
+
if default_model is not None:
|
|
355
|
+
cfg.model = updated.default_model
|
|
356
|
+
if reasoning_effort is not _UNSET:
|
|
357
|
+
effective_effort = None if updated.reasoning_effort == "auto" else updated.reasoning_effort
|
|
358
|
+
cfg.llm_reasoning_effort = effective_effort
|
|
359
|
+
cfg.llm_enable_thinking = None if effective_effort is None else effective_effort != "none"
|
|
360
|
+
return True
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def update_profile(
|
|
364
|
+
cfg: AppConfig,
|
|
365
|
+
name: str,
|
|
366
|
+
*,
|
|
367
|
+
allow_auth_profile_update: bool = False,
|
|
368
|
+
allow_subscription_selection: bool = False,
|
|
369
|
+
**fields: Any,
|
|
370
|
+
) -> None:
|
|
371
|
+
profile_name = _normalize_profile_name(name)
|
|
372
|
+
current = get_profile(cfg, profile_name)
|
|
373
|
+
if current is None:
|
|
374
|
+
_raise_config_error(f"Profile not found: {profile_name}")
|
|
375
|
+
if current.auth_provider:
|
|
376
|
+
connection_fields = {
|
|
377
|
+
"protocol",
|
|
378
|
+
"base_url",
|
|
379
|
+
"api_key_env",
|
|
380
|
+
"auth_provider",
|
|
381
|
+
"extra_headers",
|
|
382
|
+
"reasoning_trace_adapter",
|
|
383
|
+
}
|
|
384
|
+
if connection_fields.intersection(fields) and not allow_auth_profile_update:
|
|
385
|
+
_raise_config_error(
|
|
386
|
+
"Subscription connection fields are owned by their provider adapter."
|
|
387
|
+
)
|
|
388
|
+
if {"default_model", "reasoning_effort"}.intersection(fields) and not (
|
|
389
|
+
allow_subscription_selection or allow_auth_profile_update
|
|
390
|
+
):
|
|
391
|
+
_raise_config_error(
|
|
392
|
+
"Subscription model and reasoning effort are managed in /config → Default Model."
|
|
393
|
+
)
|
|
394
|
+
values = {
|
|
395
|
+
"name": current.name,
|
|
396
|
+
"protocol": current.protocol,
|
|
397
|
+
"base_url": current.base_url,
|
|
398
|
+
"api_key_env": current.api_key_env,
|
|
399
|
+
"auth_provider": current.auth_provider,
|
|
400
|
+
"extra_headers": dict(current.extra_headers),
|
|
401
|
+
"default_model": current.default_model,
|
|
402
|
+
"reasoning_effort": current.reasoning_effort,
|
|
403
|
+
"reasoning_trace_adapter": current.reasoning_trace_adapter,
|
|
404
|
+
"web_search_adapter": current.web_search_adapter,
|
|
405
|
+
"web_search_model": current.web_search_model,
|
|
406
|
+
"notes": current.notes,
|
|
407
|
+
"cache_capability": current.cache_capability,
|
|
408
|
+
}
|
|
409
|
+
values.update(fields)
|
|
410
|
+
updated = ProfileSpec(**values)
|
|
411
|
+
add_profile(
|
|
412
|
+
cfg,
|
|
413
|
+
updated,
|
|
414
|
+
allow_auth_profile_update=bool(current.auth_provider),
|
|
415
|
+
)
|
|
416
|
+
if str((cfg.extra_fields or {}).get("active_profile") or "") == profile_name:
|
|
417
|
+
set_active_profile(cfg, profile_name)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def rename_profile(cfg: AppConfig, old: str, new: str) -> None:
|
|
421
|
+
old_name = _normalize_profile_name(old)
|
|
422
|
+
new_name = _normalize_profile_name(new)
|
|
423
|
+
if old_name == new_name:
|
|
424
|
+
return
|
|
425
|
+
current = get_profile(cfg, old_name)
|
|
426
|
+
if current is None:
|
|
427
|
+
_raise_config_error(f"Profile not found: {old_name}")
|
|
428
|
+
if get_profile(cfg, new_name) is not None:
|
|
429
|
+
_raise_config_error(f"Profile already exists: {new_name}")
|
|
430
|
+
profiles = dict(_profile_dict(cfg))
|
|
431
|
+
profiles.pop(old_name, None)
|
|
432
|
+
profiles[new_name] = ProfileSpec(
|
|
433
|
+
name=new_name,
|
|
434
|
+
protocol=current.protocol,
|
|
435
|
+
base_url=current.base_url,
|
|
436
|
+
api_key_env=current.api_key_env,
|
|
437
|
+
auth_provider=current.auth_provider,
|
|
438
|
+
extra_headers=dict(current.extra_headers),
|
|
439
|
+
default_model=current.default_model,
|
|
440
|
+
reasoning_effort=current.reasoning_effort,
|
|
441
|
+
reasoning_trace_adapter=current.reasoning_trace_adapter,
|
|
442
|
+
web_search_adapter=current.web_search_adapter,
|
|
443
|
+
web_search_model=current.web_search_model,
|
|
444
|
+
notes=current.notes,
|
|
445
|
+
cache_capability=current.cache_capability,
|
|
446
|
+
).to_dict()
|
|
447
|
+
_set_profile_dict(cfg, profiles)
|
|
448
|
+
if str((cfg.extra_fields or {}).get("active_profile") or "") == old_name:
|
|
449
|
+
set_active_profile(cfg, new_name)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _rename_cloud_profile(extra_fields: dict[str, Any]) -> bool:
|
|
453
|
+
"""Rename the hosted Pro profile from its pre-rebrand key.
|
|
454
|
+
|
|
455
|
+
Pre-rebrand installs stored the hosted provider as a profile literally named
|
|
456
|
+
"sylliptor", including in ``active_profile``. Left alone, a Pro user would
|
|
457
|
+
open the CLI to find their subscription profile missing.
|
|
458
|
+
"""
|
|
459
|
+
from .config import CLOUD_PROFILE_KEY, LEGACY_CLOUD_PROFILE_KEY
|
|
460
|
+
|
|
461
|
+
profiles = extra_fields.get("profiles")
|
|
462
|
+
if not isinstance(profiles, dict) or LEGACY_CLOUD_PROFILE_KEY not in profiles:
|
|
463
|
+
return False
|
|
464
|
+
if CLOUD_PROFILE_KEY in profiles:
|
|
465
|
+
# Both present: the current one is authoritative, drop the stale entry.
|
|
466
|
+
profiles.pop(LEGACY_CLOUD_PROFILE_KEY, None)
|
|
467
|
+
else:
|
|
468
|
+
entry = dict(profiles.pop(LEGACY_CLOUD_PROFILE_KEY))
|
|
469
|
+
entry["name"] = CLOUD_PROFILE_KEY
|
|
470
|
+
profiles[CLOUD_PROFILE_KEY] = entry
|
|
471
|
+
|
|
472
|
+
if str(extra_fields.get("active_profile") or "") == LEGACY_CLOUD_PROFILE_KEY:
|
|
473
|
+
extra_fields["active_profile"] = CLOUD_PROFILE_KEY
|
|
474
|
+
extra_fields["profiles"] = profiles
|
|
475
|
+
return True
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def migrate_legacy_to_profiles(cfg: AppConfig) -> bool:
|
|
479
|
+
extra_fields = dict(getattr(cfg, "extra_fields", {}) or {})
|
|
480
|
+
if isinstance(extra_fields.get("profiles"), dict):
|
|
481
|
+
renamed = _rename_cloud_profile(extra_fields)
|
|
482
|
+
cfg.extra_fields = extra_fields
|
|
483
|
+
return renamed
|
|
484
|
+
|
|
485
|
+
base_url = str(getattr(cfg, "base_url", "") or "").strip()
|
|
486
|
+
if not base_url or _same_base_url(base_url, DEFAULT_OPENAI_BASE_URL):
|
|
487
|
+
base_url = DEFAULT_OPENAI_BASE_URL
|
|
488
|
+
api_key_env = None if _legacy_api_key_present() else "OPENAI_API_KEY"
|
|
489
|
+
else:
|
|
490
|
+
api_key_env = None
|
|
491
|
+
profile = ProfileSpec(
|
|
492
|
+
name="default",
|
|
493
|
+
protocol=OPENAI_COMPAT_PROTOCOL,
|
|
494
|
+
base_url=base_url,
|
|
495
|
+
api_key_env=api_key_env,
|
|
496
|
+
default_model=str(getattr(cfg, "model", "") or "").strip(),
|
|
497
|
+
reasoning_effort=(
|
|
498
|
+
str(getattr(cfg, "llm_reasoning_effort", "") or "").strip().lower() or None
|
|
499
|
+
),
|
|
500
|
+
notes="Migrated from legacy base_url/model config.",
|
|
501
|
+
)
|
|
502
|
+
extra_fields["profiles"] = {"default": profile.to_dict()}
|
|
503
|
+
extra_fields["active_profile"] = "default"
|
|
504
|
+
cfg.extra_fields = extra_fields
|
|
505
|
+
return True
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _profile_dict(cfg: AppConfig) -> dict[str, dict[str, Any]]:
|
|
509
|
+
raw = (cfg.extra_fields or {}).get("profiles")
|
|
510
|
+
if not isinstance(raw, dict):
|
|
511
|
+
return {}
|
|
512
|
+
profiles: dict[str, dict[str, Any]] = {}
|
|
513
|
+
for name, data in raw.items():
|
|
514
|
+
profile_name = _normalize_profile_name(str(name))
|
|
515
|
+
if isinstance(data, dict):
|
|
516
|
+
profiles[profile_name] = dict(data)
|
|
517
|
+
return profiles
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _set_profile_dict(cfg: AppConfig, profiles: dict[str, dict[str, Any]]) -> None:
|
|
521
|
+
extra_fields = dict(cfg.extra_fields or {})
|
|
522
|
+
extra_fields["profiles"] = dict(sorted(profiles.items()))
|
|
523
|
+
cfg.extra_fields = extra_fields
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _normalize_profile_name(name: str) -> str:
|
|
527
|
+
normalized = str(name or "").strip().lower()
|
|
528
|
+
_validate_profile_name(normalized)
|
|
529
|
+
return normalized
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _validate_profile_name(name: str) -> None:
|
|
533
|
+
normalized = str(name or "").strip()
|
|
534
|
+
if not normalized:
|
|
535
|
+
_raise_config_error("Profile name must be non-empty.")
|
|
536
|
+
if normalized != normalized.lower() or not _PROFILE_NAME_RE.fullmatch(normalized):
|
|
537
|
+
_raise_config_error(
|
|
538
|
+
"Profile name must use lowercase letters, digits, hyphens, or underscores."
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _validate_protocol(protocol: str) -> None:
|
|
543
|
+
if str(protocol or "").strip() not in SUPPORTED_PROTOCOLS:
|
|
544
|
+
allowed = ", ".join(sorted(SUPPORTED_PROTOCOLS))
|
|
545
|
+
_raise_config_error(f"Profile protocol must be one of: {allowed}")
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _validate_web_search_adapter(adapter: str) -> None:
|
|
549
|
+
try:
|
|
550
|
+
normalize_web_search_adapter(adapter)
|
|
551
|
+
except ValueError as exc:
|
|
552
|
+
_raise_config_error(str(exc))
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _validate_base_url(base_url: str) -> None:
|
|
556
|
+
validate_base_url(base_url, key="Profile base_url")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def validate_base_url(
|
|
560
|
+
base_url: str,
|
|
561
|
+
*,
|
|
562
|
+
key: str = "base_url",
|
|
563
|
+
allow_empty: bool = False,
|
|
564
|
+
) -> str:
|
|
565
|
+
normalized = str(base_url or "").strip()
|
|
566
|
+
if not normalized:
|
|
567
|
+
if allow_empty:
|
|
568
|
+
return ""
|
|
569
|
+
_raise_config_error(f"{key} must be non-empty.")
|
|
570
|
+
if any(ch.isspace() for ch in normalized):
|
|
571
|
+
_raise_config_error(f"{key} must not contain whitespace.")
|
|
572
|
+
try:
|
|
573
|
+
parsed = urlsplit(normalized)
|
|
574
|
+
except ValueError:
|
|
575
|
+
_raise_config_error(f"{key} must be a valid http:// or https:// URL.")
|
|
576
|
+
if parsed.scheme.lower() not in {"http", "https"} or not parsed.netloc:
|
|
577
|
+
_raise_config_error(f"{key} must be a valid http:// or https:// URL.")
|
|
578
|
+
if parsed.query or parsed.fragment:
|
|
579
|
+
_raise_config_error(f"{key} must not include query strings or fragments.")
|
|
580
|
+
return normalized
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def resolve_effective_base_url(*, cfg: AppConfig, profile: ProfileSpec | None = None) -> str:
|
|
584
|
+
resolved_profile = profile or get_active_profile(cfg)
|
|
585
|
+
profile_base_url = validate_base_url(
|
|
586
|
+
resolved_profile.base_url,
|
|
587
|
+
key=f"Profile {resolved_profile.name} base_url",
|
|
588
|
+
allow_empty=True,
|
|
589
|
+
).rstrip("/")
|
|
590
|
+
if profile_base_url:
|
|
591
|
+
return profile_base_url
|
|
592
|
+
cfg_base_url = validate_base_url(
|
|
593
|
+
str(getattr(cfg, "base_url", "") or ""),
|
|
594
|
+
key="base_url",
|
|
595
|
+
allow_empty=True,
|
|
596
|
+
).rstrip("/")
|
|
597
|
+
default_base_url = DEFAULT_OPENAI_BASE_URL.rstrip("/")
|
|
598
|
+
return cfg_base_url or default_base_url
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def connection_fingerprint(cfg: AppConfig) -> tuple[str, ...]:
|
|
602
|
+
"""Return connection fields that require rebuilding an LLM client.
|
|
603
|
+
|
|
604
|
+
API-key profiles can safely apply model/reasoning changes to their existing
|
|
605
|
+
client in place. A trace-adapter change always requires rebuilding the
|
|
606
|
+
concrete client capability, while a subscription model and reasoning effort
|
|
607
|
+
are a provider-managed pair that also require a rebuild.
|
|
608
|
+
"""
|
|
609
|
+
|
|
610
|
+
profile = get_active_profile(cfg)
|
|
611
|
+
execution = getattr(cfg, "execution", None)
|
|
612
|
+
subscription_model = profile.default_model if profile.auth_provider else ""
|
|
613
|
+
subscription_reasoning = profile.reasoning_effort if profile.auth_provider else ""
|
|
614
|
+
return (
|
|
615
|
+
str(getattr(execution, "backend", "native") or "native").strip(),
|
|
616
|
+
str(getattr(execution, "runtime", None) or "").strip(),
|
|
617
|
+
profile.name,
|
|
618
|
+
str(profile.protocol or OPENAI_COMPAT_PROTOCOL).strip(),
|
|
619
|
+
resolve_effective_base_url(cfg=cfg, profile=profile),
|
|
620
|
+
str(profile.auth_provider or "").strip(),
|
|
621
|
+
str(subscription_model or "").strip(),
|
|
622
|
+
str(subscription_reasoning or "").strip(),
|
|
623
|
+
str(profile.reasoning_trace_adapter or "auto").strip(),
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _coerce_cache_capability(raw: Any, *, source: str) -> CacheCapabilitySpec | None:
|
|
628
|
+
try:
|
|
629
|
+
return CacheCapabilitySpec.from_mapping(raw, source=source)
|
|
630
|
+
except ValueError as exc:
|
|
631
|
+
_raise_config_error(str(exc))
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _coerce_headers(raw: Any) -> dict[str, str]:
|
|
635
|
+
if raw is None:
|
|
636
|
+
return {}
|
|
637
|
+
if not isinstance(raw, dict):
|
|
638
|
+
_raise_config_error("Profile extra_headers must be a JSON object.")
|
|
639
|
+
try:
|
|
640
|
+
return canonicalize_extra_headers(raw)
|
|
641
|
+
except ValueError as exc:
|
|
642
|
+
_raise_config_error(str(exc))
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _optional_string(raw: Any) -> str | None:
|
|
646
|
+
value = str(raw or "").strip()
|
|
647
|
+
return value or None
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _same_base_url(left: str, right: str) -> bool:
|
|
651
|
+
return str(left or "").strip().rstrip("/") == str(right or "").strip().rstrip("/")
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _legacy_api_key_present() -> bool:
|
|
655
|
+
try:
|
|
656
|
+
from .config import load_persisted_api_key
|
|
657
|
+
|
|
658
|
+
return bool(load_persisted_api_key())
|
|
659
|
+
except Exception:
|
|
660
|
+
return False
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _raise_config_error(message: str) -> None:
|
|
664
|
+
from .config import ConfigError
|
|
665
|
+
|
|
666
|
+
raise ConfigError(message)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .base import (
|
|
2
|
+
ProviderAccountStatus,
|
|
3
|
+
ProviderAuthAdapter,
|
|
4
|
+
ProviderAuthError,
|
|
5
|
+
ProviderAuthSetupOption,
|
|
6
|
+
ProviderLoginRequiredError,
|
|
7
|
+
ProviderModel,
|
|
8
|
+
ProviderReasoningEffort,
|
|
9
|
+
)
|
|
10
|
+
from .registry import (
|
|
11
|
+
create_provider_auth,
|
|
12
|
+
provider_auth_ids,
|
|
13
|
+
provider_auth_setup_options,
|
|
14
|
+
register_provider_auth,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ProviderAccountStatus",
|
|
19
|
+
"ProviderAuthAdapter",
|
|
20
|
+
"ProviderAuthError",
|
|
21
|
+
"ProviderAuthSetupOption",
|
|
22
|
+
"ProviderLoginRequiredError",
|
|
23
|
+
"ProviderModel",
|
|
24
|
+
"ProviderReasoningEffort",
|
|
25
|
+
"create_provider_auth",
|
|
26
|
+
"provider_auth_ids",
|
|
27
|
+
"provider_auth_setup_options",
|
|
28
|
+
"register_provider_auth",
|
|
29
|
+
]
|