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,685 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
from urllib.parse import urlsplit
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from .llm.protocols import (
|
|
14
|
+
ANTHROPIC_MESSAGES_PROTOCOL,
|
|
15
|
+
GEMINI_GENERATE_CONTENT_PROTOCOL,
|
|
16
|
+
GEMINI_INTERACTIONS_PROTOCOL,
|
|
17
|
+
OPENAI_COMPAT_PROTOCOL,
|
|
18
|
+
)
|
|
19
|
+
from .profiles import ProfileSpec
|
|
20
|
+
|
|
21
|
+
ProviderModelCatalogStrategy = Literal["gemini", "anthropic", "openai"]
|
|
22
|
+
ModelChatCompatibility = Literal["chat", "unknown"]
|
|
23
|
+
|
|
24
|
+
_DEFAULT_TIMEOUT_S = 6.0
|
|
25
|
+
_MAX_RESPONSE_BYTES = 5 * 1024 * 1024
|
|
26
|
+
_MAX_PAGES = 25
|
|
27
|
+
_MAX_MODELS = 5_000
|
|
28
|
+
_MAX_MODEL_ID_LENGTH = 512
|
|
29
|
+
_STREAM_CHUNK_SIZE = 64 * 1024
|
|
30
|
+
_USER_AGENT = "alysis-code"
|
|
31
|
+
_ANTHROPIC_VERSION = "2023-06-01"
|
|
32
|
+
|
|
33
|
+
_NON_CHAT_MODES = frozenset(
|
|
34
|
+
{
|
|
35
|
+
"audio_speech",
|
|
36
|
+
"audio_transcription",
|
|
37
|
+
"completion",
|
|
38
|
+
"completions",
|
|
39
|
+
"embedding",
|
|
40
|
+
"embeddings",
|
|
41
|
+
"image",
|
|
42
|
+
"image_generation",
|
|
43
|
+
"moderation",
|
|
44
|
+
"rerank",
|
|
45
|
+
"reranking",
|
|
46
|
+
"speech",
|
|
47
|
+
"transcription",
|
|
48
|
+
"text_completion",
|
|
49
|
+
"text_completions",
|
|
50
|
+
"video",
|
|
51
|
+
"video_generation",
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
_CHAT_CAPABILITY_NAMES = frozenset(
|
|
55
|
+
{
|
|
56
|
+
"chat",
|
|
57
|
+
"chat_completion",
|
|
58
|
+
"chat_completions",
|
|
59
|
+
"completion_chat",
|
|
60
|
+
"generate_content",
|
|
61
|
+
"generatecontent",
|
|
62
|
+
"messages",
|
|
63
|
+
"responses",
|
|
64
|
+
"text_generation",
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
_ANSI_ESCAPE_RE = re.compile(r"\x1b(?:\[[0-?]*[ -/]*[@-~]|[@-_])")
|
|
68
|
+
_TERMINAL_CONTROL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class ProviderModelOption:
|
|
73
|
+
"""A model advertised by the active API-key provider."""
|
|
74
|
+
|
|
75
|
+
id: str
|
|
76
|
+
label: str
|
|
77
|
+
description: str = ""
|
|
78
|
+
chat_compatibility: ModelChatCompatibility = "unknown"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ProviderModelCatalogError(RuntimeError):
|
|
82
|
+
"""A provider catalog could not be loaded safely.
|
|
83
|
+
|
|
84
|
+
Messages intentionally contain neither response bodies nor request exception
|
|
85
|
+
details because both can echo credentials supplied by a user-controlled endpoint.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def provider_model_catalog_strategy(profile: ProfileSpec) -> ProviderModelCatalogStrategy:
|
|
90
|
+
"""Return the list-model wire format used by ``profile``.
|
|
91
|
+
|
|
92
|
+
First-party compatibility profiles still use their provider-native catalog:
|
|
93
|
+
Gemini's native endpoint exposes ``generateContent`` support and Anthropic's
|
|
94
|
+
native endpoint supplies account-aware model metadata. All other profiles use
|
|
95
|
+
the broadly implemented OpenAI-shaped ``/models`` surface.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
protocol = str(profile.protocol or "").strip().lower()
|
|
99
|
+
if protocol in {GEMINI_GENERATE_CONTENT_PROTOCOL, GEMINI_INTERACTIONS_PROTOCOL}:
|
|
100
|
+
return "gemini"
|
|
101
|
+
if protocol == ANTHROPIC_MESSAGES_PROTOCOL:
|
|
102
|
+
return "anthropic"
|
|
103
|
+
# Compatibility profile names are user-controlled. Dispatching a custom
|
|
104
|
+
# gateway called "gemini-proxy" or "claude" to a first-party wire format
|
|
105
|
+
# would send the wrong auth headers and parse the wrong schema. Only the
|
|
106
|
+
# official first-party hosts opt a compatibility profile into native listing.
|
|
107
|
+
if protocol == OPENAI_COMPAT_PROTOCOL:
|
|
108
|
+
hostname = _url_hostname(profile.base_url)
|
|
109
|
+
if hostname == "generativelanguage.googleapis.com":
|
|
110
|
+
return "gemini"
|
|
111
|
+
if hostname == "api.anthropic.com":
|
|
112
|
+
return "anthropic"
|
|
113
|
+
return "openai"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def discover_provider_models(
|
|
117
|
+
*,
|
|
118
|
+
profile: ProfileSpec,
|
|
119
|
+
api_key: str | None,
|
|
120
|
+
timeout_s: float = _DEFAULT_TIMEOUT_S,
|
|
121
|
+
transport: httpx.BaseTransport | None = None,
|
|
122
|
+
) -> tuple[ProviderModelOption, ...]:
|
|
123
|
+
"""Fetch the active provider's router-compatible model catalog.
|
|
124
|
+
|
|
125
|
+
Discovery is deliberately bounded and offline-safe at the API boundary: it
|
|
126
|
+
never follows redirects, caps pages, model count, and response size, and
|
|
127
|
+
translates all provider/transport failures into a sanitized
|
|
128
|
+
:class:`ProviderModelCatalogError`. Callers can then retain their curated
|
|
129
|
+
preset rows as a fallback without exposing secrets in the TUI.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
base_url = str(profile.base_url or "").strip().rstrip("/")
|
|
133
|
+
if not base_url:
|
|
134
|
+
raise ProviderModelCatalogError("Provider model catalog URL is not configured.")
|
|
135
|
+
if not _valid_timeout(timeout_s):
|
|
136
|
+
raise ProviderModelCatalogError("Provider model catalog timeout must be positive.")
|
|
137
|
+
|
|
138
|
+
strategy = provider_model_catalog_strategy(profile)
|
|
139
|
+
normalized_key = str(api_key or "").strip()
|
|
140
|
+
deadline = time.monotonic() + float(timeout_s)
|
|
141
|
+
if strategy in {"gemini", "anthropic"} and not normalized_key:
|
|
142
|
+
raise ProviderModelCatalogError(
|
|
143
|
+
"An API key is required to load this provider's model catalog."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
with httpx.Client(
|
|
148
|
+
timeout=float(timeout_s),
|
|
149
|
+
transport=transport,
|
|
150
|
+
follow_redirects=False,
|
|
151
|
+
) as client:
|
|
152
|
+
if strategy == "gemini":
|
|
153
|
+
return _discover_gemini_models(
|
|
154
|
+
client=client,
|
|
155
|
+
profile=profile,
|
|
156
|
+
api_key=normalized_key,
|
|
157
|
+
deadline=deadline,
|
|
158
|
+
)
|
|
159
|
+
if strategy == "anthropic":
|
|
160
|
+
return _discover_anthropic_models(
|
|
161
|
+
client=client,
|
|
162
|
+
profile=profile,
|
|
163
|
+
api_key=normalized_key,
|
|
164
|
+
deadline=deadline,
|
|
165
|
+
)
|
|
166
|
+
return _discover_openai_models(
|
|
167
|
+
client=client,
|
|
168
|
+
profile=profile,
|
|
169
|
+
api_key=normalized_key,
|
|
170
|
+
deadline=deadline,
|
|
171
|
+
)
|
|
172
|
+
except ProviderModelCatalogError:
|
|
173
|
+
raise
|
|
174
|
+
except Exception as exc: # noqa: BLE001 - sanitize every provider boundary failure
|
|
175
|
+
raise ProviderModelCatalogError(
|
|
176
|
+
f"Provider model catalog is unavailable ({type(exc).__name__})."
|
|
177
|
+
) from None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _discover_gemini_models(
|
|
181
|
+
*,
|
|
182
|
+
client: httpx.Client,
|
|
183
|
+
profile: ProfileSpec,
|
|
184
|
+
api_key: str,
|
|
185
|
+
deadline: float,
|
|
186
|
+
) -> tuple[ProviderModelOption, ...]:
|
|
187
|
+
url = f"{_gemini_catalog_base_url(profile.base_url)}/models"
|
|
188
|
+
headers = _merged_headers(
|
|
189
|
+
{
|
|
190
|
+
"Accept": "application/json",
|
|
191
|
+
"User-Agent": _USER_AGENT,
|
|
192
|
+
"x-goog-api-key": api_key,
|
|
193
|
+
},
|
|
194
|
+
profile.extra_headers,
|
|
195
|
+
)
|
|
196
|
+
options: list[ProviderModelOption] = []
|
|
197
|
+
seen_ids: set[str] = set()
|
|
198
|
+
seen_tokens: set[str] = set()
|
|
199
|
+
page_token = ""
|
|
200
|
+
|
|
201
|
+
for _page in range(_MAX_PAGES):
|
|
202
|
+
params: dict[str, str | int] = {"pageSize": 1000}
|
|
203
|
+
if page_token:
|
|
204
|
+
params["pageToken"] = page_token
|
|
205
|
+
payload = _get_json(
|
|
206
|
+
client,
|
|
207
|
+
url=url,
|
|
208
|
+
headers=headers,
|
|
209
|
+
params=params,
|
|
210
|
+
deadline=deadline,
|
|
211
|
+
)
|
|
212
|
+
if not isinstance(payload, dict):
|
|
213
|
+
raise ProviderModelCatalogError(
|
|
214
|
+
"Provider model catalog returned an invalid Gemini response."
|
|
215
|
+
)
|
|
216
|
+
raw_models = payload.get("models", [])
|
|
217
|
+
if not isinstance(raw_models, list):
|
|
218
|
+
raise ProviderModelCatalogError(
|
|
219
|
+
"Provider model catalog returned an invalid Gemini model list."
|
|
220
|
+
)
|
|
221
|
+
for raw in raw_models:
|
|
222
|
+
if not isinstance(raw, dict) or not _gemini_supports_generate_content(raw):
|
|
223
|
+
continue
|
|
224
|
+
model_id = _gemini_model_id(raw)
|
|
225
|
+
if not model_id:
|
|
226
|
+
continue
|
|
227
|
+
_append_option(
|
|
228
|
+
options,
|
|
229
|
+
seen_ids,
|
|
230
|
+
ProviderModelOption(
|
|
231
|
+
id=model_id,
|
|
232
|
+
label=_clean_text(raw.get("displayName")) or model_id,
|
|
233
|
+
description=_clean_text(raw.get("description")),
|
|
234
|
+
chat_compatibility="chat",
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
_enforce_model_bound(options)
|
|
238
|
+
|
|
239
|
+
next_token = str(payload.get("nextPageToken") or "").strip()
|
|
240
|
+
if not next_token:
|
|
241
|
+
return tuple(options)
|
|
242
|
+
if next_token == page_token or next_token in seen_tokens:
|
|
243
|
+
raise ProviderModelCatalogError(
|
|
244
|
+
"Provider model catalog returned a repeated Gemini page token."
|
|
245
|
+
)
|
|
246
|
+
seen_tokens.add(next_token)
|
|
247
|
+
page_token = next_token
|
|
248
|
+
|
|
249
|
+
raise ProviderModelCatalogError("Provider model catalog exceeded the Gemini page limit.")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _discover_anthropic_models(
|
|
253
|
+
*,
|
|
254
|
+
client: httpx.Client,
|
|
255
|
+
profile: ProfileSpec,
|
|
256
|
+
api_key: str,
|
|
257
|
+
deadline: float,
|
|
258
|
+
) -> tuple[ProviderModelOption, ...]:
|
|
259
|
+
url = f"{str(profile.base_url).strip().rstrip('/')}/models"
|
|
260
|
+
headers = _merged_headers(
|
|
261
|
+
{
|
|
262
|
+
"Accept": "application/json",
|
|
263
|
+
"User-Agent": _USER_AGENT,
|
|
264
|
+
"x-api-key": api_key,
|
|
265
|
+
"anthropic-version": _ANTHROPIC_VERSION,
|
|
266
|
+
},
|
|
267
|
+
profile.extra_headers,
|
|
268
|
+
)
|
|
269
|
+
options: list[ProviderModelOption] = []
|
|
270
|
+
seen_ids: set[str] = set()
|
|
271
|
+
seen_cursors: set[str] = set()
|
|
272
|
+
after_id = ""
|
|
273
|
+
|
|
274
|
+
for _page in range(_MAX_PAGES):
|
|
275
|
+
params: dict[str, str | int] = {"limit": 1000}
|
|
276
|
+
if after_id:
|
|
277
|
+
params["after_id"] = after_id
|
|
278
|
+
payload = _get_json(
|
|
279
|
+
client,
|
|
280
|
+
url=url,
|
|
281
|
+
headers=headers,
|
|
282
|
+
params=params,
|
|
283
|
+
deadline=deadline,
|
|
284
|
+
)
|
|
285
|
+
if not isinstance(payload, dict) or not isinstance(payload.get("data"), list):
|
|
286
|
+
raise ProviderModelCatalogError(
|
|
287
|
+
"Provider model catalog returned an invalid Anthropic response."
|
|
288
|
+
)
|
|
289
|
+
for raw in payload["data"]:
|
|
290
|
+
if not isinstance(raw, dict):
|
|
291
|
+
continue
|
|
292
|
+
model_id = _clean_model_id(raw.get("id"))
|
|
293
|
+
if not model_id:
|
|
294
|
+
continue
|
|
295
|
+
_append_option(
|
|
296
|
+
options,
|
|
297
|
+
seen_ids,
|
|
298
|
+
ProviderModelOption(
|
|
299
|
+
id=model_id,
|
|
300
|
+
label=_clean_text(raw.get("display_name")) or model_id,
|
|
301
|
+
description=_anthropic_description(raw),
|
|
302
|
+
chat_compatibility="chat",
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
_enforce_model_bound(options)
|
|
306
|
+
|
|
307
|
+
if not bool(payload.get("has_more")):
|
|
308
|
+
return tuple(options)
|
|
309
|
+
next_cursor = str(payload.get("last_id") or "").strip()
|
|
310
|
+
if not next_cursor:
|
|
311
|
+
raise ProviderModelCatalogError(
|
|
312
|
+
"Provider model catalog omitted the next Anthropic cursor."
|
|
313
|
+
)
|
|
314
|
+
if next_cursor == after_id or next_cursor in seen_cursors:
|
|
315
|
+
raise ProviderModelCatalogError(
|
|
316
|
+
"Provider model catalog returned a repeated Anthropic cursor."
|
|
317
|
+
)
|
|
318
|
+
seen_cursors.add(next_cursor)
|
|
319
|
+
after_id = next_cursor
|
|
320
|
+
|
|
321
|
+
raise ProviderModelCatalogError("Provider model catalog exceeded the Anthropic page limit.")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _discover_openai_models(
|
|
325
|
+
*,
|
|
326
|
+
client: httpx.Client,
|
|
327
|
+
profile: ProfileSpec,
|
|
328
|
+
api_key: str,
|
|
329
|
+
deadline: float,
|
|
330
|
+
) -> tuple[ProviderModelOption, ...]:
|
|
331
|
+
url = f"{str(profile.base_url).strip().rstrip('/')}/models"
|
|
332
|
+
base_headers = {
|
|
333
|
+
"Accept": "application/json",
|
|
334
|
+
"User-Agent": _USER_AGENT,
|
|
335
|
+
}
|
|
336
|
+
if api_key:
|
|
337
|
+
base_headers["Authorization"] = f"Bearer {api_key}"
|
|
338
|
+
headers = _merged_headers(base_headers, profile.extra_headers)
|
|
339
|
+
payload = _get_json(client, url=url, headers=headers, deadline=deadline)
|
|
340
|
+
raw_models: Any
|
|
341
|
+
if isinstance(payload, list):
|
|
342
|
+
raw_models = payload
|
|
343
|
+
elif isinstance(payload, dict):
|
|
344
|
+
raw_models = payload.get("data")
|
|
345
|
+
if raw_models is None:
|
|
346
|
+
raw_models = payload.get("models")
|
|
347
|
+
else:
|
|
348
|
+
raw_models = None
|
|
349
|
+
if not isinstance(raw_models, list):
|
|
350
|
+
raise ProviderModelCatalogError(
|
|
351
|
+
"Provider model catalog returned an invalid OpenAI-style response."
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
options: list[ProviderModelOption] = []
|
|
355
|
+
seen_ids: set[str] = set()
|
|
356
|
+
for raw in raw_models:
|
|
357
|
+
if isinstance(raw, str):
|
|
358
|
+
model_id = _clean_model_id(raw)
|
|
359
|
+
if model_id:
|
|
360
|
+
_append_option(
|
|
361
|
+
options,
|
|
362
|
+
seen_ids,
|
|
363
|
+
ProviderModelOption(id=model_id, label=model_id),
|
|
364
|
+
)
|
|
365
|
+
_enforce_model_bound(options)
|
|
366
|
+
continue
|
|
367
|
+
if not isinstance(raw, dict):
|
|
368
|
+
continue
|
|
369
|
+
model_id = _clean_model_id(raw.get("id") or raw.get("model") or raw.get("name"))
|
|
370
|
+
if not model_id or _generic_model_is_non_chat(raw):
|
|
371
|
+
continue
|
|
372
|
+
_append_option(
|
|
373
|
+
options,
|
|
374
|
+
seen_ids,
|
|
375
|
+
ProviderModelOption(
|
|
376
|
+
id=model_id,
|
|
377
|
+
label=_clean_text(raw.get("display_name") or raw.get("displayName")) or model_id,
|
|
378
|
+
description=_clean_text(raw.get("description")),
|
|
379
|
+
chat_compatibility=("chat" if _has_explicit_chat_capability(raw) else "unknown"),
|
|
380
|
+
),
|
|
381
|
+
)
|
|
382
|
+
_enforce_model_bound(options)
|
|
383
|
+
return tuple(options)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _get_json(
|
|
387
|
+
client: httpx.Client,
|
|
388
|
+
*,
|
|
389
|
+
url: str,
|
|
390
|
+
headers: dict[str, str],
|
|
391
|
+
params: dict[str, str | int] | None = None,
|
|
392
|
+
deadline: float,
|
|
393
|
+
) -> Any:
|
|
394
|
+
remaining = deadline - time.monotonic()
|
|
395
|
+
if remaining <= 0:
|
|
396
|
+
raise ProviderModelCatalogError("Provider model catalog request timed out.")
|
|
397
|
+
try:
|
|
398
|
+
with client.stream(
|
|
399
|
+
"GET",
|
|
400
|
+
url,
|
|
401
|
+
headers=headers,
|
|
402
|
+
params=params,
|
|
403
|
+
timeout=remaining,
|
|
404
|
+
) as response:
|
|
405
|
+
if not 200 <= response.status_code < 300:
|
|
406
|
+
raise ProviderModelCatalogError(
|
|
407
|
+
f"Provider model catalog request failed (HTTP {response.status_code})."
|
|
408
|
+
)
|
|
409
|
+
content_length = _positive_int(response.headers.get("content-length"))
|
|
410
|
+
if content_length is not None and content_length > _MAX_RESPONSE_BYTES:
|
|
411
|
+
raise ProviderModelCatalogError("Provider model catalog response was too large.")
|
|
412
|
+
body = bytearray()
|
|
413
|
+
for chunk in response.iter_bytes(chunk_size=_STREAM_CHUNK_SIZE):
|
|
414
|
+
if time.monotonic() >= deadline:
|
|
415
|
+
raise ProviderModelCatalogError("Provider model catalog request timed out.")
|
|
416
|
+
if len(body) + len(chunk) > _MAX_RESPONSE_BYTES:
|
|
417
|
+
raise ProviderModelCatalogError(
|
|
418
|
+
"Provider model catalog response was too large."
|
|
419
|
+
)
|
|
420
|
+
body.extend(chunk)
|
|
421
|
+
except ProviderModelCatalogError:
|
|
422
|
+
raise
|
|
423
|
+
except Exception as exc: # noqa: BLE001 - never leak request details from provider errors
|
|
424
|
+
raise ProviderModelCatalogError(
|
|
425
|
+
f"Provider model catalog request failed ({type(exc).__name__})."
|
|
426
|
+
) from None
|
|
427
|
+
try:
|
|
428
|
+
return json.loads(bytes(body))
|
|
429
|
+
except Exception: # noqa: BLE001 - body is untrusted and must not appear in the error
|
|
430
|
+
raise ProviderModelCatalogError("Provider model catalog returned malformed JSON.") from None
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _gemini_catalog_base_url(base_url: str) -> str:
|
|
434
|
+
normalized = str(base_url or "").strip().rstrip("/")
|
|
435
|
+
for suffix in ("/openai/v1", "/openai"):
|
|
436
|
+
if normalized.casefold().endswith(suffix):
|
|
437
|
+
return normalized[: -len(suffix)].rstrip("/")
|
|
438
|
+
return normalized
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _gemini_supports_generate_content(raw: dict[str, Any]) -> bool:
|
|
442
|
+
methods = raw.get("supportedGenerationMethods")
|
|
443
|
+
return isinstance(methods, list) and any(
|
|
444
|
+
str(method or "").strip().casefold() == "generatecontent" for method in methods
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _gemini_model_id(raw: dict[str, Any]) -> str:
|
|
449
|
+
model_id = _clean_model_id(raw.get("baseModelId"))
|
|
450
|
+
if model_id:
|
|
451
|
+
return model_id.removeprefix("models/")
|
|
452
|
+
return _clean_model_id(raw.get("name")).removeprefix("models/")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _anthropic_description(raw: dict[str, Any]) -> str:
|
|
456
|
+
description = _clean_text(raw.get("description"))
|
|
457
|
+
if description:
|
|
458
|
+
return description
|
|
459
|
+
details: list[str] = []
|
|
460
|
+
max_input = _positive_int(raw.get("max_input_tokens"))
|
|
461
|
+
max_output = _positive_int(raw.get("max_tokens"))
|
|
462
|
+
if max_input is not None:
|
|
463
|
+
details.append(f"{max_input:,} input tokens")
|
|
464
|
+
if max_output is not None:
|
|
465
|
+
details.append(f"{max_output:,} max output tokens")
|
|
466
|
+
return " · ".join(details)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _generic_model_is_non_chat(raw: dict[str, Any]) -> bool:
|
|
470
|
+
explicit_chat = _has_explicit_chat_capability(raw)
|
|
471
|
+
if _has_explicit_non_chat_capability(raw, explicit_chat=explicit_chat):
|
|
472
|
+
return True
|
|
473
|
+
return False
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _has_explicit_chat_capability(raw: dict[str, Any]) -> bool:
|
|
477
|
+
mode = _normalized_capability_name(raw.get("mode"))
|
|
478
|
+
if mode in _CHAT_CAPABILITY_NAMES:
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
endpoints = _string_list(raw.get("supported_endpoints"))
|
|
482
|
+
if endpoints and any(_endpoint_is_chat(endpoint) for endpoint in endpoints):
|
|
483
|
+
return True
|
|
484
|
+
|
|
485
|
+
capabilities = raw.get("capabilities")
|
|
486
|
+
if isinstance(capabilities, dict):
|
|
487
|
+
for name, enabled in capabilities.items():
|
|
488
|
+
if _normalized_capability_name(name) in _CHAT_CAPABILITY_NAMES and _is_supported(
|
|
489
|
+
enabled
|
|
490
|
+
):
|
|
491
|
+
return True
|
|
492
|
+
elif isinstance(capabilities, list):
|
|
493
|
+
if any(
|
|
494
|
+
_normalized_capability_name(value) in _CHAT_CAPABILITY_NAMES for value in capabilities
|
|
495
|
+
):
|
|
496
|
+
return True
|
|
497
|
+
return False
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _has_explicit_non_chat_capability(
|
|
501
|
+
raw: dict[str, Any],
|
|
502
|
+
*,
|
|
503
|
+
explicit_chat: bool,
|
|
504
|
+
) -> bool:
|
|
505
|
+
mode = _normalized_capability_name(raw.get("mode"))
|
|
506
|
+
if mode in _NON_CHAT_MODES:
|
|
507
|
+
return True
|
|
508
|
+
|
|
509
|
+
object_type = _normalized_capability_name(raw.get("type"))
|
|
510
|
+
if object_type in _NON_CHAT_MODES:
|
|
511
|
+
return True
|
|
512
|
+
|
|
513
|
+
output_modalities = _output_modalities(raw)
|
|
514
|
+
if output_modalities and "text" not in output_modalities:
|
|
515
|
+
return True
|
|
516
|
+
|
|
517
|
+
endpoints = _string_list(raw.get("supported_endpoints"))
|
|
518
|
+
if endpoints and not explicit_chat and all(_endpoint_is_clearly_non_chat(v) for v in endpoints):
|
|
519
|
+
return True
|
|
520
|
+
|
|
521
|
+
capabilities = raw.get("capabilities")
|
|
522
|
+
if isinstance(capabilities, dict) and not explicit_chat:
|
|
523
|
+
if any(
|
|
524
|
+
_normalized_capability_name(name) in _NON_CHAT_MODES and _is_supported(enabled)
|
|
525
|
+
for name, enabled in capabilities.items()
|
|
526
|
+
):
|
|
527
|
+
return True
|
|
528
|
+
recognized = [
|
|
529
|
+
_is_supported(enabled)
|
|
530
|
+
for name, enabled in capabilities.items()
|
|
531
|
+
if _normalized_capability_name(name) in _CHAT_CAPABILITY_NAMES
|
|
532
|
+
]
|
|
533
|
+
if recognized and not any(recognized):
|
|
534
|
+
return True
|
|
535
|
+
elif isinstance(capabilities, list) and not explicit_chat:
|
|
536
|
+
if any(_normalized_capability_name(value) in _NON_CHAT_MODES for value in capabilities):
|
|
537
|
+
return True
|
|
538
|
+
return False
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _output_modalities(raw: dict[str, Any]) -> set[str]:
|
|
542
|
+
candidates = [raw.get("output_modalities"), raw.get("supported_output_modalities")]
|
|
543
|
+
architecture = raw.get("architecture")
|
|
544
|
+
if isinstance(architecture, dict):
|
|
545
|
+
candidates.append(architecture.get("output_modalities"))
|
|
546
|
+
modalities: set[str] = set()
|
|
547
|
+
for candidate in candidates:
|
|
548
|
+
modalities.update(value.casefold() for value in _string_list(candidate))
|
|
549
|
+
return modalities
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _endpoint_is_chat(endpoint: str) -> bool:
|
|
553
|
+
normalized = endpoint.strip().casefold().replace("_", "-")
|
|
554
|
+
return any(
|
|
555
|
+
marker in normalized
|
|
556
|
+
for marker in (
|
|
557
|
+
"chat/completions",
|
|
558
|
+
"generatecontent",
|
|
559
|
+
"/messages",
|
|
560
|
+
"/responses",
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _endpoint_is_clearly_non_chat(endpoint: str) -> bool:
|
|
566
|
+
normalized = endpoint.strip().casefold().replace("_", "-")
|
|
567
|
+
if normalized.endswith("/completions") and "chat/completions" not in normalized:
|
|
568
|
+
return True
|
|
569
|
+
return any(
|
|
570
|
+
marker in normalized
|
|
571
|
+
for marker in (
|
|
572
|
+
"/audio/",
|
|
573
|
+
"/embeddings",
|
|
574
|
+
"/images",
|
|
575
|
+
"/moderations",
|
|
576
|
+
"/rerank",
|
|
577
|
+
"/videos",
|
|
578
|
+
)
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _merged_headers(
|
|
583
|
+
defaults: dict[str, str],
|
|
584
|
+
extra_headers: dict[str, str] | None,
|
|
585
|
+
) -> dict[str, str]:
|
|
586
|
+
headers = {
|
|
587
|
+
str(key).strip(): str(value).strip()
|
|
588
|
+
for key, value in defaults.items()
|
|
589
|
+
if str(key).strip() and str(value).strip()
|
|
590
|
+
}
|
|
591
|
+
for raw_key, raw_value in (extra_headers or {}).items():
|
|
592
|
+
key = str(raw_key).strip()
|
|
593
|
+
value = str(raw_value).strip()
|
|
594
|
+
if not key or not value:
|
|
595
|
+
continue
|
|
596
|
+
folded = key.casefold()
|
|
597
|
+
headers = {
|
|
598
|
+
existing: item for existing, item in headers.items() if existing.casefold() != folded
|
|
599
|
+
}
|
|
600
|
+
headers[key] = value
|
|
601
|
+
return headers
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _append_option(
|
|
605
|
+
options: list[ProviderModelOption],
|
|
606
|
+
seen_ids: set[str],
|
|
607
|
+
option: ProviderModelOption,
|
|
608
|
+
) -> None:
|
|
609
|
+
if not option.id or option.id in seen_ids:
|
|
610
|
+
return
|
|
611
|
+
seen_ids.add(option.id)
|
|
612
|
+
options.append(option)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _enforce_model_bound(options: list[ProviderModelOption]) -> None:
|
|
616
|
+
if len(options) > _MAX_MODELS:
|
|
617
|
+
raise ProviderModelCatalogError("Provider model catalog exceeded the model limit.")
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _clean_model_id(value: Any) -> str:
|
|
621
|
+
text = _sanitize_terminal_text(value)
|
|
622
|
+
return text if len(text) <= _MAX_MODEL_ID_LENGTH else ""
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _clean_text(value: Any, *, limit: int = 320) -> str:
|
|
626
|
+
text = _sanitize_terminal_text(value)
|
|
627
|
+
if len(text) > limit:
|
|
628
|
+
return text[: max(0, limit - 3)].rstrip() + "..."
|
|
629
|
+
return text
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _sanitize_terminal_text(value: Any) -> str:
|
|
633
|
+
text = str(value or "")
|
|
634
|
+
text = _ANSI_ESCAPE_RE.sub("", text)
|
|
635
|
+
text = _TERMINAL_CONTROL_RE.sub("", text)
|
|
636
|
+
return " ".join(text.split())
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _positive_int(value: Any) -> int | None:
|
|
640
|
+
try:
|
|
641
|
+
parsed = int(value)
|
|
642
|
+
except (TypeError, ValueError):
|
|
643
|
+
return None
|
|
644
|
+
return parsed if parsed > 0 else None
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _string_list(value: Any) -> tuple[str, ...]:
|
|
648
|
+
if not isinstance(value, (list, tuple)):
|
|
649
|
+
return ()
|
|
650
|
+
return tuple(text for item in value if (text := str(item or "").strip()))
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _normalized_capability_name(value: Any) -> str:
|
|
654
|
+
return re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().casefold()).strip("_")
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _is_supported(value: Any) -> bool:
|
|
658
|
+
if isinstance(value, dict):
|
|
659
|
+
return bool(value.get("supported"))
|
|
660
|
+
return bool(value)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _valid_timeout(value: Any) -> bool:
|
|
664
|
+
try:
|
|
665
|
+
parsed = float(value)
|
|
666
|
+
except (TypeError, ValueError):
|
|
667
|
+
return False
|
|
668
|
+
return math.isfinite(parsed) and parsed > 0
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _url_hostname(value: str) -> str:
|
|
672
|
+
try:
|
|
673
|
+
return str(urlsplit(str(value or "").strip()).hostname or "").rstrip(".").casefold()
|
|
674
|
+
except ValueError:
|
|
675
|
+
return ""
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
__all__ = [
|
|
679
|
+
"ModelChatCompatibility",
|
|
680
|
+
"ProviderModelCatalogError",
|
|
681
|
+
"ProviderModelCatalogStrategy",
|
|
682
|
+
"ProviderModelOption",
|
|
683
|
+
"discover_provider_models",
|
|
684
|
+
"provider_model_catalog_strategy",
|
|
685
|
+
]
|