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,739 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ..branding import env_get
|
|
11
|
+
from ..error_text import sanitize_error_text_for_output
|
|
12
|
+
from ..provider_telemetry import ProviderCallTelemetryRecorder
|
|
13
|
+
from .metadata import (
|
|
14
|
+
GEMINI_INTERACTIONS_PROVIDER_METADATA_KEY,
|
|
15
|
+
ProviderRouteIdentity,
|
|
16
|
+
build_provider_route_identity,
|
|
17
|
+
canonicalize_extra_headers,
|
|
18
|
+
credential_scope_fingerprint,
|
|
19
|
+
gate_messages_for_provider_route,
|
|
20
|
+
merge_canonical_headers,
|
|
21
|
+
stamp_response_for_route,
|
|
22
|
+
)
|
|
23
|
+
from .provider_limits import (
|
|
24
|
+
DEFAULT_PROVIDER_CONCURRENCY_CAPS,
|
|
25
|
+
ProviderRetrySettings,
|
|
26
|
+
best_effort_provider_key,
|
|
27
|
+
run_provider_limited_call,
|
|
28
|
+
)
|
|
29
|
+
from .request_plan import LLMRequestPlan
|
|
30
|
+
from .request_shape import build_request_shape_report
|
|
31
|
+
from .temperature_compat import documented_temperature_omit_reason
|
|
32
|
+
from .types import (
|
|
33
|
+
InputTokenCount,
|
|
34
|
+
LLMError,
|
|
35
|
+
LLMResponse,
|
|
36
|
+
LLMUsage,
|
|
37
|
+
ReasoningOutput,
|
|
38
|
+
ReasoningOutputKind,
|
|
39
|
+
UsageConfidence,
|
|
40
|
+
UsageContract,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from ..config import AppConfig
|
|
45
|
+
|
|
46
|
+
GEMINI_INTERACTIONS_EXPERIMENT_ENV = "ALYSIS_EXPERIMENTAL_GEMINI_INTERACTIONS"
|
|
47
|
+
GEMINI_INTERACTIONS_CONFIG_FLAG = "experimental_gemini_interactions_enabled"
|
|
48
|
+
_INTERACTIONS_API_REVISION = "2026-05-20"
|
|
49
|
+
GEMINI_INTERACTIONS_ROUTE_REVISION = _INTERACTIONS_API_REVISION
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def gemini_interactions_enabled(cfg: AppConfig | None = None) -> bool:
|
|
53
|
+
"""Return whether the experimental Gemini Interactions protocol is explicitly enabled."""
|
|
54
|
+
env_value = str(env_get(GEMINI_INTERACTIONS_EXPERIMENT_ENV, "") or "").strip().lower()
|
|
55
|
+
if env_value in {"1", "true", "yes", "on"}:
|
|
56
|
+
return True
|
|
57
|
+
if env_value in {"0", "false", "no", "off"}:
|
|
58
|
+
return False
|
|
59
|
+
return bool(getattr(cfg, GEMINI_INTERACTIONS_CONFIG_FLAG, False))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def gemini_interactions_disabled_message() -> str:
|
|
63
|
+
return (
|
|
64
|
+
"protocol='gemini_interactions' is experimental and disabled by default. "
|
|
65
|
+
f"Set {GEMINI_INTERACTIONS_EXPERIMENT_ENV}=1 or run "
|
|
66
|
+
f"`alysis config set {GEMINI_INTERACTIONS_CONFIG_FLAG} true` to enable the "
|
|
67
|
+
"text-only prototype. Gemini GenerateContent remains the stable native Gemini protocol."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class GeminiInteractionsClient:
|
|
72
|
+
"""Minimal text-only prototype for Google's beta Gemini Interactions API."""
|
|
73
|
+
|
|
74
|
+
usage_contract = UsageContract(
|
|
75
|
+
response_usage_confidence=UsageConfidence.AUTHORITATIVE,
|
|
76
|
+
input_token_count_strategy="gemini_count_tokens_projection",
|
|
77
|
+
)
|
|
78
|
+
usage_counts_authoritative = usage_contract.response_usage_authoritative
|
|
79
|
+
supports_tool_calling = False
|
|
80
|
+
supports_forced_tool_choice = False
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
base_url: str,
|
|
86
|
+
api_key: str,
|
|
87
|
+
model: str,
|
|
88
|
+
timeout_s: float = 60.0,
|
|
89
|
+
temperature: float = 0.2,
|
|
90
|
+
prompt_cache_key: str | None = None,
|
|
91
|
+
prompt_cache_retention: str | None = None,
|
|
92
|
+
prompt_cache_policy_metadata: Mapping[str, Any] | None = None,
|
|
93
|
+
enable_thinking: bool | None = None,
|
|
94
|
+
reasoning_effort: str | None = None,
|
|
95
|
+
transport: httpx.BaseTransport | None = None,
|
|
96
|
+
extra_headers: dict[str, str] | None = None,
|
|
97
|
+
provider_key: str | None = None,
|
|
98
|
+
provider_concurrency_caps: dict[str, int] | None = None,
|
|
99
|
+
provider_retry_settings: ProviderRetrySettings | None = None,
|
|
100
|
+
provider_sleep_fn: Callable[[float], None] | None = None,
|
|
101
|
+
provider_random_fn: Callable[[], float] | None = None,
|
|
102
|
+
usage_contract: UsageContract | None = None,
|
|
103
|
+
route_identity: ProviderRouteIdentity | None = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
self.base_url = _gemini_interactions_base_url(base_url)
|
|
106
|
+
self.api_key = api_key
|
|
107
|
+
self.model = model
|
|
108
|
+
self.timeout_s = timeout_s
|
|
109
|
+
self.temperature = temperature
|
|
110
|
+
self.prompt_cache_key = str(prompt_cache_key or "").strip() or None
|
|
111
|
+
self.prompt_cache_retention = str(prompt_cache_retention or "").strip() or None
|
|
112
|
+
self.prompt_cache_policy_metadata = (
|
|
113
|
+
copy.deepcopy(dict(prompt_cache_policy_metadata))
|
|
114
|
+
if isinstance(prompt_cache_policy_metadata, Mapping)
|
|
115
|
+
else None
|
|
116
|
+
)
|
|
117
|
+
self.enable_thinking = enable_thinking
|
|
118
|
+
self.reasoning_effort = str(reasoning_effort or "").strip().lower() or None
|
|
119
|
+
self._transport = transport
|
|
120
|
+
self.extra_headers = canonicalize_extra_headers(extra_headers)
|
|
121
|
+
self.provider_key = str(provider_key or "").strip() or None
|
|
122
|
+
self.route_identity = route_identity or build_provider_route_identity(
|
|
123
|
+
protocol="gemini_interactions",
|
|
124
|
+
base_url=self.base_url,
|
|
125
|
+
provider_key=self.provider_key,
|
|
126
|
+
model=self.model,
|
|
127
|
+
credential_scope=credential_scope_fingerprint(self.api_key),
|
|
128
|
+
routing_headers=self.extra_headers,
|
|
129
|
+
protocol_revision=GEMINI_INTERACTIONS_ROUTE_REVISION,
|
|
130
|
+
)
|
|
131
|
+
self.provider_concurrency_caps = dict(
|
|
132
|
+
DEFAULT_PROVIDER_CONCURRENCY_CAPS
|
|
133
|
+
if provider_concurrency_caps is None
|
|
134
|
+
else provider_concurrency_caps
|
|
135
|
+
)
|
|
136
|
+
self.provider_retry_settings = provider_retry_settings or ProviderRetrySettings()
|
|
137
|
+
self._provider_sleep_fn = provider_sleep_fn
|
|
138
|
+
self._provider_random_fn = provider_random_fn
|
|
139
|
+
self.usage_contract = usage_contract or type(self).usage_contract
|
|
140
|
+
self.usage_counts_authoritative = self.usage_contract.response_usage_authoritative
|
|
141
|
+
self._input_token_count_available: bool | None = None
|
|
142
|
+
self._thinking_summaries_supported: bool | None = None
|
|
143
|
+
|
|
144
|
+
def _headers(self) -> dict[str, str]:
|
|
145
|
+
return merge_canonical_headers(
|
|
146
|
+
{
|
|
147
|
+
"Api-Revision": _INTERACTIONS_API_REVISION,
|
|
148
|
+
"Content-Type": "application/json",
|
|
149
|
+
"User-Agent": "alysis-code/0.1.0",
|
|
150
|
+
"x-goog-api-key": self.api_key,
|
|
151
|
+
},
|
|
152
|
+
self.extra_headers,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def _llm_error_from_response(response: httpx.Response) -> LLMError:
|
|
157
|
+
try:
|
|
158
|
+
data = response.json()
|
|
159
|
+
except Exception:
|
|
160
|
+
body = response.text
|
|
161
|
+
if len(body) > 1000:
|
|
162
|
+
body = body[:1000] + "...(truncated)"
|
|
163
|
+
return LLMError(
|
|
164
|
+
sanitize_error_text_for_output(
|
|
165
|
+
f"Gemini Interactions error {response.status_code}: {body}"
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
message = _extract_error_message(data)
|
|
169
|
+
if message:
|
|
170
|
+
return LLMError(
|
|
171
|
+
sanitize_error_text_for_output(
|
|
172
|
+
f"Gemini Interactions error {response.status_code}: {message}"
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
return LLMError(
|
|
176
|
+
sanitize_error_text_for_output(
|
|
177
|
+
f"Gemini Interactions error {response.status_code}: {data!r}"
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def count_input_tokens(
|
|
182
|
+
self,
|
|
183
|
+
*,
|
|
184
|
+
messages: list[dict[str, Any]],
|
|
185
|
+
tools: list[dict[str, Any]] | None = None,
|
|
186
|
+
tool_choice: Any | None = None,
|
|
187
|
+
) -> InputTokenCount | None:
|
|
188
|
+
if self._input_token_count_available is False or tools or tool_choice is not None:
|
|
189
|
+
return None
|
|
190
|
+
messages = gate_messages_for_provider_route(messages, self.route_identity)
|
|
191
|
+
prompt = _last_user_text(messages)
|
|
192
|
+
if not prompt:
|
|
193
|
+
return None
|
|
194
|
+
_validate_text_only_single_turn_history(messages)
|
|
195
|
+
encoded_model = quote(self.model, safe="")
|
|
196
|
+
url = f"{self.base_url}/models/{encoded_model}:countTokens"
|
|
197
|
+
payload = {
|
|
198
|
+
"contents": [{"role": "user", "parts": [{"text": prompt}]}],
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
def _send_request() -> InputTokenCount | None:
|
|
202
|
+
try:
|
|
203
|
+
with httpx.Client(timeout=self.timeout_s, transport=self._transport) as client:
|
|
204
|
+
response = client.post(url, headers=self._headers(), json=payload)
|
|
205
|
+
except httpx.HTTPError as exc:
|
|
206
|
+
raise LLMError(
|
|
207
|
+
"Gemini input token count request failed: "
|
|
208
|
+
f"{sanitize_error_text_for_output(exc)}"
|
|
209
|
+
) from exc
|
|
210
|
+
if response.status_code in {404, 405, 501}:
|
|
211
|
+
self._input_token_count_available = False
|
|
212
|
+
return None
|
|
213
|
+
if response.status_code >= 400:
|
|
214
|
+
raise self._llm_error_from_response(response)
|
|
215
|
+
try:
|
|
216
|
+
data = response.json()
|
|
217
|
+
except Exception as exc: # noqa: BLE001
|
|
218
|
+
raise LLMError("Gemini input token count returned non-JSON response") from exc
|
|
219
|
+
count = _int_or_none(data.get("totalTokens") if isinstance(data, dict) else None)
|
|
220
|
+
if count is None or count < 0:
|
|
221
|
+
raise LLMError("Gemini input token count response omitted totalTokens")
|
|
222
|
+
self._input_token_count_available = True
|
|
223
|
+
return InputTokenCount(
|
|
224
|
+
input_tokens=count,
|
|
225
|
+
confidence=UsageConfidence.REPORTED,
|
|
226
|
+
raw_provider_usage=copy.deepcopy(data),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return run_provider_limited_call(
|
|
230
|
+
call=_send_request,
|
|
231
|
+
provider_key=self.provider_key,
|
|
232
|
+
provider_concurrency_caps=self.provider_concurrency_caps,
|
|
233
|
+
retry_settings=self.provider_retry_settings,
|
|
234
|
+
operation="gemini_interactions_count_input_tokens",
|
|
235
|
+
sleep_fn=self._provider_sleep_fn,
|
|
236
|
+
random_fn=self._provider_random_fn,
|
|
237
|
+
retry_deadline_allows=getattr(self, "_provider_retry_deadline_allows", None),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def chat(
|
|
241
|
+
self,
|
|
242
|
+
*,
|
|
243
|
+
messages: list[dict[str, Any]],
|
|
244
|
+
tools: list[dict[str, Any]] | None = None,
|
|
245
|
+
tool_choice: Any | None = None,
|
|
246
|
+
response_format: dict[str, Any] | None = None,
|
|
247
|
+
stream: bool = False,
|
|
248
|
+
on_text_delta: Callable[[str], None] | None = None,
|
|
249
|
+
on_reasoning_delta: Callable[[str], None] | None = None,
|
|
250
|
+
temperature: float | None = None,
|
|
251
|
+
max_tokens: int | None = None,
|
|
252
|
+
) -> LLMResponse:
|
|
253
|
+
del on_text_delta
|
|
254
|
+
messages = gate_messages_for_provider_route(messages, self.route_identity)
|
|
255
|
+
if stream:
|
|
256
|
+
raise LLMError("Gemini Interactions prototype does not support streaming yet")
|
|
257
|
+
if tools:
|
|
258
|
+
raise LLMError("Gemini Interactions prototype does not support tools yet")
|
|
259
|
+
if tool_choice is not None:
|
|
260
|
+
raise LLMError("Gemini Interactions prototype does not support tool_choice yet")
|
|
261
|
+
if response_format is not None:
|
|
262
|
+
raise LLMError("Gemini Interactions prototype does not support response_format yet")
|
|
263
|
+
if self.prompt_cache_key or self.prompt_cache_retention:
|
|
264
|
+
raise LLMError("Gemini Interactions does not support prompt_cache_key settings")
|
|
265
|
+
if self.enable_thinking is not None or self.reasoning_effort:
|
|
266
|
+
raise LLMError("Gemini Interactions prototype does not support thinking settings yet")
|
|
267
|
+
|
|
268
|
+
prompt = _last_user_text(messages)
|
|
269
|
+
if not prompt:
|
|
270
|
+
raise LLMError("Gemini Interactions prototype requires a non-empty user message")
|
|
271
|
+
_validate_text_only_single_turn_history(messages)
|
|
272
|
+
|
|
273
|
+
payload: dict[str, Any] = {
|
|
274
|
+
"input": prompt,
|
|
275
|
+
"model": self.model,
|
|
276
|
+
}
|
|
277
|
+
temperature_omit_reason = documented_temperature_omit_reason(self.model)
|
|
278
|
+
generation_config: dict[str, Any] = {}
|
|
279
|
+
if temperature_omit_reason is None:
|
|
280
|
+
if temperature is not None:
|
|
281
|
+
generation_config["temperature"] = float(temperature)
|
|
282
|
+
elif self.temperature is not None:
|
|
283
|
+
generation_config["temperature"] = self.temperature
|
|
284
|
+
if max_tokens is not None:
|
|
285
|
+
generation_config["max_output_tokens"] = int(max_tokens)
|
|
286
|
+
if on_reasoning_delta is not None and self._thinking_summaries_supported is not False:
|
|
287
|
+
generation_config["thinking_summaries"] = "auto"
|
|
288
|
+
if generation_config:
|
|
289
|
+
payload["generation_config"] = generation_config
|
|
290
|
+
original_payload = copy.deepcopy(payload)
|
|
291
|
+
request_plan = LLMRequestPlan.from_chat_args(
|
|
292
|
+
messages=messages,
|
|
293
|
+
tools=tools,
|
|
294
|
+
tool_choice=tool_choice,
|
|
295
|
+
response_format=response_format,
|
|
296
|
+
stream=stream,
|
|
297
|
+
temperature=temperature,
|
|
298
|
+
max_tokens=max_tokens,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def _request_plan_metadata(
|
|
302
|
+
current_payload: dict[str, Any],
|
|
303
|
+
*,
|
|
304
|
+
input_mode: str,
|
|
305
|
+
fallback_used: bool = False,
|
|
306
|
+
) -> dict[str, Any]:
|
|
307
|
+
extra: dict[str, Any] = {}
|
|
308
|
+
if temperature_omit_reason is not None:
|
|
309
|
+
extra.update(
|
|
310
|
+
{
|
|
311
|
+
"temperature_omitted": True,
|
|
312
|
+
"temperature_omit_reason": temperature_omit_reason,
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
if fallback_used:
|
|
316
|
+
extra["fallback_used"] = True
|
|
317
|
+
return request_plan.request_plan_metadata(
|
|
318
|
+
input_mode=input_mode,
|
|
319
|
+
continuation_strategy="full_replay",
|
|
320
|
+
provider_payload=original_payload,
|
|
321
|
+
sent_provider_payload=current_payload,
|
|
322
|
+
cache_policy_metadata=self.prompt_cache_policy_metadata,
|
|
323
|
+
extra=extra or None,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
request_plan_metadata = _request_plan_metadata(payload, input_mode="full")
|
|
327
|
+
|
|
328
|
+
provider_key = self.provider_key or best_effort_provider_key(
|
|
329
|
+
base_url=self.base_url,
|
|
330
|
+
model=self.model,
|
|
331
|
+
)
|
|
332
|
+
telemetry = ProviderCallTelemetryRecorder(
|
|
333
|
+
provider_key=provider_key,
|
|
334
|
+
protocol="gemini_interactions",
|
|
335
|
+
model=self.model,
|
|
336
|
+
base_url=self.base_url,
|
|
337
|
+
stream=False,
|
|
338
|
+
tools=tools,
|
|
339
|
+
web_search_mode="off",
|
|
340
|
+
web_search_adapter="gemini_grounding",
|
|
341
|
+
native_web_search=False,
|
|
342
|
+
cache_policy=self.prompt_cache_policy_metadata,
|
|
343
|
+
request_plan=request_plan_metadata,
|
|
344
|
+
request_shape=build_request_shape_report(
|
|
345
|
+
messages=messages,
|
|
346
|
+
tools=tools,
|
|
347
|
+
cache_policy=self.prompt_cache_policy_metadata,
|
|
348
|
+
provider_payload=payload,
|
|
349
|
+
),
|
|
350
|
+
operation="gemini_interactions_chat",
|
|
351
|
+
)
|
|
352
|
+
telemetry_on_reasoning_delta = telemetry.wrap_reasoning_delta(on_reasoning_delta)
|
|
353
|
+
|
|
354
|
+
def _send_request() -> LLMResponse:
|
|
355
|
+
nonlocal request_plan_metadata
|
|
356
|
+
thinking_summaries_fallback_used = False
|
|
357
|
+
try:
|
|
358
|
+
with httpx.Client(timeout=self.timeout_s, transport=self._transport) as client:
|
|
359
|
+
while True:
|
|
360
|
+
response = client.post(
|
|
361
|
+
f"{self.base_url}/interactions",
|
|
362
|
+
headers=self._headers(),
|
|
363
|
+
json=payload,
|
|
364
|
+
)
|
|
365
|
+
if response.status_code < 400:
|
|
366
|
+
break
|
|
367
|
+
if (
|
|
368
|
+
not thinking_summaries_fallback_used
|
|
369
|
+
and _thinking_summaries_request_rejected(response)
|
|
370
|
+
and _remove_thinking_summaries_request(payload)
|
|
371
|
+
):
|
|
372
|
+
thinking_summaries_fallback_used = True
|
|
373
|
+
self._thinking_summaries_supported = False
|
|
374
|
+
request_plan_metadata = _request_plan_metadata(
|
|
375
|
+
payload,
|
|
376
|
+
input_mode="retry_without_thinking_summaries",
|
|
377
|
+
fallback_used=True,
|
|
378
|
+
)
|
|
379
|
+
telemetry.set_request_plan(request_plan_metadata)
|
|
380
|
+
telemetry.set_request_shape(
|
|
381
|
+
build_request_shape_report(
|
|
382
|
+
messages=messages,
|
|
383
|
+
tools=tools,
|
|
384
|
+
cache_policy=self.prompt_cache_policy_metadata,
|
|
385
|
+
provider_payload=payload,
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
continue
|
|
389
|
+
raise self._llm_error_from_response(response)
|
|
390
|
+
except Exception as exc: # noqa: BLE001
|
|
391
|
+
if isinstance(exc, LLMError):
|
|
392
|
+
raise
|
|
393
|
+
raise LLMError(
|
|
394
|
+
f"Gemini Interactions request failed: {sanitize_error_text_for_output(exc)}"
|
|
395
|
+
) from exc
|
|
396
|
+
return self._parse_chat_response(
|
|
397
|
+
response,
|
|
398
|
+
request_plan_metadata=request_plan_metadata,
|
|
399
|
+
on_reasoning_delta=telemetry_on_reasoning_delta,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return stamp_response_for_route(
|
|
403
|
+
telemetry.run(
|
|
404
|
+
lambda: run_provider_limited_call(
|
|
405
|
+
call=_send_request,
|
|
406
|
+
provider_key=provider_key,
|
|
407
|
+
provider_concurrency_caps=self.provider_concurrency_caps,
|
|
408
|
+
retry_settings=self.provider_retry_settings,
|
|
409
|
+
operation="gemini_interactions_chat",
|
|
410
|
+
sleep_fn=self._provider_sleep_fn,
|
|
411
|
+
random_fn=self._provider_random_fn,
|
|
412
|
+
on_retry=telemetry.on_retry,
|
|
413
|
+
on_retry_event=getattr(
|
|
414
|
+
self,
|
|
415
|
+
"_provider_retry_event_observer",
|
|
416
|
+
None,
|
|
417
|
+
),
|
|
418
|
+
retry_deadline_allows=getattr(self, "_provider_retry_deadline_allows", None),
|
|
419
|
+
)
|
|
420
|
+
),
|
|
421
|
+
self.route_identity,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def _parse_chat_response(
|
|
425
|
+
self,
|
|
426
|
+
response: httpx.Response,
|
|
427
|
+
request_plan_metadata: dict[str, Any] | None = None,
|
|
428
|
+
on_reasoning_delta: Callable[[str], None] | None = None,
|
|
429
|
+
) -> LLMResponse:
|
|
430
|
+
try:
|
|
431
|
+
data = response.json()
|
|
432
|
+
except Exception as exc:
|
|
433
|
+
raise LLMError(f"Gemini Interactions returned invalid JSON: {exc}") from exc
|
|
434
|
+
if not isinstance(data, dict):
|
|
435
|
+
raise LLMError("Gemini Interactions returned non-object JSON")
|
|
436
|
+
text = _interaction_output_text(data).strip()
|
|
437
|
+
reasoning = _interaction_thought_summaries(data)
|
|
438
|
+
actions = data.get("actions")
|
|
439
|
+
if isinstance(actions, list) and actions:
|
|
440
|
+
raise LLMError(
|
|
441
|
+
"Gemini Interactions returned actions, but this prototype only supports text output"
|
|
442
|
+
)
|
|
443
|
+
metadata: dict[str, Any] = {}
|
|
444
|
+
interaction_id = data.get("id") or data.get("interaction_id") or data.get("interactionId")
|
|
445
|
+
if isinstance(interaction_id, str) and interaction_id:
|
|
446
|
+
metadata["interaction_id"] = interaction_id
|
|
447
|
+
response_payload = data.get("response")
|
|
448
|
+
if isinstance(response_payload, dict):
|
|
449
|
+
metadata["response"] = copy.deepcopy(response_payload)
|
|
450
|
+
if request_plan_metadata:
|
|
451
|
+
metadata["request_plan"] = copy.deepcopy(request_plan_metadata)
|
|
452
|
+
|
|
453
|
+
parsed_response = LLMResponse(
|
|
454
|
+
content=text,
|
|
455
|
+
tool_calls=[],
|
|
456
|
+
raw=data,
|
|
457
|
+
response_model=_response_model(data) or self.model,
|
|
458
|
+
usage=_usage_from_response(data),
|
|
459
|
+
provider_metadata=(
|
|
460
|
+
{GEMINI_INTERACTIONS_PROVIDER_METADATA_KEY: metadata} if metadata else None
|
|
461
|
+
),
|
|
462
|
+
reasoning=reasoning,
|
|
463
|
+
)
|
|
464
|
+
if on_reasoning_delta is not None:
|
|
465
|
+
for item in reasoning:
|
|
466
|
+
on_reasoning_delta(item.text)
|
|
467
|
+
return parsed_response
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _thinking_summaries_request_rejected(response: httpx.Response) -> bool:
|
|
471
|
+
if response.status_code not in {400, 422}:
|
|
472
|
+
return False
|
|
473
|
+
detail = response.text.casefold()
|
|
474
|
+
names_summary_field = any(
|
|
475
|
+
marker in detail for marker in ("thinking_summaries", "thinking summaries")
|
|
476
|
+
)
|
|
477
|
+
rejects_field = any(
|
|
478
|
+
marker in detail
|
|
479
|
+
for marker in (
|
|
480
|
+
"unsupported",
|
|
481
|
+
"not supported",
|
|
482
|
+
"unknown field",
|
|
483
|
+
"unknown name",
|
|
484
|
+
"unrecognized",
|
|
485
|
+
"invalid field",
|
|
486
|
+
"invalid argument",
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
return names_summary_field and rejects_field
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _remove_thinking_summaries_request(payload: dict[str, Any]) -> bool:
|
|
493
|
+
generation_config = payload.get("generation_config")
|
|
494
|
+
if not isinstance(generation_config, dict) or "thinking_summaries" not in generation_config:
|
|
495
|
+
return False
|
|
496
|
+
generation_config.pop("thinking_summaries", None)
|
|
497
|
+
if not generation_config:
|
|
498
|
+
payload.pop("generation_config", None)
|
|
499
|
+
return True
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _gemini_interactions_base_url(base_url: str) -> str:
|
|
503
|
+
normalized = str(base_url or "").strip().rstrip("/")
|
|
504
|
+
if normalized.endswith("/openai"):
|
|
505
|
+
normalized = normalized.removesuffix("/openai")
|
|
506
|
+
return normalized or "https://generativelanguage.googleapis.com/v1beta"
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _last_user_text(messages: list[dict[str, Any]]) -> str:
|
|
510
|
+
for message in reversed(messages):
|
|
511
|
+
if not isinstance(message, dict):
|
|
512
|
+
continue
|
|
513
|
+
if str(message.get("role") or "").strip() != "user":
|
|
514
|
+
continue
|
|
515
|
+
text = _content_text(message.get("content"))
|
|
516
|
+
if text.strip():
|
|
517
|
+
return text.strip()
|
|
518
|
+
return ""
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _content_text(value: Any) -> str:
|
|
522
|
+
if value is None:
|
|
523
|
+
return ""
|
|
524
|
+
if isinstance(value, str):
|
|
525
|
+
return value
|
|
526
|
+
if isinstance(value, list):
|
|
527
|
+
return "".join(_content_text(item) for item in value)
|
|
528
|
+
if isinstance(value, dict):
|
|
529
|
+
text = value.get("text") or value.get("content")
|
|
530
|
+
return text if isinstance(text, str) else ""
|
|
531
|
+
return str(value)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _interaction_output_text(data: dict[str, Any]) -> str:
|
|
535
|
+
step_text = _interaction_model_output_text(data)
|
|
536
|
+
if step_text:
|
|
537
|
+
return step_text
|
|
538
|
+
output_text = data.get("output_text")
|
|
539
|
+
if isinstance(output_text, str):
|
|
540
|
+
return output_text
|
|
541
|
+
output = data.get("output")
|
|
542
|
+
if isinstance(output, str):
|
|
543
|
+
return output
|
|
544
|
+
text = _collect_text(output)
|
|
545
|
+
if text:
|
|
546
|
+
return text
|
|
547
|
+
text = _collect_text(data.get("outputs"))
|
|
548
|
+
if text:
|
|
549
|
+
return text
|
|
550
|
+
return _collect_text(data.get("response"))
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _interaction_model_output_text(data: dict[str, Any]) -> str:
|
|
554
|
+
steps = data.get("steps")
|
|
555
|
+
if not isinstance(steps, list):
|
|
556
|
+
return ""
|
|
557
|
+
parts: list[str] = []
|
|
558
|
+
for step in steps:
|
|
559
|
+
if not isinstance(step, dict) or step.get("type") != "model_output":
|
|
560
|
+
continue
|
|
561
|
+
content = step.get("content")
|
|
562
|
+
if not isinstance(content, list):
|
|
563
|
+
continue
|
|
564
|
+
for block in content:
|
|
565
|
+
if not isinstance(block, dict) or block.get("type") != "text":
|
|
566
|
+
continue
|
|
567
|
+
text = block.get("text")
|
|
568
|
+
if isinstance(text, str):
|
|
569
|
+
parts.append(text)
|
|
570
|
+
return "".join(parts)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _interaction_thought_summaries(data: dict[str, Any]) -> tuple[ReasoningOutput, ...]:
|
|
574
|
+
steps = data.get("steps")
|
|
575
|
+
if not isinstance(steps, list):
|
|
576
|
+
return ()
|
|
577
|
+
summaries: list[ReasoningOutput] = []
|
|
578
|
+
for step in steps:
|
|
579
|
+
if not isinstance(step, dict) or step.get("type") != "thought":
|
|
580
|
+
continue
|
|
581
|
+
summary = step.get("summary")
|
|
582
|
+
if not isinstance(summary, list):
|
|
583
|
+
continue
|
|
584
|
+
for block in summary:
|
|
585
|
+
if not isinstance(block, dict) or block.get("type") != "text":
|
|
586
|
+
continue
|
|
587
|
+
text = block.get("text")
|
|
588
|
+
if isinstance(text, str) and text.strip():
|
|
589
|
+
summaries.append(
|
|
590
|
+
ReasoningOutput(
|
|
591
|
+
text=text,
|
|
592
|
+
kind=ReasoningOutputKind.SUMMARY,
|
|
593
|
+
provider="gemini",
|
|
594
|
+
)
|
|
595
|
+
)
|
|
596
|
+
return tuple(summaries)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _collect_text(value: Any) -> str:
|
|
600
|
+
if value is None:
|
|
601
|
+
return ""
|
|
602
|
+
if isinstance(value, str):
|
|
603
|
+
return value
|
|
604
|
+
if isinstance(value, list):
|
|
605
|
+
return "".join(_collect_text(item) for item in value)
|
|
606
|
+
if isinstance(value, dict):
|
|
607
|
+
item_type = str(value.get("type") or "").strip().lower()
|
|
608
|
+
if item_type in {"thought", "thought_summary", "thought_signature"}:
|
|
609
|
+
return ""
|
|
610
|
+
parts: list[str] = []
|
|
611
|
+
text = value.get("text")
|
|
612
|
+
if isinstance(text, str):
|
|
613
|
+
parts.append(text)
|
|
614
|
+
for key in ("content", "parts", "candidates", "output", "response"):
|
|
615
|
+
child = value.get(key)
|
|
616
|
+
if child is not None:
|
|
617
|
+
parts.append(_collect_text(child))
|
|
618
|
+
return "".join(parts)
|
|
619
|
+
return ""
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _usage_from_response(data: dict[str, Any]) -> LLMUsage | None:
|
|
623
|
+
usage = data.get("usageMetadata") or data.get("usage_metadata") or data.get("usage")
|
|
624
|
+
if not isinstance(usage, dict):
|
|
625
|
+
return None
|
|
626
|
+
prompt_tokens = _int_or_none(
|
|
627
|
+
usage.get("promptTokenCount")
|
|
628
|
+
or usage.get("prompt_tokens")
|
|
629
|
+
or usage.get("input_tokens")
|
|
630
|
+
or usage.get("total_input_tokens")
|
|
631
|
+
)
|
|
632
|
+
completion_tokens = _int_or_none(
|
|
633
|
+
usage.get("candidatesTokenCount")
|
|
634
|
+
or usage.get("completion_tokens")
|
|
635
|
+
or usage.get("output_tokens")
|
|
636
|
+
or usage.get("total_output_tokens")
|
|
637
|
+
)
|
|
638
|
+
tool_use_prompt_tokens = _int_or_none(
|
|
639
|
+
usage.get("toolUsePromptTokenCount")
|
|
640
|
+
or usage.get("tool_use_prompt_token_count")
|
|
641
|
+
or usage.get("tool_use_prompt_tokens")
|
|
642
|
+
)
|
|
643
|
+
thoughts_tokens = _int_or_none(
|
|
644
|
+
usage.get("thoughtsTokenCount")
|
|
645
|
+
or usage.get("thoughts_token_count")
|
|
646
|
+
or usage.get("thinking_tokens")
|
|
647
|
+
or usage.get("total_thought_tokens")
|
|
648
|
+
)
|
|
649
|
+
total_tokens = _int_or_none(usage.get("totalTokenCount") or usage.get("total_tokens"))
|
|
650
|
+
cached_tokens = _int_or_none(
|
|
651
|
+
usage.get("cachedContentTokenCount")
|
|
652
|
+
or usage.get("cached_content_token_count")
|
|
653
|
+
or usage.get("cached_prompt_tokens")
|
|
654
|
+
)
|
|
655
|
+
# Gemini reports tool-use prompts separately from the ordinary prompt and
|
|
656
|
+
# candidates separately from thinking. Fold them into their billing sides;
|
|
657
|
+
# the untouched provider payload retains the original breakdown.
|
|
658
|
+
if tool_use_prompt_tokens:
|
|
659
|
+
prompt_tokens = (prompt_tokens or 0) + tool_use_prompt_tokens
|
|
660
|
+
if thoughts_tokens:
|
|
661
|
+
completion_tokens = (completion_tokens or 0) + thoughts_tokens
|
|
662
|
+
if (
|
|
663
|
+
prompt_tokens is None
|
|
664
|
+
and completion_tokens is None
|
|
665
|
+
and total_tokens is None
|
|
666
|
+
and cached_tokens is None
|
|
667
|
+
):
|
|
668
|
+
return None
|
|
669
|
+
input_tokens_uncached = None
|
|
670
|
+
if prompt_tokens is not None and cached_tokens is not None:
|
|
671
|
+
input_tokens_uncached = max(0, prompt_tokens - cached_tokens)
|
|
672
|
+
return LLMUsage(
|
|
673
|
+
prompt_tokens=prompt_tokens,
|
|
674
|
+
completion_tokens=completion_tokens,
|
|
675
|
+
total_tokens=total_tokens,
|
|
676
|
+
cached_prompt_tokens=cached_tokens,
|
|
677
|
+
input_tokens_uncached=input_tokens_uncached,
|
|
678
|
+
cache_read_input_tokens=cached_tokens,
|
|
679
|
+
reasoning_tokens=thoughts_tokens,
|
|
680
|
+
raw_provider_usage=copy.deepcopy(usage),
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _int_or_none(value: Any) -> int | None:
|
|
685
|
+
try:
|
|
686
|
+
return int(value)
|
|
687
|
+
except (TypeError, ValueError):
|
|
688
|
+
return None
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _response_model(data: dict[str, Any]) -> str | None:
|
|
692
|
+
for key in ("modelVersion", "model_version", "model"):
|
|
693
|
+
value = data.get(key)
|
|
694
|
+
if isinstance(value, str) and value.strip():
|
|
695
|
+
return value.strip()
|
|
696
|
+
response = data.get("response")
|
|
697
|
+
if isinstance(response, dict):
|
|
698
|
+
for key in ("modelVersion", "model_version", "model"):
|
|
699
|
+
value = response.get(key)
|
|
700
|
+
if isinstance(value, str) and value.strip():
|
|
701
|
+
return value.strip()
|
|
702
|
+
return None
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _validate_text_only_single_turn_history(messages: list[dict[str, Any]]) -> None:
|
|
706
|
+
user_message_count = 0
|
|
707
|
+
for message in messages:
|
|
708
|
+
if not isinstance(message, dict):
|
|
709
|
+
continue
|
|
710
|
+
role = str(message.get("role") or "").strip()
|
|
711
|
+
if role in {"system", "developer"}:
|
|
712
|
+
raise LLMError(
|
|
713
|
+
"Gemini Interactions prototype does not support system/developer instructions yet"
|
|
714
|
+
)
|
|
715
|
+
if role == "user":
|
|
716
|
+
user_message_count += 1
|
|
717
|
+
continue
|
|
718
|
+
raise LLMError(
|
|
719
|
+
"Gemini Interactions prototype supports only a single text-only user turn; "
|
|
720
|
+
"server-side previous_interaction_id continuation is not implemented yet"
|
|
721
|
+
)
|
|
722
|
+
if user_message_count != 1:
|
|
723
|
+
raise LLMError(
|
|
724
|
+
"Gemini Interactions prototype supports exactly one user message until "
|
|
725
|
+
"previous_interaction_id continuation is implemented"
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _extract_error_message(data: Any) -> str:
|
|
730
|
+
if isinstance(data, dict):
|
|
731
|
+
error = data.get("error")
|
|
732
|
+
if isinstance(error, dict):
|
|
733
|
+
message = error.get("message")
|
|
734
|
+
if isinstance(message, str) and message.strip():
|
|
735
|
+
return message.strip()
|
|
736
|
+
message = data.get("message")
|
|
737
|
+
if isinstance(message, str) and message.strip():
|
|
738
|
+
return message.strip()
|
|
739
|
+
return ""
|