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,450 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass, replace
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
12
|
+
|
|
13
|
+
from ..internal_artifacts import INTERNAL_ARTIFACT_MESSAGE_KEY
|
|
14
|
+
|
|
15
|
+
PROVIDER_METADATA_KEY = "_alysis_provider_metadata"
|
|
16
|
+
ROUTE_IDENTITY_PROVIDER_METADATA_KEY = "_route_identity"
|
|
17
|
+
OPENAI_RESPONSES_PROVIDER_METADATA_KEY = "openai_responses"
|
|
18
|
+
ANTHROPIC_MESSAGES_PROVIDER_METADATA_KEY = "anthropic_messages"
|
|
19
|
+
GEMINI_GENERATE_CONTENT_PROVIDER_METADATA_KEY = "gemini_generate_content"
|
|
20
|
+
GEMINI_INTERACTIONS_PROVIDER_METADATA_KEY = "gemini_interactions"
|
|
21
|
+
MISTRAL_PROVIDER_METADATA_KEY = "mistral"
|
|
22
|
+
MOONSHOT_PROVIDER_METADATA_KEY = "moonshot"
|
|
23
|
+
QWEN_PROVIDER_METADATA_KEY = "qwen"
|
|
24
|
+
MISTRAL_CONTENT_CHUNKS_KEY = "content_chunks"
|
|
25
|
+
TOOL_CALL_PROVIDER_METADATA_KEY = "_tool_calls"
|
|
26
|
+
DEEPSEEK_REASONING_CONTENT_KEY = "reasoning_content"
|
|
27
|
+
DEEPSEEK_PROVIDER_METADATA_KEY = "deepseek"
|
|
28
|
+
OPENROUTER_REASONING_KEY = "reasoning"
|
|
29
|
+
OPENROUTER_REASONING_DETAILS_KEY = "reasoning_details"
|
|
30
|
+
|
|
31
|
+
STATEFUL_PROVIDER_METADATA_KEYS = frozenset(
|
|
32
|
+
{
|
|
33
|
+
OPENAI_RESPONSES_PROVIDER_METADATA_KEY,
|
|
34
|
+
ANTHROPIC_MESSAGES_PROVIDER_METADATA_KEY,
|
|
35
|
+
GEMINI_GENERATE_CONTENT_PROVIDER_METADATA_KEY,
|
|
36
|
+
GEMINI_INTERACTIONS_PROVIDER_METADATA_KEY,
|
|
37
|
+
# DeepSeek requires opaque reasoning_content to survive every
|
|
38
|
+
# tools-enabled assistant turn, even when that turn called no tool.
|
|
39
|
+
DEEPSEEK_PROVIDER_METADATA_KEY,
|
|
40
|
+
# Mistral requires the full assistant content, including opaque
|
|
41
|
+
# ThinkChunk/signature state, to be replayed on later same-route turns.
|
|
42
|
+
MISTRAL_PROVIDER_METADATA_KEY,
|
|
43
|
+
# Kimi requires reasoning_content to be replayed for same-route
|
|
44
|
+
# multi-turn and tool continuations, including ordinary K3 replies.
|
|
45
|
+
MOONSHOT_PROVIDER_METADATA_KEY,
|
|
46
|
+
# Qwen reasoning is retained privately so model contracts that require
|
|
47
|
+
# preserved thinking can replay it on later same-route assistant turns.
|
|
48
|
+
QWEN_PROVIDER_METADATA_KEY,
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
_HIERARCHICAL_URL_RE = re.compile(
|
|
52
|
+
r"\b[A-Za-z][A-Za-z0-9+.-]*://\S+",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class ProviderRouteIdentity:
|
|
58
|
+
"""Immutable identity for provider-owned continuation state.
|
|
59
|
+
|
|
60
|
+
Provider reasoning payloads, signatures, and server-side continuation IDs
|
|
61
|
+
are only valid on the exact route that produced them. Factory-created
|
|
62
|
+
clients include the configured profile and auth adapter; directly-created
|
|
63
|
+
clients use deterministic empty values for those two fields.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
protocol: str
|
|
67
|
+
base_url: str
|
|
68
|
+
provider_key: str
|
|
69
|
+
model: str
|
|
70
|
+
profile_name: str
|
|
71
|
+
auth_provider: str
|
|
72
|
+
credential_scope: str
|
|
73
|
+
routing_headers: tuple[tuple[str, str], ...]
|
|
74
|
+
routing_fields: tuple[tuple[str, str], ...]
|
|
75
|
+
reasoning_state_adapter: str
|
|
76
|
+
protocol_revision: str
|
|
77
|
+
session_scope: str
|
|
78
|
+
fingerprint: str
|
|
79
|
+
|
|
80
|
+
def as_metadata(self) -> dict[str, Any]:
|
|
81
|
+
# Persist only the versioned digest. Route inputs can contain API-key
|
|
82
|
+
# query parameters or account-routing identifiers and must not enter
|
|
83
|
+
# transcripts/support bundles even in normalized form.
|
|
84
|
+
return {
|
|
85
|
+
"version": 1,
|
|
86
|
+
"fingerprint": self.fingerprint,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _normalize_route_base_url(value: str) -> str:
|
|
91
|
+
raw = str(value or "").strip().rstrip("/")
|
|
92
|
+
if not raw:
|
|
93
|
+
return ""
|
|
94
|
+
try:
|
|
95
|
+
parsed = urlsplit(raw)
|
|
96
|
+
except ValueError:
|
|
97
|
+
return raw
|
|
98
|
+
if not parsed.scheme or not parsed.netloc:
|
|
99
|
+
return raw
|
|
100
|
+
path = parsed.path.rstrip("/")
|
|
101
|
+
query = urlencode(sorted(parse_qsl(parsed.query, keep_blank_values=True)))
|
|
102
|
+
userinfo, separator, authority = parsed.netloc.rpartition("@")
|
|
103
|
+
normalized_netloc = (
|
|
104
|
+
f"{userinfo}@{authority.casefold()}" if separator else parsed.netloc.casefold()
|
|
105
|
+
)
|
|
106
|
+
return urlunsplit(
|
|
107
|
+
(
|
|
108
|
+
parsed.scheme.casefold(),
|
|
109
|
+
normalized_netloc,
|
|
110
|
+
path,
|
|
111
|
+
query,
|
|
112
|
+
"",
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def endpoint_descriptor(value: str) -> dict[str, Any]:
|
|
118
|
+
"""Return a persistence-safe identity for a configured provider endpoint.
|
|
119
|
+
|
|
120
|
+
Provider URLs can contain credentials in userinfo, signed path segments, or
|
|
121
|
+
API keys in query parameters. Session logs need an exact-match signal for
|
|
122
|
+
safe resume, but must not retain those route inputs. The versioned digest
|
|
123
|
+
covers the same normalized endpoint material used by route identities while
|
|
124
|
+
the optional host is derived through ``urlsplit().hostname`` so userinfo,
|
|
125
|
+
ports, paths, queries, and fragments cannot enter persisted metadata.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
normalized = _normalize_route_base_url(value)
|
|
129
|
+
fingerprint = hashlib.sha256(f"alysis-endpoint:v1:{normalized}".encode()).hexdigest()
|
|
130
|
+
descriptor: dict[str, Any] = {
|
|
131
|
+
"version": 1,
|
|
132
|
+
"fingerprint": fingerprint,
|
|
133
|
+
}
|
|
134
|
+
try:
|
|
135
|
+
hostname = str(urlsplit(str(value or "").strip()).hostname or "").rstrip(".")
|
|
136
|
+
safe_host = hostname.encode("idna").decode("ascii").casefold()
|
|
137
|
+
except (UnicodeError, ValueError):
|
|
138
|
+
safe_host = ""
|
|
139
|
+
if safe_host and not any(char.isspace() or char in "/@?#" for char in safe_host):
|
|
140
|
+
descriptor["host"] = safe_host
|
|
141
|
+
return descriptor
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def endpoint_descriptor_matches(value: str, descriptor: Any) -> bool:
|
|
145
|
+
"""Return whether ``value`` exactly matches a persisted endpoint digest."""
|
|
146
|
+
|
|
147
|
+
if not isinstance(descriptor, dict) or descriptor.get("version") != 1:
|
|
148
|
+
return False
|
|
149
|
+
fingerprint = descriptor.get("fingerprint")
|
|
150
|
+
if not isinstance(fingerprint, str) or len(fingerprint) != 64:
|
|
151
|
+
return False
|
|
152
|
+
try:
|
|
153
|
+
int(fingerprint, 16)
|
|
154
|
+
except ValueError:
|
|
155
|
+
return False
|
|
156
|
+
expected = endpoint_descriptor(value)["fingerprint"]
|
|
157
|
+
return hmac.compare_digest(fingerprint.casefold(), expected)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def endpoint_label(value: str) -> str:
|
|
161
|
+
"""Return a human-readable endpoint label that is safe for logs/errors."""
|
|
162
|
+
|
|
163
|
+
descriptor = endpoint_descriptor(value)
|
|
164
|
+
fingerprint = str(descriptor["fingerprint"])
|
|
165
|
+
host = str(descriptor.get("host") or "").strip()
|
|
166
|
+
if host:
|
|
167
|
+
authority = f"[{host}]" if ":" in host else host
|
|
168
|
+
try:
|
|
169
|
+
port = urlsplit(str(value or "").strip()).port
|
|
170
|
+
except ValueError:
|
|
171
|
+
port = None
|
|
172
|
+
if port is not None:
|
|
173
|
+
authority = f"{authority}:{port}"
|
|
174
|
+
return f"{authority} (endpoint {fingerprint[:12]})"
|
|
175
|
+
return f"endpoint {fingerprint[:12]}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def sanitize_urls_for_output(value: Any) -> str:
|
|
179
|
+
"""Remove URL credentials and route material from persisted/displayed text.
|
|
180
|
+
|
|
181
|
+
Provider/network exceptions can embed the full request URL. Replace every
|
|
182
|
+
hierarchical URL with the same safe host+digest label used by transport
|
|
183
|
+
errors, retaining enough correlation for diagnostics without userinfo,
|
|
184
|
+
path, query, or fragment data.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
text = str(value or "")
|
|
188
|
+
|
|
189
|
+
def replace_url(match: re.Match[str]) -> str:
|
|
190
|
+
raw = match.group(0)
|
|
191
|
+
trailing = ""
|
|
192
|
+
while raw and raw[-1] in ".,;:!?)]":
|
|
193
|
+
trailing = raw[-1] + trailing
|
|
194
|
+
raw = raw[:-1]
|
|
195
|
+
return endpoint_label(raw) + trailing
|
|
196
|
+
|
|
197
|
+
return _HIERARCHICAL_URL_RE.sub(replace_url, text)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def build_provider_route_identity(
|
|
201
|
+
*,
|
|
202
|
+
protocol: str,
|
|
203
|
+
base_url: str,
|
|
204
|
+
provider_key: str | None,
|
|
205
|
+
model: str,
|
|
206
|
+
profile_name: str | None = None,
|
|
207
|
+
auth_provider: str | None = None,
|
|
208
|
+
credential_scope: str | None = None,
|
|
209
|
+
routing_headers: Mapping[str, Any] | None = None,
|
|
210
|
+
routing_fields: dict[str, str] | None = None,
|
|
211
|
+
reasoning_state_adapter: str | None = None,
|
|
212
|
+
protocol_revision: str | None = None,
|
|
213
|
+
session_scope: str | None = None,
|
|
214
|
+
) -> ProviderRouteIdentity:
|
|
215
|
+
normalized_routing_headers = canonicalize_extra_headers(routing_headers)
|
|
216
|
+
canonical_routing_headers = tuple(
|
|
217
|
+
sorted(
|
|
218
|
+
(
|
|
219
|
+
str(name).strip().casefold(),
|
|
220
|
+
credential_scope_fingerprint(value),
|
|
221
|
+
)
|
|
222
|
+
for name, value in normalized_routing_headers.items()
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
canonical_routing_fields = tuple(
|
|
226
|
+
sorted(
|
|
227
|
+
(
|
|
228
|
+
str(name).strip(),
|
|
229
|
+
credential_scope_fingerprint(value),
|
|
230
|
+
)
|
|
231
|
+
for name, value in (routing_fields or {}).items()
|
|
232
|
+
if str(name).strip() and str(value or "").strip()
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
fields = {
|
|
236
|
+
"protocol": str(protocol or "").strip().casefold(),
|
|
237
|
+
"base_url": _normalize_route_base_url(base_url),
|
|
238
|
+
"provider_key": str(provider_key or "").strip().casefold(),
|
|
239
|
+
"model": str(model or "").strip(),
|
|
240
|
+
"profile_name": str(profile_name or "").strip(),
|
|
241
|
+
"auth_provider": str(auth_provider or "").strip().casefold(),
|
|
242
|
+
"credential_scope": str(credential_scope or "").strip().casefold(),
|
|
243
|
+
"routing_headers": canonical_routing_headers,
|
|
244
|
+
"routing_fields": canonical_routing_fields,
|
|
245
|
+
"reasoning_state_adapter": str(reasoning_state_adapter or "").strip().casefold(),
|
|
246
|
+
"protocol_revision": str(protocol_revision or "").strip(),
|
|
247
|
+
"session_scope": str(session_scope or "").strip().casefold(),
|
|
248
|
+
}
|
|
249
|
+
canonical = json.dumps(fields, ensure_ascii=True, separators=(",", ":"), sort_keys=True)
|
|
250
|
+
fingerprint = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
251
|
+
return ProviderRouteIdentity(**fields, fingerprint=fingerprint)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def credential_scope_fingerprint(value: Any) -> str:
|
|
255
|
+
material = str(value or "").strip()
|
|
256
|
+
if not material:
|
|
257
|
+
return ""
|
|
258
|
+
return hashlib.sha256(f"alysis-route-scope:v1:{material}".encode()).hexdigest()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def canonicalize_extra_headers(headers: Mapping[str, Any] | None) -> dict[str, str]:
|
|
262
|
+
"""Normalize configured headers once for both transport and route identity.
|
|
263
|
+
|
|
264
|
+
HTTP header names are case-insensitive, so case-variant duplicates are
|
|
265
|
+
ambiguous and rejected. Optional whitespace is removed from names and
|
|
266
|
+
values before either sending or hashing, preventing wire/fingerprint drift.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
canonical: dict[str, str] = {}
|
|
270
|
+
for raw_name, raw_value in (headers or {}).items():
|
|
271
|
+
name = str(raw_name or "").strip().casefold()
|
|
272
|
+
value = str(raw_value or "").strip()
|
|
273
|
+
if not name or not value:
|
|
274
|
+
continue
|
|
275
|
+
if name in canonical:
|
|
276
|
+
raise ValueError(f"Duplicate extra header name (case-insensitive): {name}")
|
|
277
|
+
canonical[name] = value
|
|
278
|
+
return canonical
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def merge_canonical_headers(*header_maps: Mapping[str, Any] | None) -> dict[str, str]:
|
|
282
|
+
"""Case-insensitively merge headers, with each later map overriding earlier maps."""
|
|
283
|
+
|
|
284
|
+
merged: dict[str, str] = {}
|
|
285
|
+
for header_map in header_maps:
|
|
286
|
+
merged.update(canonicalize_extra_headers(header_map))
|
|
287
|
+
return merged
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def stamp_provider_metadata_for_route(
|
|
291
|
+
provider_metadata: dict[str, Any] | None,
|
|
292
|
+
route_identity: ProviderRouteIdentity,
|
|
293
|
+
) -> dict[str, Any]:
|
|
294
|
+
stamped = copy.deepcopy(provider_metadata) if isinstance(provider_metadata, dict) else {}
|
|
295
|
+
stamped[ROUTE_IDENTITY_PROVIDER_METADATA_KEY] = route_identity.as_metadata()
|
|
296
|
+
return stamped
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def stamp_response_for_route(response: Any, route_identity: ProviderRouteIdentity) -> Any:
|
|
300
|
+
"""Return a response whose provider metadata is bound to ``route_identity``."""
|
|
301
|
+
|
|
302
|
+
provider_metadata = getattr(response, "provider_metadata", None)
|
|
303
|
+
tool_calls = getattr(response, "tool_calls", None)
|
|
304
|
+
has_tool_metadata = isinstance(tool_calls, list) and any(
|
|
305
|
+
isinstance(getattr(tool_call, "provider_metadata", None), dict)
|
|
306
|
+
and bool(getattr(tool_call, "provider_metadata", None))
|
|
307
|
+
for tool_call in tool_calls
|
|
308
|
+
)
|
|
309
|
+
if not (isinstance(provider_metadata, dict) and provider_metadata) and not has_tool_metadata:
|
|
310
|
+
return response
|
|
311
|
+
return replace(
|
|
312
|
+
response,
|
|
313
|
+
provider_metadata=stamp_provider_metadata_for_route(
|
|
314
|
+
provider_metadata,
|
|
315
|
+
route_identity,
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def provider_metadata_matches_route(
|
|
321
|
+
provider_metadata: Any,
|
|
322
|
+
route_identity: ProviderRouteIdentity,
|
|
323
|
+
) -> bool:
|
|
324
|
+
if not isinstance(provider_metadata, dict):
|
|
325
|
+
return False
|
|
326
|
+
stamped = provider_metadata.get(ROUTE_IDENTITY_PROVIDER_METADATA_KEY)
|
|
327
|
+
return isinstance(stamped, dict) and stamped == route_identity.as_metadata()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def gate_messages_for_provider_route(
|
|
331
|
+
messages: list[dict[str, Any]],
|
|
332
|
+
route_identity: ProviderRouteIdentity,
|
|
333
|
+
) -> list[dict[str, Any]]:
|
|
334
|
+
"""Keep provider state only when its exact route stamp matches.
|
|
335
|
+
|
|
336
|
+
Legacy unstamped state and mismatched state fail closed. Public message
|
|
337
|
+
content and tool calls remain available for ordinary full-history replay.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
gated: list[dict[str, Any]] = []
|
|
341
|
+
for message in messages:
|
|
342
|
+
if not isinstance(message, dict):
|
|
343
|
+
continue
|
|
344
|
+
metadata = message.get(PROVIDER_METADATA_KEY)
|
|
345
|
+
copied = strip_provider_metadata_from_message(copy.deepcopy(message))
|
|
346
|
+
if provider_metadata_matches_route(metadata, route_identity):
|
|
347
|
+
copied[PROVIDER_METADATA_KEY] = copy.deepcopy(metadata)
|
|
348
|
+
gated.append(copied)
|
|
349
|
+
return gated
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def strip_provider_metadata_from_messages(
|
|
353
|
+
messages: list[dict[str, Any]],
|
|
354
|
+
) -> list[dict[str, Any]]:
|
|
355
|
+
return [
|
|
356
|
+
strip_provider_metadata_from_message(copy.deepcopy(message))
|
|
357
|
+
for message in messages
|
|
358
|
+
if isinstance(message, dict)
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def merge_provider_metadata(*items: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
363
|
+
merged: dict[str, Any] = {}
|
|
364
|
+
for item in items:
|
|
365
|
+
if not isinstance(item, dict):
|
|
366
|
+
continue
|
|
367
|
+
for key, value in item.items():
|
|
368
|
+
if isinstance(value, dict) and value:
|
|
369
|
+
merged[str(key)] = copy.deepcopy(value)
|
|
370
|
+
return merged or None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def tool_call_metadata_entries(response: Any) -> list[dict[str, Any]]:
|
|
374
|
+
tool_calls = getattr(response, "tool_calls", None)
|
|
375
|
+
if not isinstance(tool_calls, list):
|
|
376
|
+
return []
|
|
377
|
+
entries: list[dict[str, Any]] = []
|
|
378
|
+
for index, tool_call in enumerate(tool_calls):
|
|
379
|
+
metadata = getattr(tool_call, "provider_metadata", None)
|
|
380
|
+
merged = merge_provider_metadata(metadata)
|
|
381
|
+
if not merged:
|
|
382
|
+
continue
|
|
383
|
+
entry: dict[str, Any] = {
|
|
384
|
+
"index": index,
|
|
385
|
+
"metadata": merged,
|
|
386
|
+
}
|
|
387
|
+
tool_call_id = str(getattr(tool_call, "id", "") or "")
|
|
388
|
+
if tool_call_id:
|
|
389
|
+
entry["id"] = tool_call_id
|
|
390
|
+
entries.append(entry)
|
|
391
|
+
return entries
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def strip_provider_metadata_from_message(message: dict[str, Any]) -> dict[str, Any]:
|
|
395
|
+
copied = dict(message)
|
|
396
|
+
copied.pop(PROVIDER_METADATA_KEY, None)
|
|
397
|
+
copied.pop(INTERNAL_ARTIFACT_MESSAGE_KEY, None)
|
|
398
|
+
copied.pop(DEEPSEEK_REASONING_CONTENT_KEY, None)
|
|
399
|
+
copied.pop(OPENROUTER_REASONING_KEY, None)
|
|
400
|
+
copied.pop(OPENROUTER_REASONING_DETAILS_KEY, None)
|
|
401
|
+
return copied
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def attach_provider_metadata_to_assistant_message(
|
|
405
|
+
message: dict[str, Any],
|
|
406
|
+
response: Any,
|
|
407
|
+
) -> dict[str, Any]:
|
|
408
|
+
if str(message.get("role") or "") != "assistant":
|
|
409
|
+
return message
|
|
410
|
+
message_metadata = merge_provider_metadata(getattr(response, "provider_metadata", None))
|
|
411
|
+
if not message.get("tool_calls") and not (
|
|
412
|
+
isinstance(message_metadata, dict)
|
|
413
|
+
and any(key in message_metadata for key in STATEFUL_PROVIDER_METADATA_KEYS)
|
|
414
|
+
):
|
|
415
|
+
return message
|
|
416
|
+
tool_call_metadata = tool_call_metadata_entries(response)
|
|
417
|
+
if not message_metadata and not tool_call_metadata:
|
|
418
|
+
return message
|
|
419
|
+
copied = dict(message)
|
|
420
|
+
merged = dict(message_metadata or {})
|
|
421
|
+
if tool_call_metadata:
|
|
422
|
+
merged[TOOL_CALL_PROVIDER_METADATA_KEY] = tool_call_metadata
|
|
423
|
+
copied[PROVIDER_METADATA_KEY] = merged
|
|
424
|
+
return copied
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def assistant_message_from_response(
|
|
428
|
+
response: Any,
|
|
429
|
+
*,
|
|
430
|
+
content: str | None = None,
|
|
431
|
+
) -> dict[str, Any]:
|
|
432
|
+
message_content = getattr(response, "content", "") if content is None else content
|
|
433
|
+
message: dict[str, Any] = {
|
|
434
|
+
"role": "assistant",
|
|
435
|
+
"content": str(message_content or ""),
|
|
436
|
+
}
|
|
437
|
+
tool_calls = getattr(response, "tool_calls", None)
|
|
438
|
+
if isinstance(tool_calls, list) and tool_calls:
|
|
439
|
+
message["tool_calls"] = [
|
|
440
|
+
{
|
|
441
|
+
"id": str(getattr(tool_call, "id", "") or ""),
|
|
442
|
+
"type": "function",
|
|
443
|
+
"function": {
|
|
444
|
+
"name": str(getattr(tool_call, "name", "") or ""),
|
|
445
|
+
"arguments": json.dumps(getattr(tool_call, "arguments", {}) or {}),
|
|
446
|
+
},
|
|
447
|
+
}
|
|
448
|
+
for tool_call in tool_calls
|
|
449
|
+
]
|
|
450
|
+
return attach_provider_metadata_to_assistant_message(message, response)
|