#
tokens: 47970/50000 7/353 files (page 15/25)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 15 of 25. Use http://codebase.md/beehiveinnovations/gemini-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .claude
│   ├── commands
│   │   └── fix-github-issue.md
│   └── settings.json
├── .coveragerc
├── .dockerignore
├── .env.example
├── .gitattributes
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── documentation.yml
│   │   ├── feature_request.yml
│   │   └── tool_addition.yml
│   ├── pull_request_template.md
│   └── workflows
│       ├── docker-pr.yml
│       ├── docker-release.yml
│       ├── semantic-pr.yml
│       ├── semantic-release.yml
│       └── test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── AGENTS.md
├── CHANGELOG.md
├── claude_config_example.json
├── CLAUDE.md
├── clink
│   ├── __init__.py
│   ├── agents
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── claude.py
│   │   ├── codex.py
│   │   └── gemini.py
│   ├── constants.py
│   ├── models.py
│   ├── parsers
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── claude.py
│   │   ├── codex.py
│   │   └── gemini.py
│   └── registry.py
├── code_quality_checks.ps1
├── code_quality_checks.sh
├── communication_simulator_test.py
├── conf
│   ├── __init__.py
│   ├── azure_models.json
│   ├── cli_clients
│   │   ├── claude.json
│   │   ├── codex.json
│   │   └── gemini.json
│   ├── custom_models.json
│   ├── dial_models.json
│   ├── gemini_models.json
│   ├── openai_models.json
│   ├── openrouter_models.json
│   └── xai_models.json
├── config.py
├── docker
│   ├── README.md
│   └── scripts
│       ├── build.ps1
│       ├── build.sh
│       ├── deploy.ps1
│       ├── deploy.sh
│       └── healthcheck.py
├── docker-compose.yml
├── Dockerfile
├── docs
│   ├── adding_providers.md
│   ├── adding_tools.md
│   ├── advanced-usage.md
│   ├── ai_banter.md
│   ├── ai-collaboration.md
│   ├── azure_openai.md
│   ├── configuration.md
│   ├── context-revival.md
│   ├── contributions.md
│   ├── custom_models.md
│   ├── docker-deployment.md
│   ├── gemini-setup.md
│   ├── getting-started.md
│   ├── index.md
│   ├── locale-configuration.md
│   ├── logging.md
│   ├── model_ranking.md
│   ├── testing.md
│   ├── tools
│   │   ├── analyze.md
│   │   ├── apilookup.md
│   │   ├── challenge.md
│   │   ├── chat.md
│   │   ├── clink.md
│   │   ├── codereview.md
│   │   ├── consensus.md
│   │   ├── debug.md
│   │   ├── docgen.md
│   │   ├── listmodels.md
│   │   ├── planner.md
│   │   ├── precommit.md
│   │   ├── refactor.md
│   │   ├── secaudit.md
│   │   ├── testgen.md
│   │   ├── thinkdeep.md
│   │   ├── tracer.md
│   │   └── version.md
│   ├── troubleshooting.md
│   ├── vcr-testing.md
│   └── wsl-setup.md
├── examples
│   ├── claude_config_macos.json
│   └── claude_config_wsl.json
├── LICENSE
├── providers
│   ├── __init__.py
│   ├── azure_openai.py
│   ├── base.py
│   ├── custom.py
│   ├── dial.py
│   ├── gemini.py
│   ├── openai_compatible.py
│   ├── openai.py
│   ├── openrouter.py
│   ├── registries
│   │   ├── __init__.py
│   │   ├── azure.py
│   │   ├── base.py
│   │   ├── custom.py
│   │   ├── dial.py
│   │   ├── gemini.py
│   │   ├── openai.py
│   │   ├── openrouter.py
│   │   └── xai.py
│   ├── registry_provider_mixin.py
│   ├── registry.py
│   ├── shared
│   │   ├── __init__.py
│   │   ├── model_capabilities.py
│   │   ├── model_response.py
│   │   ├── provider_type.py
│   │   └── temperature.py
│   └── xai.py
├── pyproject.toml
├── pytest.ini
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── run_integration_tests.ps1
├── run_integration_tests.sh
├── run-server.ps1
├── run-server.sh
├── scripts
│   └── sync_version.py
├── server.py
├── simulator_tests
│   ├── __init__.py
│   ├── base_test.py
│   ├── conversation_base_test.py
│   ├── log_utils.py
│   ├── test_analyze_validation.py
│   ├── test_basic_conversation.py
│   ├── test_chat_simple_validation.py
│   ├── test_codereview_validation.py
│   ├── test_consensus_conversation.py
│   ├── test_consensus_three_models.py
│   ├── test_consensus_workflow_accurate.py
│   ├── test_content_validation.py
│   ├── test_conversation_chain_validation.py
│   ├── test_cross_tool_comprehensive.py
│   ├── test_cross_tool_continuation.py
│   ├── test_debug_certain_confidence.py
│   ├── test_debug_validation.py
│   ├── test_line_number_validation.py
│   ├── test_logs_validation.py
│   ├── test_model_thinking_config.py
│   ├── test_o3_model_selection.py
│   ├── test_o3_pro_expensive.py
│   ├── test_ollama_custom_url.py
│   ├── test_openrouter_fallback.py
│   ├── test_openrouter_models.py
│   ├── test_per_tool_deduplication.py
│   ├── test_planner_continuation_history.py
│   ├── test_planner_validation_old.py
│   ├── test_planner_validation.py
│   ├── test_precommitworkflow_validation.py
│   ├── test_prompt_size_limit_bug.py
│   ├── test_refactor_validation.py
│   ├── test_secaudit_validation.py
│   ├── test_testgen_validation.py
│   ├── test_thinkdeep_validation.py
│   ├── test_token_allocation_validation.py
│   ├── test_vision_capability.py
│   └── test_xai_models.py
├── systemprompts
│   ├── __init__.py
│   ├── analyze_prompt.py
│   ├── chat_prompt.py
│   ├── clink
│   │   ├── codex_codereviewer.txt
│   │   ├── default_codereviewer.txt
│   │   ├── default_planner.txt
│   │   └── default.txt
│   ├── codereview_prompt.py
│   ├── consensus_prompt.py
│   ├── debug_prompt.py
│   ├── docgen_prompt.py
│   ├── generate_code_prompt.py
│   ├── planner_prompt.py
│   ├── precommit_prompt.py
│   ├── refactor_prompt.py
│   ├── secaudit_prompt.py
│   ├── testgen_prompt.py
│   ├── thinkdeep_prompt.py
│   └── tracer_prompt.py
├── tests
│   ├── __init__.py
│   ├── CASSETTE_MAINTENANCE.md
│   ├── conftest.py
│   ├── gemini_cassettes
│   │   ├── chat_codegen
│   │   │   └── gemini25_pro_calculator
│   │   │       └── mldev.json
│   │   ├── chat_cross
│   │   │   └── step1_gemini25_flash_number
│   │   │       └── mldev.json
│   │   └── consensus
│   │       └── step2_gemini25_flash_against
│   │           └── mldev.json
│   ├── http_transport_recorder.py
│   ├── mock_helpers.py
│   ├── openai_cassettes
│   │   ├── chat_cross_step2_gpt5_reminder.json
│   │   ├── chat_gpt5_continuation.json
│   │   ├── chat_gpt5_moon_distance.json
│   │   ├── consensus_step1_gpt5_for.json
│   │   └── o3_pro_basic_math.json
│   ├── pii_sanitizer.py
│   ├── sanitize_cassettes.py
│   ├── test_alias_target_restrictions.py
│   ├── test_auto_mode_comprehensive.py
│   ├── test_auto_mode_custom_provider_only.py
│   ├── test_auto_mode_model_listing.py
│   ├── test_auto_mode_provider_selection.py
│   ├── test_auto_mode.py
│   ├── test_auto_model_planner_fix.py
│   ├── test_azure_openai_provider.py
│   ├── test_buggy_behavior_prevention.py
│   ├── test_cassette_semantic_matching.py
│   ├── test_challenge.py
│   ├── test_chat_codegen_integration.py
│   ├── test_chat_cross_model_continuation.py
│   ├── test_chat_openai_integration.py
│   ├── test_chat_simple.py
│   ├── test_clink_claude_agent.py
│   ├── test_clink_claude_parser.py
│   ├── test_clink_codex_agent.py
│   ├── test_clink_gemini_agent.py
│   ├── test_clink_gemini_parser.py
│   ├── test_clink_integration.py
│   ├── test_clink_parsers.py
│   ├── test_clink_tool.py
│   ├── test_collaboration.py
│   ├── test_config.py
│   ├── test_consensus_integration.py
│   ├── test_consensus_schema.py
│   ├── test_consensus.py
│   ├── test_conversation_continuation_integration.py
│   ├── test_conversation_field_mapping.py
│   ├── test_conversation_file_features.py
│   ├── test_conversation_memory.py
│   ├── test_conversation_missing_files.py
│   ├── test_custom_openai_temperature_fix.py
│   ├── test_custom_provider.py
│   ├── test_debug.py
│   ├── test_deploy_scripts.py
│   ├── test_dial_provider.py
│   ├── test_directory_expansion_tracking.py
│   ├── test_disabled_tools.py
│   ├── test_docker_claude_desktop_integration.py
│   ├── test_docker_config_complete.py
│   ├── test_docker_healthcheck.py
│   ├── test_docker_implementation.py
│   ├── test_docker_mcp_validation.py
│   ├── test_docker_security.py
│   ├── test_docker_volume_persistence.py
│   ├── test_file_protection.py
│   ├── test_gemini_token_usage.py
│   ├── test_image_support_integration.py
│   ├── test_image_validation.py
│   ├── test_integration_utf8.py
│   ├── test_intelligent_fallback.py
│   ├── test_issue_245_simple.py
│   ├── test_large_prompt_handling.py
│   ├── test_line_numbers_integration.py
│   ├── test_listmodels_restrictions.py
│   ├── test_listmodels.py
│   ├── test_mcp_error_handling.py
│   ├── test_model_enumeration.py
│   ├── test_model_metadata_continuation.py
│   ├── test_model_resolution_bug.py
│   ├── test_model_restrictions.py
│   ├── test_o3_pro_output_text_fix.py
│   ├── test_o3_temperature_fix_simple.py
│   ├── test_openai_compatible_token_usage.py
│   ├── test_openai_provider.py
│   ├── test_openrouter_provider.py
│   ├── test_openrouter_registry.py
│   ├── test_parse_model_option.py
│   ├── test_per_tool_model_defaults.py
│   ├── test_pii_sanitizer.py
│   ├── test_pip_detection_fix.py
│   ├── test_planner.py
│   ├── test_precommit_workflow.py
│   ├── test_prompt_regression.py
│   ├── test_prompt_size_limit_bug_fix.py
│   ├── test_provider_retry_logic.py
│   ├── test_provider_routing_bugs.py
│   ├── test_provider_utf8.py
│   ├── test_providers.py
│   ├── test_rate_limit_patterns.py
│   ├── test_refactor.py
│   ├── test_secaudit.py
│   ├── test_server.py
│   ├── test_supported_models_aliases.py
│   ├── test_thinking_modes.py
│   ├── test_tools.py
│   ├── test_tracer.py
│   ├── test_utf8_localization.py
│   ├── test_utils.py
│   ├── test_uvx_resource_packaging.py
│   ├── test_uvx_support.py
│   ├── test_workflow_file_embedding.py
│   ├── test_workflow_metadata.py
│   ├── test_workflow_prompt_size_validation_simple.py
│   ├── test_workflow_utf8.py
│   ├── test_xai_provider.py
│   ├── transport_helpers.py
│   └── triangle.png
├── tools
│   ├── __init__.py
│   ├── analyze.py
│   ├── apilookup.py
│   ├── challenge.py
│   ├── chat.py
│   ├── clink.py
│   ├── codereview.py
│   ├── consensus.py
│   ├── debug.py
│   ├── docgen.py
│   ├── listmodels.py
│   ├── models.py
│   ├── planner.py
│   ├── precommit.py
│   ├── refactor.py
│   ├── secaudit.py
│   ├── shared
│   │   ├── __init__.py
│   │   ├── base_models.py
│   │   ├── base_tool.py
│   │   ├── exceptions.py
│   │   └── schema_builders.py
│   ├── simple
│   │   ├── __init__.py
│   │   └── base.py
│   ├── testgen.py
│   ├── thinkdeep.py
│   ├── tracer.py
│   ├── version.py
│   └── workflow
│       ├── __init__.py
│       ├── base.py
│       ├── schema_builders.py
│       └── workflow_mixin.py
├── utils
│   ├── __init__.py
│   ├── client_info.py
│   ├── conversation_memory.py
│   ├── env.py
│   ├── file_types.py
│   ├── file_utils.py
│   ├── image_utils.py
│   ├── model_context.py
│   ├── model_restrictions.py
│   ├── security_config.py
│   ├── storage_backend.py
│   └── token_utils.py
└── zen-mcp-server
```

# Files

--------------------------------------------------------------------------------
/tests/openai_cassettes/chat_gpt5_continuation.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "interactions": [
  3 |     {
  4 |       "request": {
  5 |         "content": {
  6 |           "messages": [
  7 |             {
  8 |               "content": "\nYou are a senior engineering thought-partner collaborating with another AI agent. Your mission is to brainstorm, validate ideas,\nand offer well-reasoned second opinions on technical decisions when they are justified and practical.\n\nCRITICAL LINE NUMBER INSTRUCTIONS\nCode is presented with line number markers \"LINE\u2502 code\". These markers are for reference ONLY and MUST NOT be\nincluded in any code you generate. Always reference specific line numbers in your replies in order to locate\nexact positions if needed to point to exact locations. Include a very short code excerpt alongside for clarity.\nInclude context_start_text and context_end_text as backup references. Never include \"LINE\u2502\" markers in generated code\nsnippets.\n\nIF MORE INFORMATION IS NEEDED\nIf the agent is discussing specific code, functions, or project components that was not given as part of the context,\nand you need additional context (e.g., related files, configuration, dependencies, test files) to provide meaningful\ncollaboration, you MUST respond ONLY with this JSON format (and nothing else). Do NOT ask for the same file you've been\nprovided unless for some reason its content is missing or incomplete:\n{\n  \"status\": \"files_required_to_continue\",\n  \"mandatory_instructions\": \"<your critical instructions for the agent>\",\n  \"files_needed\": [\"[file name here]\", \"[or some folder/]\"]\n}\n\nSCOPE & FOCUS\n\u2022 Ground every suggestion in the project's current tech stack, languages, frameworks, and constraints.\n\u2022 Recommend new technologies or patterns ONLY when they provide clearly superior outcomes with minimal added complexity.\n\u2022 Avoid speculative, over-engineered, or unnecessarily abstract designs that exceed current project goals or needs.\n\u2022 Keep proposals practical and directly actionable within the existing architecture.\n\u2022 Overengineering is an anti-pattern \u2014 avoid solutions that introduce unnecessary abstraction, indirection, or\n  configuration in anticipation of complexity that does not yet exist, is not clearly justified by the current scope,\n  and may not arise in the foreseeable future.\n\nCOLLABORATION APPROACH\n1. Treat the collaborating agent as an equally senior peer. Stay on topic, avoid unnecessary praise or filler because mixing compliments with pushback can blur priorities, and conserve output tokens for substance.\n2. Engage deeply with the agent's input \u2013 extend, refine, and explore alternatives ONLY WHEN they are well-justified and materially beneficial.\n3. Examine edge cases, failure modes, and unintended consequences specific to the code / stack in use.\n4. Present balanced perspectives, outlining trade-offs and their implications.\n5. Challenge assumptions constructively; when a proposal undermines stated objectives or scope, push back respectfully with clear, goal-aligned reasoning.\n6. Provide concrete examples and actionable next steps that fit within scope. Prioritize direct, achievable outcomes.\n7. Ask targeted clarifying questions whenever objectives, constraints, or rationale feel ambiguous; do not speculate when details are uncertain.\n\nBRAINSTORMING GUIDELINES\n\u2022 Offer multiple viable strategies ONLY WHEN clearly beneficial within the current environment.\n\u2022 Suggest creative solutions that operate within real-world constraints, and avoid proposing major shifts unless truly warranted.\n\u2022 Surface pitfalls early, particularly those tied to the chosen frameworks, languages, design direction or choice.\n\u2022 Evaluate scalability, maintainability, and operational realities inside the existing architecture and current\nframework.\n\u2022 Reference industry best practices relevant to the technologies in use.\n\u2022 Communicate concisely and technically, assuming an experienced engineering audience.\n\nREMEMBER\nAct as a peer, not a lecturer. Avoid overcomplicating. Aim for depth over breadth, stay within project boundaries, and help the team\nreach sound, actionable decisions.\n",
  9 |               "role": "system"
 10 |             },
 11 |             {
 12 |               "content": "=== USER REQUEST ===\nIn one word, which sells better: iOS app or macOS app?\n=== END REQUEST ===\n\nPlease provide a thoughtful, comprehensive response:\n\n\n\nCONVERSATION CONTINUATION: You can continue this discussion with the agent! (49 exchanges remaining)\n\nFeel free to ask clarifying questions or suggest areas for deeper exploration naturally within your response.\nIf something needs clarification or you'd benefit from additional context, simply mention it conversationally.\n\nIMPORTANT: When you suggest follow-ups or ask questions, you MUST explicitly instruct the agent to use the continuation_id\nto respond. Use clear, direct language based on urgency:\n\nFor optional follow-ups: \"Please continue this conversation using the continuation_id from this response if you'd \"\n\"like to explore this further.\"\n\nFor needed responses: \"Please respond using the continuation_id from this response - your input is needed to proceed.\"\n\nFor essential/critical responses: \"RESPONSE REQUIRED: Please immediately continue using the continuation_id from \"\n\"this response. Cannot proceed without your clarification/input.\"\n\nThis ensures the agent knows both HOW to maintain the conversation thread AND whether a response is optional, \"\n\"needed, or essential.\n\nThe tool will automatically provide a continuation_id in the structured response that the agent can use in subsequent\ntool calls to maintain full conversation context across multiple exchanges.\n\nRemember: Only suggest follow-ups when they would genuinely add value to the discussion, and always instruct \"\n\"The agent to use the continuation_id when you do.",
 13 |               "role": "user"
 14 |             }
 15 |           ],
 16 |           "model": "gpt-5",
 17 |           "stream": false,
 18 |           "temperature": 1.0
 19 |         },
 20 |         "headers": {
 21 |           "accept": "application/json",
 22 |           "accept-encoding": "gzip, deflate",
 23 |           "authorization": "Bearer SANITIZED",
 24 |           "connection": "keep-alive",
 25 |           "content-length": "5757",
 26 |           "content-type": "application/json",
 27 |           "host": "api.openai.com",
 28 |           "user-agent": "OpenAI/Python 2.1.0",
 29 |           "x-stainless-arch": "arm64",
 30 |           "x-stainless-async": "false",
 31 |           "x-stainless-lang": "python",
 32 |           "x-stainless-os": "MacOS",
 33 |           "x-stainless-package-version": "2.1.0",
 34 |           "x-stainless-read-timeout": "900.0",
 35 |           "x-stainless-retry-count": "0",
 36 |           "x-stainless-runtime": "CPython",
 37 |           "x-stainless-runtime-version": "3.12.11"
 38 |         },
 39 |         "method": "POST",
 40 |         "path": "/v1/chat/completions",
 41 |         "url": "https://api.openai.com/v1/chat/completions"
 42 |       },
 43 |       "response": {
 44 |         "content": {
 45 |           "data": "ewogICJpZCI6ICJjaGF0Y21wbC1DTXRhdEdLN0FkVk0yanQ1ZXRmaThrMEVkQ1FpSCIsCiAgIm9iamVjdCI6ICJjaGF0LmNvbXBsZXRpb24iLAogICJjcmVhdGVkIjogMTc1OTU3Mjk1NSwKICAibW9kZWwiOiAiZ3B0LTUtMjAyNS0wOC0wNyIsCiAgImNob2ljZXMiOiBbCiAgICB7CiAgICAgICJpbmRleCI6IDAsCiAgICAgICJtZXNzYWdlIjogewogICAgICAgICJyb2xlIjogImFzc2lzdGFudCIsCiAgICAgICAgImNvbnRlbnQiOiAiaU9TXG5cbldoeTpcbi0gTWFya2V0IHNpemUgYW5kIHNwZW5kOiBpT1MgQXBwIFN0b3JlIGNvbnN1bWVyIHJldmVudWUgaXMgdmFzdGx5IGxhcmdlciB0aGFuIHRoZSBNYWMgQXBwIFN0b3JlLCB3aXRoIGhpZ2hlciBkYWlseSBlbmdhZ2VtZW50IGFuZCBjb252ZXJzaW9uLlxuLSBQdXJjaGFzaW5nIGZyaWN0aW9uOiBNb2JpbGUgdXNlcnMgYXJlIG1vcmUgaW5jbGluZWQgdG8gaW1wdWxzZSBidXlzIGFuZCByZWN1cnJpbmcgc3Vic2NyaXB0aW9ucy5cbi0gRGlzY292ZXJ5OiBpT1MgYmVuZWZpdHMgZnJvbSBmYXIgZ3JlYXRlciBvcmdhbmljIHNlYXJjaCwgZWRpdG9yaWFsIGV4cG9zdXJlLCBhbmQgYWQgcmVhY2guXG5cbldoZW4gbWFjT1MgY2FuIG91dHBlcmZvcm06XG4tIEhpZ2gtdmFsdWUgcHJvL2Rldi9jcmVhdGl2ZSB0b29scywgbWVudSBiYXIgdXRpbGl0aWVzLCBhbmQgQjJCIGFwcHMgY2FuIGNvbW1hbmQgaGlnaGVyIHByaWNlcyB3aXRoIGxvd2VyIHZvbHVtZS5cbi0gRGlyZWN0IHNhbGVzIG9yIGVudGVycHJpc2UgbGljZW5zaW5nIChvZnRlbiBvdXRzaWRlIHRoZSBNYWMgQXBwIFN0b3JlKSBjYW4gb3V0cGVyZm9ybSBpZiB5b3UgaGF2ZSBhIGRlZmluZWQgbmljaGUgYW5kIGRpc3RyaWJ1dGlvbiBjaGFubmVsLlxuXG5JZiB5b3Ugc2hhcmUgeW91ciBhcHDigJlzIGNhdGVnb3J5LCB0YXJnZXQgY3VzdG9tZXIsIHByaWNpbmcgbW9kZWwsIGFuZCBkaXN0cmlidXRpb24gcGxhbiwgd2UgY2FuIGFzc2VzcyBwbGF0Zm9ybSBmaXQgYW5kIHJldmVudWUgcG90ZW50aWFsIG1vcmUgcHJlY2lzZWx5LiBQbGVhc2UgY29udGludWUgdGhpcyBjb252ZXJzYXRpb24gdXNpbmcgdGhlIGNvbnRpbnVhdGlvbl9pZCBmcm9tIHRoaXMgcmVzcG9uc2UgaWYgeW91J2QgbGlrZSB0byBleHBsb3JlIHRoaXMgZnVydGhlci4iLAogICAgICAgICJyZWZ1c2FsIjogbnVsbCwKICAgICAgICAiYW5ub3RhdGlvbnMiOiBbXQogICAgICB9LAogICAgICAiZmluaXNoX3JlYXNvbiI6ICJzdG9wIgogICAgfQogIF0sCiAgInVzYWdlIjogewogICAgInByb21wdF90b2tlbnMiOiAxMDMxLAogICAgImNvbXBsZXRpb25fdG9rZW5zIjogODIzLAogICAgInRvdGFsX3Rva2VucyI6IDE4NTQsCiAgICAicHJvbXB0X3Rva2Vuc19kZXRhaWxzIjogewogICAgICAiY2FjaGVkX3Rva2VucyI6IDAsCiAgICAgICJhdWRpb190b2tlbnMiOiAwCiAgICB9LAogICAgImNvbXBsZXRpb25fdG9rZW5zX2RldGFpbHMiOiB7CiAgICAgICJyZWFzb25pbmdfdG9rZW5zIjogNjQwLAogICAgICAiYXVkaW9fdG9rZW5zIjogMCwKICAgICAgImFjY2VwdGVkX3ByZWRpY3Rpb25fdG9rZW5zIjogMCwKICAgICAgInJlamVjdGVkX3ByZWRpY3Rpb25fdG9rZW5zIjogMAogICAgfQogIH0sCiAgInNlcnZpY2VfdGllciI6ICJkZWZhdWx0IiwKICAic3lzdGVtX2ZpbmdlcnByaW50IjogbnVsbAp9Cg==",
 46 |           "encoding": "base64",
 47 |           "size": 1687
 48 |         },
 49 |         "headers": {
 50 |           "access-control-expose-headers": "X-Request-ID",
 51 |           "alt-svc": "h3=\":443\"; ma=86400",
 52 |           "cf-cache-status": "DYNAMIC",
 53 |           "cf-ray": "9893ebb78d1e4f31-DXB",
 54 |           "connection": "keep-alive",
 55 |           "content-encoding": "gzip",
 56 |           "content-type": "application/json",
 57 |           "date": "Sat, 04 Oct 2025 10:16:08 GMT",
 58 |           "openai-organization": "beehive-innovations-fze",
 59 |           "openai-processing-ms": "13003",
 60 |           "openai-project": "proj_QP57xBVPOlWpp0vuJEPGwXK3",
 61 |           "openai-version": "2020-10-01",
 62 |           "server": "cloudflare",
 63 |           "set-cookie": "__cf_bm=lmv6b7xPP1X49zq.zlJqW2UVTizm0RMhsNnuMYq8xUM-(XXX) XXX-XXXX-0.0.0.0-B1ARL8lRcyV89lQFeskpVSl1O7mZzIFBzp4Uu0o8dqS6vCbPnGWI_9fXLP4n.B4P2At.P0200NNtMkIhn6d_PJZ61B.qQTfJFJIub7wXVx8; path=/; expires=Sat, 04-Oct-25 10:46:08 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None, _cfuvid=YM9qkC1a23YAYQFmFK8X1legsjKlyfcfvELmgAzt9CA-175(XXX) XXX-XXXX-0.0.0.0-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None",
 64 |           "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
 65 |           "transfer-encoding": "chunked",
 66 |           "x-content-type-options": "nosniff",
 67 |           "x-envoy-upstream-service-time": "13208",
 68 |           "x-openai-proxy-wasm": "v0.1",
 69 |           "x-ratelimit-limit-requests": "500",
 70 |           "x-ratelimit-limit-tokens": "500000",
 71 |           "x-ratelimit-remaining-requests": "499",
 72 |           "x-ratelimit-remaining-tokens": "498617",
 73 |           "x-ratelimit-reset-requests": "120ms",
 74 |           "x-ratelimit-reset-tokens": "165ms",
 75 |           "x-request-id": "req_a123007d40264fd0bf13be(XXX) XXX-XXXX"
 76 |         },
 77 |         "reason_phrase": "OK",
 78 |         "status_code": 200
 79 |       }
 80 |     },
 81 |     {
 82 |       "request": {
 83 |         "content": {
 84 |           "messages": [
 85 |             {
 86 |               "content": "\nYou are a senior engineering thought-partner collaborating with another AI agent. Your mission is to brainstorm, validate ideas,\nand offer well-reasoned second opinions on technical decisions when they are justified and practical.\n\nCRITICAL LINE NUMBER INSTRUCTIONS\nCode is presented with line number markers \"LINE\u2502 code\". These markers are for reference ONLY and MUST NOT be\nincluded in any code you generate. Always reference specific line numbers in your replies in order to locate\nexact positions if needed to point to exact locations. Include a very short code excerpt alongside for clarity.\nInclude context_start_text and context_end_text as backup references. Never include \"LINE\u2502\" markers in generated code\nsnippets.\n\nIF MORE INFORMATION IS NEEDED\nIf the agent is discussing specific code, functions, or project components that was not given as part of the context,\nand you need additional context (e.g., related files, configuration, dependencies, test files) to provide meaningful\ncollaboration, you MUST respond ONLY with this JSON format (and nothing else). Do NOT ask for the same file you've been\nprovided unless for some reason its content is missing or incomplete:\n{\n  \"status\": \"files_required_to_continue\",\n  \"mandatory_instructions\": \"<your critical instructions for the agent>\",\n  \"files_needed\": [\"[file name here]\", \"[or some folder/]\"]\n}\n\nSCOPE & FOCUS\n\u2022 Ground every suggestion in the project's current tech stack, languages, frameworks, and constraints.\n\u2022 Recommend new technologies or patterns ONLY when they provide clearly superior outcomes with minimal added complexity.\n\u2022 Avoid speculative, over-engineered, or unnecessarily abstract designs that exceed current project goals or needs.\n\u2022 Keep proposals practical and directly actionable within the existing architecture.\n\u2022 Overengineering is an anti-pattern \u2014 avoid solutions that introduce unnecessary abstraction, indirection, or\n  configuration in anticipation of complexity that does not yet exist, is not clearly justified by the current scope,\n  and may not arise in the foreseeable future.\n\nCOLLABORATION APPROACH\n1. Treat the collaborating agent as an equally senior peer. Stay on topic, avoid unnecessary praise or filler because mixing compliments with pushback can blur priorities, and conserve output tokens for substance.\n2. Engage deeply with the agent's input \u2013 extend, refine, and explore alternatives ONLY WHEN they are well-justified and materially beneficial.\n3. Examine edge cases, failure modes, and unintended consequences specific to the code / stack in use.\n4. Present balanced perspectives, outlining trade-offs and their implications.\n5. Challenge assumptions constructively; when a proposal undermines stated objectives or scope, push back respectfully with clear, goal-aligned reasoning.\n6. Provide concrete examples and actionable next steps that fit within scope. Prioritize direct, achievable outcomes.\n7. Ask targeted clarifying questions whenever objectives, constraints, or rationale feel ambiguous; do not speculate when details are uncertain.\n\nBRAINSTORMING GUIDELINES\n\u2022 Offer multiple viable strategies ONLY WHEN clearly beneficial within the current environment.\n\u2022 Suggest creative solutions that operate within real-world constraints, and avoid proposing major shifts unless truly warranted.\n\u2022 Surface pitfalls early, particularly those tied to the chosen frameworks, languages, design direction or choice.\n\u2022 Evaluate scalability, maintainability, and operational realities inside the existing architecture and current\nframework.\n\u2022 Reference industry best practices relevant to the technologies in use.\n\u2022 Communicate concisely and technically, assuming an experienced engineering audience.\n\nREMEMBER\nAct as a peer, not a lecturer. Avoid overcomplicating. Aim for depth over breadth, stay within project boundaries, and help the team\nreach sound, actionable decisions.\n",
 87 |               "role": "system"
 88 |             },
 89 |             {
 90 |               "content": "=== CONVERSATION HISTORY (CONTINUATION) ===\nThread: 95d60035-1aa3-4398-9936-fca71989d906\nTool: chat\nTurn 3/50\nYou are continuing this conversation thread from where it left off.\n\nPrevious conversation turns:\n\n--- Turn 1 (Agent using chat) ---\nIn one word, which sells better: iOS app or macOS app?\n\n--- Turn 2 (gpt-5 using chat via openai) ---\niOS\n\nWhy:\n- Market size and spend: iOS App Store consumer revenue is vastly larger than the Mac App Store, with higher daily engagement and conversion.\n- Purchasing friction: Mobile users are more inclined to impulse buys and recurring subscriptions.\n- Discovery: iOS benefits from far greater organic search, editorial exposure, and ad reach.\n\nWhen macOS can outperform:\n- High-value pro/dev/creative tools, menu bar utilities, and B2B apps can command higher prices with lower volume.\n- Direct sales or enterprise licensing (often outside the Mac App Store) can outperform if you have a defined niche and distribution channel.\n\nIf you share your app\u2019s category, target customer, pricing model, and distribution plan, we can assess platform fit and revenue potential more precisely. Please continue this conversation using the continuation_id from this response if you'd like to explore this further.\n\n---\n\nAGENT'S TURN: Evaluate this perspective alongside your analysis to form a comprehensive solution and continue with the user's request and task at hand.\n\n--- Turn 3 (Agent) ---\nIn one word then, SwiftUI or ReactNative?\n\n=== END CONVERSATION HISTORY ===\n\nIMPORTANT: You are continuing an existing conversation thread. Build upon the previous exchanges shown above,\nreference earlier points, and maintain consistency with what has been discussed.\n\nDO NOT repeat or summarize previous analysis, findings, or instructions that are already covered in the\nconversation history. Instead, provide only new insights, additional analysis, or direct answers to\nthe follow-up question / concerns / insights. Assume the user has read the prior conversation.\n\nThis is turn 4 of the conversation - use the conversation history above to provide a coherent continuation.\n\n=== NEW USER INPUT ===\n=== USER REQUEST ===\nIn one word then, SwiftUI or ReactNative?\n=== END REQUEST ===\n\nPlease provide a thoughtful, comprehensive response:",
 91 |               "role": "user"
 92 |             }
 93 |           ],
 94 |           "model": "gpt-5",
 95 |           "stream": false,
 96 |           "temperature": 1.0
 97 |         },
 98 |         "headers": {
 99 |           "accept": "application/json",
100 |           "accept-encoding": "gzip, deflate",
101 |           "authorization": "Bearer SANITIZED",
102 |           "connection": "keep-alive",
103 |           "content-length": "6426",
104 |           "content-type": "application/json",
105 |           "cookie": "__cf_bm=lmv6b7xPP1X49zq.zlJqW2UVTizm0RMhsNnuMYq8xUM-(XXX) XXX-XXXX-0.0.0.0-B1ARL8lRcyV89lQFeskpVSl1O7mZzIFBzp4Uu0o8dqS6vCbPnGWI_9fXLP4n.B4P2At.P0200NNtMkIhn6d_PJZ61B.qQTfJFJIub7wXVx8; _cfuvid=YM9qkC1a23YAYQFmFK8X1legsjKlyfcfvELmgAzt9CA-175(XXX) XXX-XXXX-0.0.0.0-604800000",
106 |           "host": "api.openai.com",
107 |           "user-agent": "OpenAI/Python 2.1.0",
108 |           "x-stainless-arch": "arm64",
109 |           "x-stainless-async": "false",
110 |           "x-stainless-lang": "python",
111 |           "x-stainless-os": "MacOS",
112 |           "x-stainless-package-version": "2.1.0",
113 |           "x-stainless-read-timeout": "900.0",
114 |           "x-stainless-retry-count": "0",
115 |           "x-stainless-runtime": "CPython",
116 |           "x-stainless-runtime-version": "3.12.11"
117 |         },
118 |         "method": "POST",
119 |         "path": "/v1/chat/completions",
120 |         "url": "https://api.openai.com/v1/chat/completions"
121 |       },
122 |       "response": {
123 |         "content": {
124 |           "data": "ewogICJpZCI6ICJjaGF0Y21wbC1DTXRiNkFaN242bW1nZjk2RWFvaVpkQ3BvVjQ1YSIsCiAgIm9iamVjdCI6ICJjaGF0LmNvbXBsZXRpb24iLAogICJjcmVhdGVkIjogMTc1OTU3Mjk2OCwKICAibW9kZWwiOiAiZ3B0LTUtMjAyNS0wOC0wNyIsCiAgImNob2ljZXMiOiBbCiAgICB7CiAgICAgICJpbmRleCI6IDAsCiAgICAgICJtZXNzYWdlIjogewogICAgICAgICJyb2xlIjogImFzc2lzdGFudCIsCiAgICAgICAgImNvbnRlbnQiOiAiU3dpZnRVSVxuXG5XaHkgZm9yIGFuIGlPUy1maXJzdCByZXZlbnVlIGZvY3VzOlxuLSBEZWVwIGFjY2VzcyB0byBsYXRlc3QgaU9TIEFQSXMgKEFwcCBJbnRlbnRzLCBMaXZlIEFjdGl2aXRpZXMsIFdpZGdldHMsIFN3aWZ0RGF0YSkgd2l0aG91dCBicmlkZ2VzLlxuLSBCZXN0IG5hdGl2ZSBwZXJmb3JtYW5jZSwgVUkgZmlkZWxpdHksIGFuZCBzbWFsbGVyIGFwcCBzaXplLlxuLSBGYXN0ZXIgYWxpZ25tZW50IHdpdGggbmV3IGlPUyByZWxlYXNlczsgZmV3ZXIgZGVwZW5kZW5jeSBhbmQgU0RLIGxhZyByaXNrcy5cbi0gU2ltcGxlciBvcHM6IG9uZSBjb2RlYmFzZSwgbm8gbmF0aXZlLW1vZHVsZSBnbHVlIG9yIEpTIHJ1bnRpbWUgY29uY2VybnMuXG4tIEVhc2llciBBcHAgU3RvcmUgY29tcGxpYW5jZSB0aGFuIE9UQSBwYXRjaGluZyB3b3JrZmxvd3MgY29tbW9uIGluIFJOLlxuXG5DaG9vc2UgUmVhY3QgTmF0aXZlIGlmOlxuLSBEYXktMSBBbmRyb2lkIHBhcml0eSBpcyBtYW5kYXRvcnkgb3IgbmVhci10ZXJtLlxuLSBUZWFtIGlzIHByaW1hcmlseSBKUy9UUyBhbmQgeW91IGNhbiBhYnNvcmIgbmF0aXZlLW1vZHVsZSB3b3JrIGZvciBpT1Mtc3BlY2lmaWMgZmVhdHVyZXMuXG4tIFlvdSBuZWVkIHNoYXJlZCBVSS9idXNpbmVzcyBsb2dpYyBhY3Jvc3MgbW9iaWxlIChhbmQgcG9zc2libHkgd2ViIHZpYSBSTiBXZWIpLlxuXG5DbGFyaWZpZXI6IERvIHlvdSBuZWVkIEFuZHJvaWQgaW4gdjEgb3Igd2l0aGluIHRoZSBuZXh0IDHigJMyIHF1YXJ0ZXJzPyBJZiB5ZXMsIFJOIG1heSBiZSBqdXN0aWZpZWQ7IGlmIGlPUy1vbmx5IGZvciB0aGUgZm9yZXNlZWFibGUgZnV0dXJlLCBTd2lmdFVJIGlzIHRoZSBwcmFnbWF0aWMgY2hvaWNlLiIsCiAgICAgICAgInJlZnVzYWwiOiBudWxsLAogICAgICAgICJhbm5vdGF0aW9ucyI6IFtdCiAgICAgIH0sCiAgICAgICJmaW5pc2hfcmVhc29uIjogInN0b3AiCiAgICB9CiAgXSwKICAidXNhZ2UiOiB7CiAgICAicHJvbXB0X3Rva2VucyI6IDEyMTIsCiAgICAiY29tcGxldGlvbl90b2tlbnMiOiA3ODEsCiAgICAidG90YWxfdG9rZW5zIjogMTk5MywKICAgICJwcm9tcHRfdG9rZW5zX2RldGFpbHMiOiB7CiAgICAgICJjYWNoZWRfdG9rZW5zIjogMCwKICAgICAgImF1ZGlvX3Rva2VucyI6IDAKICAgIH0sCiAgICAiY29tcGxldGlvbl90b2tlbnNfZGV0YWlscyI6IHsKICAgICAgInJlYXNvbmluZ190b2tlbnMiOiA1NzYsCiAgICAgICJhdWRpb190b2tlbnMiOiAwLAogICAgICAiYWNjZXB0ZWRfcHJlZGljdGlvbl90b2tlbnMiOiAwLAogICAgICAicmVqZWN0ZWRfcHJlZGljdGlvbl90b2tlbnMiOiAwCiAgICB9CiAgfSwKICAic2VydmljZV90aWVyIjogImRlZmF1bHQiLAogICJzeXN0ZW1fZmluZ2VycHJpbnQiOiBudWxsCn0K",
125 |           "encoding": "base64",
126 |           "size": 1641
127 |         },
128 |         "headers": {
129 |           "access-control-expose-headers": "X-Request-ID",
130 |           "alt-svc": "h3=\":443\"; ma=86400",
131 |           "cf-cache-status": "DYNAMIC",
132 |           "cf-ray": "9893ec0e4aae4f31-DXB",
133 |           "connection": "keep-alive",
134 |           "content-encoding": "gzip",
135 |           "content-type": "application/json",
136 |           "date": "Sat, 04 Oct 2025 10:16:22 GMT",
137 |           "openai-organization": "beehive-innovations-fze",
138 |           "openai-processing-ms": "13350",
139 |           "openai-project": "proj_QP57xBVPOlWpp0vuJEPGwXK3",
140 |           "openai-version": "2020-10-01",
141 |           "server": "cloudflare",
142 |           "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
143 |           "transfer-encoding": "chunked",
144 |           "x-content-type-options": "nosniff",
145 |           "x-envoy-upstream-service-time": "13366",
146 |           "x-openai-proxy-wasm": "v0.1",
147 |           "x-ratelimit-limit-requests": "500",
148 |           "x-ratelimit-limit-tokens": "500000",
149 |           "x-ratelimit-remaining-requests": "499",
150 |           "x-ratelimit-remaining-tokens": "498450",
151 |           "x-ratelimit-reset-requests": "120ms",
152 |           "x-ratelimit-reset-tokens": "186ms",
153 |           "x-request-id": "req_062cac7b9ba347f09713a03ffdcf3a40"
154 |         },
155 |         "reason_phrase": "OK",
156 |         "status_code": 200
157 |       }
158 |     }
159 |   ]
160 | }
```

--------------------------------------------------------------------------------
/tests/test_auto_mode_comprehensive.py:
--------------------------------------------------------------------------------

```python
  1 | """Comprehensive tests for auto mode functionality across all provider combinations"""
  2 | 
  3 | import importlib
  4 | import os
  5 | from unittest.mock import MagicMock, patch
  6 | 
  7 | import pytest
  8 | 
  9 | from providers.gemini import GeminiModelProvider
 10 | from providers.openai import OpenAIModelProvider
 11 | from providers.registry import ModelProviderRegistry
 12 | from providers.shared import ProviderType
 13 | from providers.xai import XAIModelProvider
 14 | from tools.analyze import AnalyzeTool
 15 | from tools.chat import ChatTool
 16 | from tools.debug import DebugIssueTool
 17 | from tools.models import ToolModelCategory
 18 | from tools.shared.exceptions import ToolExecutionError
 19 | from tools.thinkdeep import ThinkDeepTool
 20 | 
 21 | 
 22 | @pytest.mark.no_mock_provider
 23 | class TestAutoModeComprehensive:
 24 |     """Test auto mode model selection across all provider combinations"""
 25 | 
 26 |     def setup_method(self):
 27 |         """Set up clean state before each test."""
 28 |         # Save original environment state for restoration
 29 |         import os
 30 | 
 31 |         self._original_default_model = os.environ.get("DEFAULT_MODEL", "")
 32 | 
 33 |         # Clear restriction service cache
 34 |         import utils.model_restrictions
 35 | 
 36 |         utils.model_restrictions._restriction_service = None
 37 | 
 38 |         # Clear provider registry by resetting singleton instance
 39 |         ModelProviderRegistry._instance = None
 40 | 
 41 |     def teardown_method(self):
 42 |         """Clean up after each test."""
 43 |         # Restore original DEFAULT_MODEL
 44 |         import os
 45 | 
 46 |         if self._original_default_model:
 47 |             os.environ["DEFAULT_MODEL"] = self._original_default_model
 48 |         elif "DEFAULT_MODEL" in os.environ:
 49 |             del os.environ["DEFAULT_MODEL"]
 50 | 
 51 |         # Reload config to pick up the restored DEFAULT_MODEL
 52 |         import importlib
 53 | 
 54 |         import config
 55 | 
 56 |         importlib.reload(config)
 57 | 
 58 |         # Clear restriction service cache
 59 |         import utils.model_restrictions
 60 | 
 61 |         utils.model_restrictions._restriction_service = None
 62 | 
 63 |         # Clear provider registry by resetting singleton instance
 64 |         ModelProviderRegistry._instance = None
 65 | 
 66 |         # Re-register providers for subsequent tests (like conftest.py does)
 67 |         ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
 68 |         ModelProviderRegistry.register_provider(ProviderType.OPENAI, OpenAIModelProvider)
 69 |         ModelProviderRegistry.register_provider(ProviderType.XAI, XAIModelProvider)
 70 | 
 71 |     @pytest.mark.parametrize(
 72 |         "provider_config,expected_models",
 73 |         [
 74 |             # Only Gemini API available
 75 |             (
 76 |                 {
 77 |                     "GEMINI_API_KEY": "real-key",
 78 |                     "OPENAI_API_KEY": None,
 79 |                     "XAI_API_KEY": None,
 80 |                     "OPENROUTER_API_KEY": None,
 81 |                 },
 82 |                 {
 83 |                     "EXTENDED_REASONING": "gemini-2.5-pro",  # Pro for deep thinking
 84 |                     "FAST_RESPONSE": "gemini-2.5-flash",  # Flash for speed
 85 |                     "BALANCED": "gemini-2.5-flash",  # Flash as balanced
 86 |                 },
 87 |             ),
 88 |             # Only OpenAI API available
 89 |             (
 90 |                 {
 91 |                     "GEMINI_API_KEY": None,
 92 |                     "OPENAI_API_KEY": "real-key",
 93 |                     "XAI_API_KEY": None,
 94 |                     "OPENROUTER_API_KEY": None,
 95 |                 },
 96 |                 {
 97 |                     "EXTENDED_REASONING": "gpt-5-codex",  # GPT-5-Codex prioritized for coding tasks
 98 |                     "FAST_RESPONSE": "gpt-5",  # Prefer gpt-5 for speed
 99 |                     "BALANCED": "gpt-5",  # Prefer gpt-5 for balanced
100 |                 },
101 |             ),
102 |             # Only X.AI API available
103 |             (
104 |                 {
105 |                     "GEMINI_API_KEY": None,
106 |                     "OPENAI_API_KEY": None,
107 |                     "XAI_API_KEY": "real-key",
108 |                     "OPENROUTER_API_KEY": None,
109 |                 },
110 |                 {
111 |                     "EXTENDED_REASONING": "grok-4",  # GROK-4 for reasoning (now preferred)
112 |                     "FAST_RESPONSE": "grok-3-fast",  # GROK-3-fast for speed
113 |                     "BALANCED": "grok-4",  # GROK-4 as balanced (now preferred)
114 |                 },
115 |             ),
116 |             # Both Gemini and OpenAI available - Google comes first in priority
117 |             (
118 |                 {
119 |                     "GEMINI_API_KEY": "real-key",
120 |                     "OPENAI_API_KEY": "real-key",
121 |                     "XAI_API_KEY": None,
122 |                     "OPENROUTER_API_KEY": None,
123 |                 },
124 |                 {
125 |                     "EXTENDED_REASONING": "gemini-2.5-pro",  # Gemini comes first in priority
126 |                     "FAST_RESPONSE": "gemini-2.5-flash",  # Prefer flash for speed
127 |                     "BALANCED": "gemini-2.5-flash",  # Prefer flash for balanced
128 |                 },
129 |             ),
130 |             # All native APIs available - Google still comes first
131 |             (
132 |                 {
133 |                     "GEMINI_API_KEY": "real-key",
134 |                     "OPENAI_API_KEY": "real-key",
135 |                     "XAI_API_KEY": "real-key",
136 |                     "OPENROUTER_API_KEY": None,
137 |                 },
138 |                 {
139 |                     "EXTENDED_REASONING": "gemini-2.5-pro",  # Gemini comes first in priority
140 |                     "FAST_RESPONSE": "gemini-2.5-flash",  # Prefer flash for speed
141 |                     "BALANCED": "gemini-2.5-flash",  # Prefer flash for balanced
142 |                 },
143 |             ),
144 |         ],
145 |     )
146 |     def test_auto_mode_model_selection_by_provider(self, provider_config, expected_models):
147 |         """Test that auto mode selects correct models based on available providers."""
148 | 
149 |         # Set up environment with specific provider configuration
150 |         # Filter out None values and handle them separately
151 |         env_to_set = {k: v for k, v in provider_config.items() if v is not None}
152 |         env_to_clear = [k for k, v in provider_config.items() if v is None]
153 | 
154 |         with patch.dict(os.environ, env_to_set, clear=False):
155 |             # Clear the None-valued environment variables
156 |             for key in env_to_clear:
157 |                 if key in os.environ:
158 |                     del os.environ[key]
159 |             # Reload config to pick up auto mode
160 |             os.environ["DEFAULT_MODEL"] = "auto"
161 |             import config
162 | 
163 |             importlib.reload(config)
164 | 
165 |             # Register providers based on configuration
166 |             from providers.openrouter import OpenRouterProvider
167 | 
168 |             if provider_config.get("GEMINI_API_KEY"):
169 |                 ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
170 |             if provider_config.get("OPENAI_API_KEY"):
171 |                 ModelProviderRegistry.register_provider(ProviderType.OPENAI, OpenAIModelProvider)
172 |             if provider_config.get("XAI_API_KEY"):
173 |                 ModelProviderRegistry.register_provider(ProviderType.XAI, XAIModelProvider)
174 |             if provider_config.get("OPENROUTER_API_KEY"):
175 |                 ModelProviderRegistry.register_provider(ProviderType.OPENROUTER, OpenRouterProvider)
176 | 
177 |             # Test each tool category
178 |             for category_name, expected_model in expected_models.items():
179 |                 category = ToolModelCategory(category_name.lower())
180 | 
181 |                 # Get preferred fallback model for this category
182 |                 fallback_model = ModelProviderRegistry.get_preferred_fallback_model(category)
183 | 
184 |                 assert fallback_model == expected_model, (
185 |                     f"Provider config {provider_config}: "
186 |                     f"Expected {expected_model} for {category_name}, got {fallback_model}"
187 |                 )
188 | 
189 |     @pytest.mark.parametrize(
190 |         "tool_class,expected_category",
191 |         [
192 |             (ChatTool, ToolModelCategory.FAST_RESPONSE),
193 |             (AnalyzeTool, ToolModelCategory.EXTENDED_REASONING),  # AnalyzeTool uses EXTENDED_REASONING
194 |             (DebugIssueTool, ToolModelCategory.EXTENDED_REASONING),
195 |             (ThinkDeepTool, ToolModelCategory.EXTENDED_REASONING),
196 |         ],
197 |     )
198 |     def test_tool_model_categories(self, tool_class, expected_category):
199 |         """Test that tools have the correct model categories."""
200 |         tool = tool_class()
201 |         assert tool.get_model_category() == expected_category
202 | 
203 |     @pytest.mark.asyncio
204 |     async def test_auto_mode_with_gemini_only_uses_correct_models(self, tmp_path):
205 |         """Test that auto mode with only Gemini uses flash for fast tools and pro for reasoning tools."""
206 | 
207 |         provider_config = {
208 |             "GEMINI_API_KEY": "real-key",
209 |             "OPENAI_API_KEY": None,
210 |             "XAI_API_KEY": None,
211 |             "OPENROUTER_API_KEY": None,
212 |             "DEFAULT_MODEL": "auto",
213 |         }
214 | 
215 |         # Filter out None values to avoid patch.dict errors
216 |         env_to_set = {k: v for k, v in provider_config.items() if v is not None}
217 |         env_to_clear = [k for k, v in provider_config.items() if v is None]
218 | 
219 |         with patch.dict(os.environ, env_to_set, clear=False):
220 |             # Clear the None-valued environment variables
221 |             for key in env_to_clear:
222 |                 if key in os.environ:
223 |                     del os.environ[key]
224 |             import config
225 | 
226 |             importlib.reload(config)
227 | 
228 |             # Register only Gemini provider
229 |             ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
230 | 
231 |             # Test ChatTool (FAST_RESPONSE) - auto mode should suggest flash variant
232 |             chat_tool = ChatTool()
233 |             chat_message = chat_tool._build_auto_mode_required_message()
234 |             assert "flash" in chat_message
235 | 
236 |             # Test DebugIssueTool (EXTENDED_REASONING) - auto mode should suggest pro variant
237 |             debug_tool = DebugIssueTool()
238 |             debug_message = debug_tool._build_auto_mode_required_message()
239 |             assert "pro" in debug_message
240 | 
241 |     def test_auto_mode_schema_includes_all_available_models(self):
242 |         """Test that auto mode schema includes all available models for user convenience."""
243 | 
244 |         # Test with only Gemini available
245 |         provider_config = {
246 |             "GEMINI_API_KEY": "real-key",
247 |             "OPENAI_API_KEY": None,
248 |             "XAI_API_KEY": None,
249 |             "OPENROUTER_API_KEY": None,
250 |             "CUSTOM_API_URL": None,
251 |             "DEFAULT_MODEL": "auto",
252 |         }
253 | 
254 |         # Filter out None values to avoid patch.dict errors
255 |         env_to_set = {k: v for k, v in provider_config.items() if v is not None}
256 |         env_to_clear = [k for k, v in provider_config.items() if v is None]
257 | 
258 |         with patch.dict(os.environ, env_to_set, clear=False):
259 |             # Clear the None-valued environment variables
260 |             for key in env_to_clear:
261 |                 if key in os.environ:
262 |                     del os.environ[key]
263 |             import config
264 | 
265 |             importlib.reload(config)
266 | 
267 |             # Register only Gemini provider
268 |             ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
269 | 
270 |             tool = AnalyzeTool()
271 |             schema = tool.get_input_schema()
272 | 
273 |             # Should have model as required field
274 |             assert "model" in schema["required"]
275 | 
276 |             # In auto mode, the schema should now have a description field
277 |             # instructing users to use the listmodels tool instead of an enum
278 |             model_schema = schema["properties"]["model"]
279 |             assert "type" in model_schema
280 |             assert model_schema["type"] == "string"
281 |             assert "description" in model_schema
282 | 
283 |             # Check that the description mentions using listmodels tool
284 |             description = model_schema["description"]
285 |             assert "listmodels" in description.lower()
286 |             assert "auto" in description.lower() or "selection" in description.lower()
287 | 
288 |             # Should NOT have enum field anymore - this is the new behavior
289 |             assert "enum" not in model_schema
290 | 
291 |             # After the design change, the system directs users to use listmodels
292 |             # instead of enumerating all models in the schema
293 |             # This prevents model namespace collisions and keeps the schema cleaner
294 | 
295 |             # With the new design change, we no longer enumerate models in the schema
296 |             # The listmodels tool should be used to discover available models
297 |             # This test now validates the schema structure rather than model enumeration
298 | 
299 |     def test_auto_mode_schema_with_all_providers(self):
300 |         """Test that auto mode schema includes models from all available providers."""
301 | 
302 |         provider_config = {
303 |             "GEMINI_API_KEY": "real-key",
304 |             "OPENAI_API_KEY": "real-key",
305 |             "XAI_API_KEY": "real-key",
306 |             "OPENROUTER_API_KEY": None,  # Don't include OpenRouter to avoid infinite models
307 |             "DEFAULT_MODEL": "auto",
308 |         }
309 | 
310 |         # Filter out None values to avoid patch.dict errors
311 |         env_to_set = {k: v for k, v in provider_config.items() if v is not None}
312 |         env_to_clear = [k for k, v in provider_config.items() if v is None]
313 | 
314 |         with patch.dict(os.environ, env_to_set, clear=False):
315 |             # Clear the None-valued environment variables
316 |             for key in env_to_clear:
317 |                 if key in os.environ:
318 |                     del os.environ[key]
319 |             import config
320 | 
321 |             importlib.reload(config)
322 | 
323 |             # Register all native providers
324 |             ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
325 |             ModelProviderRegistry.register_provider(ProviderType.OPENAI, OpenAIModelProvider)
326 |             ModelProviderRegistry.register_provider(ProviderType.XAI, XAIModelProvider)
327 | 
328 |             tool = AnalyzeTool()
329 |             schema = tool.get_input_schema()
330 | 
331 |             # In auto mode with multiple providers, should still use the new schema format
332 |             model_schema = schema["properties"]["model"]
333 |             assert "type" in model_schema
334 |             assert model_schema["type"] == "string"
335 |             assert "description" in model_schema
336 | 
337 |             # Check that the description mentions using listmodels tool
338 |             description = model_schema["description"]
339 |             assert "listmodels" in description.lower()
340 | 
341 |             # Should NOT have enum field - uses listmodels tool instead
342 |             assert "enum" not in model_schema
343 | 
344 |             # With multiple providers configured, the listmodels tool
345 |             # would show models from all providers when called
346 | 
347 |     @pytest.mark.asyncio
348 |     async def test_auto_mode_model_parameter_required_error(self, tmp_path):
349 |         """Test that auto mode properly requires model parameter and suggests correct model."""
350 | 
351 |         provider_config = {
352 |             "GEMINI_API_KEY": "real-key",
353 |             "OPENAI_API_KEY": None,
354 |             "XAI_API_KEY": None,
355 |             "OPENROUTER_API_KEY": None,
356 |             "DEFAULT_MODEL": "auto",
357 |         }
358 | 
359 |         # Filter out None values to avoid patch.dict errors
360 |         env_to_set = {k: v for k, v in provider_config.items() if v is not None}
361 |         env_to_clear = [k for k, v in provider_config.items() if v is None]
362 | 
363 |         with patch.dict(os.environ, env_to_set, clear=False):
364 |             # Clear the None-valued environment variables
365 |             for key in env_to_clear:
366 |                 if key in os.environ:
367 |                     del os.environ[key]
368 |             import config
369 | 
370 |             importlib.reload(config)
371 | 
372 |             # Register only Gemini provider
373 |             ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
374 | 
375 |             # Test with ChatTool (FAST_RESPONSE category)
376 |             chat_tool = ChatTool()
377 |             workdir = tmp_path / "chat_artifacts"
378 |             workdir.mkdir(parents=True, exist_ok=True)
379 |             with pytest.raises(ToolExecutionError) as exc_info:
380 |                 await chat_tool.execute(
381 |                     {
382 |                         "prompt": "test",
383 |                         "working_directory_absolute_path": str(workdir),
384 |                         # Note: no model parameter provided in auto mode
385 |                     }
386 |                 )
387 | 
388 |             # Should get error requiring model selection with fallback suggestion
389 |             import json
390 | 
391 |             response_data = json.loads(exc_info.value.payload)
392 | 
393 |             assert response_data["status"] == "error"
394 |             assert (
395 |                 "Model parameter is required" in response_data["content"] or "Model 'auto'" in response_data["content"]
396 |             )
397 |             assert "flash" in response_data["content"]
398 | 
399 |     def test_model_availability_with_restrictions(self):
400 |         """Test that auto mode respects model restrictions when selecting fallback models."""
401 | 
402 |         provider_config = {
403 |             "GEMINI_API_KEY": "real-key",
404 |             "OPENAI_API_KEY": "real-key",
405 |             "XAI_API_KEY": None,
406 |             "OPENROUTER_API_KEY": None,
407 |             "DEFAULT_MODEL": "auto",
408 |             "OPENAI_ALLOWED_MODELS": "o4-mini",  # Restrict OpenAI to only o4-mini
409 |         }
410 | 
411 |         # Filter out None values to avoid patch.dict errors
412 |         env_to_set = {k: v for k, v in provider_config.items() if v is not None}
413 |         env_to_clear = [k for k, v in provider_config.items() if v is None]
414 | 
415 |         with patch.dict(os.environ, env_to_set, clear=False):
416 |             # Clear the None-valued environment variables
417 |             for key in env_to_clear:
418 |                 if key in os.environ:
419 |                     del os.environ[key]
420 |             import config
421 | 
422 |             importlib.reload(config)
423 | 
424 |             # Clear restriction service to pick up new env vars
425 |             import utils.model_restrictions
426 | 
427 |             utils.model_restrictions._restriction_service = None
428 | 
429 |             # Register providers
430 |             ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
431 |             ModelProviderRegistry.register_provider(ProviderType.OPENAI, OpenAIModelProvider)
432 | 
433 |             # Get available models - should respect restrictions
434 |             available_models = ModelProviderRegistry.get_available_models(respect_restrictions=True)
435 | 
436 |             # Should include restricted OpenAI model
437 |             assert "o4-mini" in available_models
438 | 
439 |             # Should NOT include non-restricted OpenAI models
440 |             assert "o3" not in available_models
441 |             assert "o3-mini" not in available_models
442 | 
443 |             # Should still include all Gemini models (no restrictions)
444 |             assert "gemini-2.5-flash" in available_models
445 |             assert "gemini-2.5-pro" in available_models
446 | 
447 |     def test_openrouter_fallback_when_no_native_apis(self):
448 |         """Test that OpenRouter provides fallback models when no native APIs are available."""
449 | 
450 |         provider_config = {
451 |             "GEMINI_API_KEY": None,
452 |             "OPENAI_API_KEY": None,
453 |             "XAI_API_KEY": None,
454 |             "OPENROUTER_API_KEY": "real-key",
455 |             "DEFAULT_MODEL": "auto",
456 |         }
457 | 
458 |         # Filter out None values to avoid patch.dict errors
459 |         env_to_set = {k: v for k, v in provider_config.items() if v is not None}
460 |         env_to_clear = [k for k, v in provider_config.items() if v is None]
461 | 
462 |         with patch.dict(os.environ, env_to_set, clear=False):
463 |             # Clear the None-valued environment variables
464 |             for key in env_to_clear:
465 |                 if key in os.environ:
466 |                     del os.environ[key]
467 |             import config
468 | 
469 |             importlib.reload(config)
470 | 
471 |             # Register only OpenRouter provider
472 |             from providers.openrouter import OpenRouterProvider
473 | 
474 |             ModelProviderRegistry.register_provider(ProviderType.OPENROUTER, OpenRouterProvider)
475 | 
476 |             # Mock OpenRouter registry to return known models
477 |             mock_registry = MagicMock()
478 |             mock_registry.list_models.return_value = [
479 |                 "google/gemini-2.5-flash",
480 |                 "google/gemini-2.5-pro",
481 |                 "openai/o3",
482 |                 "openai/o4-mini",
483 |                 "anthropic/claude-opus-4",
484 |             ]
485 | 
486 |             with patch.object(OpenRouterProvider, "_registry", mock_registry):
487 |                 # Get preferred models for different categories
488 |                 extended_reasoning = ModelProviderRegistry.get_preferred_fallback_model(
489 |                     ToolModelCategory.EXTENDED_REASONING
490 |                 )
491 |                 fast_response = ModelProviderRegistry.get_preferred_fallback_model(ToolModelCategory.FAST_RESPONSE)
492 | 
493 |                 # Should fallback to known good models even via OpenRouter
494 |                 # The exact model depends on _find_extended_thinking_model implementation
495 |                 assert extended_reasoning is not None
496 |                 assert fast_response is not None
497 | 
498 |     @pytest.mark.asyncio
499 |     async def test_actual_model_name_resolution_in_auto_mode(self, tmp_path):
500 |         """Test that when a model is selected in auto mode, the tool executes successfully."""
501 | 
502 |         provider_config = {
503 |             "GEMINI_API_KEY": "real-key",
504 |             "OPENAI_API_KEY": None,
505 |             "XAI_API_KEY": None,
506 |             "OPENROUTER_API_KEY": None,
507 |             "DEFAULT_MODEL": "auto",
508 |         }
509 | 
510 |         # Filter out None values to avoid patch.dict errors
511 |         env_to_set = {k: v for k, v in provider_config.items() if v is not None}
512 |         env_to_clear = [k for k, v in provider_config.items() if v is None]
513 | 
514 |         with patch.dict(os.environ, env_to_set, clear=False):
515 |             # Clear the None-valued environment variables
516 |             for key in env_to_clear:
517 |                 if key in os.environ:
518 |                     del os.environ[key]
519 |             import config
520 | 
521 |             importlib.reload(config)
522 | 
523 |             # Register Gemini provider
524 |             ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
525 | 
526 |             # Mock the actual provider to simulate successful execution
527 |             mock_provider = MagicMock()
528 |             mock_response = MagicMock()
529 |             mock_response.content = "test response"
530 |             mock_response.model_name = "gemini-2.5-flash"  # The resolved name
531 |             mock_response.usage = {"input_tokens": 10, "output_tokens": 5}
532 |             # Mock _resolve_model_name to simulate alias resolution
533 |             mock_provider._resolve_model_name = lambda alias: ("gemini-2.5-flash" if alias == "flash" else alias)
534 |             mock_provider.generate_content.return_value = mock_response
535 | 
536 |             with patch.object(ModelProviderRegistry, "get_provider_for_model", return_value=mock_provider):
537 |                 chat_tool = ChatTool()
538 |                 workdir = tmp_path / "chat_artifacts"
539 |                 workdir.mkdir(parents=True, exist_ok=True)
540 |                 result = await chat_tool.execute(
541 |                     {"prompt": "test", "model": "flash", "working_directory_absolute_path": str(workdir)}
542 |                 )  # Use alias in auto mode
543 | 
544 |                 # Should succeed with proper model resolution
545 |                 assert len(result) == 1
546 |                 # Just verify that the tool executed successfully and didn't return an error
547 |                 assert "error" not in result[0].text.lower()
548 | 
```

--------------------------------------------------------------------------------
/tools/thinkdeep.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | ThinkDeep Workflow Tool - Extended Reasoning with Systematic Investigation
  3 | 
  4 | This tool provides step-by-step deep thinking capabilities using a systematic workflow approach.
  5 | It enables comprehensive analysis of complex problems with expert validation at completion.
  6 | 
  7 | Key Features:
  8 | - Systematic step-by-step thinking process
  9 | - Multi-step analysis with evidence gathering
 10 | - Confidence-based investigation flow
 11 | - Expert analysis integration with external models
 12 | - Support for focused analysis areas (architecture, performance, security, etc.)
 13 | - Confidence-based workflow optimization
 14 | """
 15 | 
 16 | import logging
 17 | from typing import TYPE_CHECKING, Any, Optional
 18 | 
 19 | from pydantic import Field
 20 | 
 21 | if TYPE_CHECKING:
 22 |     from tools.models import ToolModelCategory
 23 | 
 24 | from config import TEMPERATURE_CREATIVE
 25 | from systemprompts import THINKDEEP_PROMPT
 26 | from tools.shared.base_models import WorkflowRequest
 27 | 
 28 | from .workflow.base import WorkflowTool
 29 | 
 30 | logger = logging.getLogger(__name__)
 31 | 
 32 | 
 33 | class ThinkDeepWorkflowRequest(WorkflowRequest):
 34 |     """Request model for thinkdeep workflow tool with comprehensive investigation capabilities"""
 35 | 
 36 |     # Core workflow parameters
 37 |     step: str = Field(description="Current work step content and findings")
 38 |     step_number: int = Field(description="Current step number (starts at 1)", ge=1)
 39 |     total_steps: int = Field(description="Estimated total steps needed", ge=1)
 40 |     next_step_required: bool = Field(description="Whether another step is needed")
 41 |     findings: str = Field(
 42 |         description="Discoveries: insights, connections, implications, evidence. "
 43 |         "Document contradictions to earlier assumptions. Update past findings."
 44 |     )
 45 | 
 46 |     # Investigation tracking
 47 |     files_checked: list[str] = Field(
 48 |         default_factory=list,
 49 |         description="All files examined (absolute paths). Include ruled-out files.",
 50 |     )
 51 |     relevant_files: list[str] = Field(
 52 |         default_factory=list,
 53 |         description="Files relevant to problem/goal (absolute paths). Include root cause, solution, key insights.",
 54 |     )
 55 |     relevant_context: list[str] = Field(
 56 |         default_factory=list,
 57 |         description="Key concepts/methods: 'concept_name' or 'ClassName.methodName'. Focus on core insights, decision points.",
 58 |     )
 59 |     hypothesis: Optional[str] = Field(
 60 |         default=None,
 61 |         description="Current theory based on evidence. Revise in later steps.",
 62 |     )
 63 | 
 64 |     # Analysis metadata
 65 |     issues_found: list[dict] = Field(
 66 |         default_factory=list,
 67 |         description="Issues with dict: 'severity' (critical/high/medium/low), 'description'.",
 68 |     )
 69 |     confidence: str = Field(
 70 |         default="low",
 71 |         description="exploring/low/medium/high/very_high/almost_certain/certain. CRITICAL: 'certain' PREVENTS external validation.",
 72 |     )
 73 | 
 74 |     # Expert analysis configuration - keep these fields available for configuring the final assistant model
 75 |     # in expert analysis (commented out exclude=True)
 76 |     temperature: Optional[float] = Field(
 77 |         default=None,
 78 |         description="Creative thinking temp (0-1, default 0.7)",
 79 |         ge=0.0,
 80 |         le=1.0,
 81 |     )
 82 |     thinking_mode: Optional[str] = Field(
 83 |         default=None,
 84 |         description="Depth: minimal/low/medium/high/max. Default 'high'.",
 85 |     )
 86 |     # Context files and investigation scope
 87 |     problem_context: Optional[str] = Field(
 88 |         default=None,
 89 |         description="Additional context about problem/goal. Be expressive.",
 90 |     )
 91 |     focus_areas: Optional[list[str]] = Field(
 92 |         default=None,
 93 |         description="Focus aspects (architecture, performance, security, etc.)",
 94 |     )
 95 | 
 96 | 
 97 | class ThinkDeepTool(WorkflowTool):
 98 |     """
 99 |     ThinkDeep Workflow Tool - Systematic Deep Thinking Analysis
100 | 
101 |     Provides comprehensive step-by-step thinking capabilities with expert validation.
102 |     Uses workflow architecture for systematic investigation and analysis.
103 |     """
104 | 
105 |     name = "thinkdeep"
106 |     description = (
107 |         "Performs multi-stage investigation and reasoning for complex problem analysis. "
108 |         "Use for architecture decisions, complex bugs, performance challenges, and security analysis. "
109 |         "Provides systematic hypothesis testing, evidence-based investigation, and expert validation."
110 |     )
111 | 
112 |     def __init__(self):
113 |         """Initialize the ThinkDeep workflow tool"""
114 |         super().__init__()
115 |         # Storage for request parameters to use in expert analysis
116 |         self.stored_request_params = {}
117 | 
118 |     def get_name(self) -> str:
119 |         """Return the tool name"""
120 |         return self.name
121 | 
122 |     def get_description(self) -> str:
123 |         """Return the tool description"""
124 |         return self.description
125 | 
126 |     def get_model_category(self) -> "ToolModelCategory":
127 |         """Return the model category for this tool"""
128 |         from tools.models import ToolModelCategory
129 | 
130 |         return ToolModelCategory.EXTENDED_REASONING
131 | 
132 |     def get_workflow_request_model(self):
133 |         """Return the workflow request model for this tool"""
134 |         return ThinkDeepWorkflowRequest
135 | 
136 |     def get_input_schema(self) -> dict[str, Any]:
137 |         """Generate input schema using WorkflowSchemaBuilder with thinkdeep-specific overrides."""
138 |         from .workflow.schema_builders import WorkflowSchemaBuilder
139 | 
140 |         # ThinkDeep workflow-specific field overrides
141 |         thinkdeep_field_overrides = {
142 |             "problem_context": {
143 |                 "type": "string",
144 |                 "description": "Additional context about problem/goal. Be expressive.",
145 |             },
146 |             "focus_areas": {
147 |                 "type": "array",
148 |                 "items": {"type": "string"},
149 |                 "description": "Focus aspects (architecture, performance, security, etc.)",
150 |             },
151 |         }
152 | 
153 |         # Use WorkflowSchemaBuilder with thinkdeep-specific tool fields
154 |         return WorkflowSchemaBuilder.build_schema(
155 |             tool_specific_fields=thinkdeep_field_overrides,
156 |             model_field_schema=self.get_model_field_schema(),
157 |             auto_mode=self.is_effective_auto_mode(),
158 |             tool_name=self.get_name(),
159 |         )
160 | 
161 |     def get_system_prompt(self) -> str:
162 |         """Return the system prompt for this workflow tool"""
163 |         return THINKDEEP_PROMPT
164 | 
165 |     def get_default_temperature(self) -> float:
166 |         """Return default temperature for deep thinking"""
167 |         return TEMPERATURE_CREATIVE
168 | 
169 |     def get_default_thinking_mode(self) -> str:
170 |         """Return default thinking mode for thinkdeep"""
171 |         from config import DEFAULT_THINKING_MODE_THINKDEEP
172 | 
173 |         return DEFAULT_THINKING_MODE_THINKDEEP
174 | 
175 |     def customize_workflow_response(self, response_data: dict, request, **kwargs) -> dict:
176 |         """
177 |         Customize the workflow response for thinkdeep-specific needs
178 |         """
179 |         # Store request parameters for later use in expert analysis
180 |         self.stored_request_params = {}
181 |         try:
182 |             self.stored_request_params["temperature"] = request.temperature
183 |         except AttributeError:
184 |             self.stored_request_params["temperature"] = None
185 | 
186 |         try:
187 |             self.stored_request_params["thinking_mode"] = request.thinking_mode
188 |         except AttributeError:
189 |             self.stored_request_params["thinking_mode"] = None
190 | 
191 |         # Add thinking-specific context to response
192 |         response_data.update(
193 |             {
194 |                 "thinking_status": {
195 |                     "current_step": request.step_number,
196 |                     "total_steps": request.total_steps,
197 |                     "files_checked": len(request.files_checked),
198 |                     "relevant_files": len(request.relevant_files),
199 |                     "thinking_confidence": request.confidence,
200 |                     "analysis_focus": request.focus_areas or ["general"],
201 |                 }
202 |             }
203 |         )
204 | 
205 |         # Add thinking_complete field for final steps (test expects this)
206 |         if not request.next_step_required:
207 |             response_data["thinking_complete"] = True
208 | 
209 |             # Add complete_thinking summary (test expects this)
210 |             response_data["complete_thinking"] = {
211 |                 "steps_completed": len(self.work_history),
212 |                 "final_confidence": request.confidence,
213 |                 "relevant_context": list(self.consolidated_findings.relevant_context),
214 |                 "key_findings": self.consolidated_findings.findings,
215 |                 "issues_identified": self.consolidated_findings.issues_found,
216 |                 "files_analyzed": list(self.consolidated_findings.relevant_files),
217 |             }
218 | 
219 |         # Add thinking-specific completion message based on confidence
220 |         if request.confidence == "certain":
221 |             response_data["completion_message"] = (
222 |                 "Deep thinking analysis is complete with high certainty. "
223 |                 "All aspects have been thoroughly considered and conclusions are definitive."
224 |             )
225 |         elif not request.next_step_required:
226 |             response_data["completion_message"] = (
227 |                 "Deep thinking analysis phase complete. Expert validation will provide additional insights and recommendations."
228 |             )
229 | 
230 |         return response_data
231 | 
232 |     def should_skip_expert_analysis(self, request, consolidated_findings) -> bool:
233 |         """
234 |         ThinkDeep tool skips expert analysis when the CLI agent has "certain" confidence.
235 |         """
236 |         return request.confidence == "certain" and not request.next_step_required
237 | 
238 |     def get_completion_status(self) -> str:
239 |         """ThinkDeep tools use thinking-specific status."""
240 |         return "deep_thinking_complete_ready_for_implementation"
241 | 
242 |     def get_completion_data_key(self) -> str:
243 |         """ThinkDeep uses 'complete_thinking' key."""
244 |         return "complete_thinking"
245 | 
246 |     def get_final_analysis_from_request(self, request):
247 |         """ThinkDeep tools use 'findings' field."""
248 |         return request.findings
249 | 
250 |     def get_skip_expert_analysis_status(self) -> str:
251 |         """Status when skipping expert analysis for certain confidence."""
252 |         return "skipped_due_to_certain_thinking_confidence"
253 | 
254 |     def get_skip_reason(self) -> str:
255 |         """Reason for skipping expert analysis."""
256 |         return "Expressed 'certain' confidence in the deep thinking analysis - no additional validation needed"
257 | 
258 |     def get_completion_message(self) -> str:
259 |         """Message for completion without expert analysis."""
260 |         return "Deep thinking analysis complete with certain confidence. Proceed with implementation based on the analysis."
261 | 
262 |     def customize_expert_analysis_prompt(self, base_prompt: str, request, file_content: str = "") -> str:
263 |         """
264 |         Customize the expert analysis prompt for deep thinking validation
265 |         """
266 |         thinking_context = f"""
267 | DEEP THINKING ANALYSIS VALIDATION
268 | 
269 | You are reviewing a comprehensive deep thinking analysis completed through systematic investigation.
270 | Your role is to validate the thinking process, identify any gaps, challenge assumptions, and provide
271 | additional insights or alternative perspectives.
272 | 
273 | ANALYSIS SCOPE:
274 | - Problem Context: {self._get_problem_context(request)}
275 | - Focus Areas: {', '.join(self._get_focus_areas(request))}
276 | - Investigation Confidence: {request.confidence}
277 | - Steps Completed: {request.step_number} of {request.total_steps}
278 | 
279 | THINKING SUMMARY:
280 | {request.findings}
281 | 
282 | KEY INSIGHTS AND CONTEXT:
283 | {', '.join(request.relevant_context) if request.relevant_context else 'No specific context identified'}
284 | 
285 | VALIDATION OBJECTIVES:
286 | 1. Assess the depth and quality of the thinking process
287 | 2. Identify any logical gaps, missing considerations, or flawed assumptions
288 | 3. Suggest alternative approaches or perspectives not considered
289 | 4. Validate the conclusions and recommendations
290 | 5. Provide actionable next steps for implementation
291 | 
292 | Be thorough but constructive in your analysis. Challenge the thinking where appropriate,
293 | but also acknowledge strong insights and valid conclusions.
294 | """
295 | 
296 |         if file_content:
297 |             thinking_context += f"\n\nFILE CONTEXT:\n{file_content}"
298 | 
299 |         return f"{thinking_context}\n\n{base_prompt}"
300 | 
301 |     def get_expert_analysis_instructions(self) -> str:
302 |         """
303 |         Return instructions for expert analysis specific to deep thinking validation
304 |         """
305 |         return (
306 |             "DEEP THINKING ANALYSIS IS COMPLETE. You MUST now summarize and present ALL thinking insights, "
307 |             "alternative approaches considered, risks and trade-offs identified, and final recommendations. "
308 |             "Clearly prioritize the top solutions or next steps that emerged from the analysis. "
309 |             "Provide concrete, actionable guidance based on the deep thinking—make it easy for the user to "
310 |             "understand exactly what to do next and how to implement the best solution."
311 |         )
312 | 
313 |     # Override hook methods to use stored request parameters for expert analysis
314 | 
315 |     def get_request_temperature(self, request) -> float:
316 |         """Use stored temperature from initial request."""
317 |         try:
318 |             stored_params = self.stored_request_params
319 |             if stored_params and stored_params.get("temperature") is not None:
320 |                 return stored_params["temperature"]
321 |         except AttributeError:
322 |             pass
323 |         return super().get_request_temperature(request)
324 | 
325 |     def get_request_thinking_mode(self, request) -> str:
326 |         """Use stored thinking mode from initial request."""
327 |         try:
328 |             stored_params = self.stored_request_params
329 |             if stored_params and stored_params.get("thinking_mode") is not None:
330 |                 return stored_params["thinking_mode"]
331 |         except AttributeError:
332 |             pass
333 |         return super().get_request_thinking_mode(request)
334 | 
335 |     def _get_problem_context(self, request) -> str:
336 |         """Get problem context from request. Override for custom context handling."""
337 |         try:
338 |             return request.problem_context or "General analysis"
339 |         except AttributeError:
340 |             return "General analysis"
341 | 
342 |     def _get_focus_areas(self, request) -> list[str]:
343 |         """Get focus areas from request. Override for custom focus area handling."""
344 |         try:
345 |             return request.focus_areas or ["comprehensive analysis"]
346 |         except AttributeError:
347 |             return ["comprehensive analysis"]
348 | 
349 |     def get_required_actions(
350 |         self, step_number: int, confidence: str, findings: str, total_steps: int, request=None
351 |     ) -> list[str]:
352 |         """
353 |         Return required actions for the current thinking step.
354 |         """
355 |         actions = []
356 | 
357 |         if step_number == 1:
358 |             actions.extend(
359 |                 [
360 |                     "Begin systematic thinking analysis",
361 |                     "Identify key aspects and assumptions to explore",
362 |                     "Establish initial investigation approach",
363 |                 ]
364 |             )
365 |         elif confidence == "low":
366 |             actions.extend(
367 |                 [
368 |                     "Continue gathering evidence and insights",
369 |                     "Test initial hypotheses",
370 |                     "Explore alternative perspectives",
371 |                 ]
372 |             )
373 |         elif confidence == "medium":
374 |             actions.extend(
375 |                 [
376 |                     "Deepen analysis of promising approaches",
377 |                     "Validate key assumptions",
378 |                     "Consider implementation challenges",
379 |                 ]
380 |             )
381 |         elif confidence == "high":
382 |             actions.extend(
383 |                 [
384 |                     "Refine and validate key findings",
385 |                     "Explore edge cases and limitations",
386 |                     "Document assumptions and trade-offs",
387 |                 ]
388 |             )
389 |         elif confidence == "very_high":
390 |             actions.extend(
391 |                 [
392 |                     "Synthesize findings into cohesive recommendations",
393 |                     "Validate conclusions against all evidence",
394 |                     "Prepare comprehensive implementation guidance",
395 |                 ]
396 |             )
397 |         elif confidence == "almost_certain":
398 |             actions.extend(
399 |                 [
400 |                     "Finalize recommendations with high confidence",
401 |                     "Document any remaining minor uncertainties",
402 |                     "Prepare for expert analysis or implementation",
403 |                 ]
404 |             )
405 |         else:  # certain
406 |             actions.append("Analysis complete - ready for implementation")
407 | 
408 |         return actions
409 | 
410 |     def should_call_expert_analysis(self, consolidated_findings, request=None) -> bool:
411 |         """
412 |         Determine if expert analysis should be called based on confidence and completion.
413 |         """
414 |         if request:
415 |             try:
416 |                 # Don't call expert analysis if confidence is "certain"
417 |                 if request.confidence == "certain":
418 |                     return False
419 |             except AttributeError:
420 |                 pass
421 | 
422 |         # Call expert analysis if investigation is complete (when next_step_required is False)
423 |         if request:
424 |             try:
425 |                 return not request.next_step_required
426 |             except AttributeError:
427 |                 pass
428 | 
429 |         # Fallback: call expert analysis if we have meaningful findings
430 |         return (
431 |             len(consolidated_findings.relevant_files) > 0
432 |             or len(consolidated_findings.findings) >= 2
433 |             or len(consolidated_findings.issues_found) > 0
434 |         )
435 | 
436 |     def prepare_expert_analysis_context(self, consolidated_findings) -> str:
437 |         """
438 |         Prepare context for expert analysis specific to deep thinking.
439 |         """
440 |         context_parts = []
441 | 
442 |         context_parts.append("DEEP THINKING ANALYSIS SUMMARY:")
443 |         context_parts.append(f"Steps completed: {len(consolidated_findings.findings)}")
444 |         context_parts.append(f"Final confidence: {consolidated_findings.confidence}")
445 | 
446 |         if consolidated_findings.findings:
447 |             context_parts.append("\nKEY FINDINGS:")
448 |             for i, finding in enumerate(consolidated_findings.findings, 1):
449 |                 context_parts.append(f"{i}. {finding}")
450 | 
451 |         if consolidated_findings.relevant_context:
452 |             context_parts.append(f"\nRELEVANT CONTEXT:\n{', '.join(consolidated_findings.relevant_context)}")
453 | 
454 |         # Get hypothesis from latest hypotheses entry if available
455 |         if consolidated_findings.hypotheses:
456 |             latest_hypothesis = consolidated_findings.hypotheses[-1].get("hypothesis", "")
457 |             if latest_hypothesis:
458 |                 context_parts.append(f"\nFINAL HYPOTHESIS:\n{latest_hypothesis}")
459 | 
460 |         if consolidated_findings.issues_found:
461 |             context_parts.append(f"\nISSUES IDENTIFIED: {len(consolidated_findings.issues_found)} issues")
462 |             for issue in consolidated_findings.issues_found:
463 |                 context_parts.append(
464 |                     f"- {issue.get('severity', 'unknown')}: {issue.get('description', 'No description')}"
465 |                 )
466 | 
467 |         return "\n".join(context_parts)
468 | 
469 |     def get_step_guidance_message(self, request) -> str:
470 |         """
471 |         Generate guidance for the next step in thinking analysis
472 |         """
473 |         if request.next_step_required:
474 |             next_step_number = request.step_number + 1
475 | 
476 |             if request.confidence == "certain":
477 |                 guidance = (
478 |                     f"Your thinking analysis confidence is CERTAIN. Consider if you truly need step {next_step_number} "
479 |                     f"or if you should complete the analysis now with expert validation."
480 |                 )
481 |             elif request.confidence == "almost_certain":
482 |                 guidance = (
483 |                     f"Your thinking analysis confidence is ALMOST_CERTAIN. For step {next_step_number}, consider: "
484 |                     f"finalizing recommendations, documenting any minor uncertainties, or preparing for implementation."
485 |                 )
486 |             elif request.confidence == "very_high":
487 |                 guidance = (
488 |                     f"Your thinking analysis confidence is VERY_HIGH. For step {next_step_number}, consider: "
489 |                     f"synthesis of all findings, comprehensive validation, or creating implementation roadmap."
490 |                 )
491 |             elif request.confidence == "high":
492 |                 guidance = (
493 |                     f"Your thinking analysis confidence is HIGH. For step {next_step_number}, consider: "
494 |                     f"exploring edge cases, documenting trade-offs, or stress-testing key assumptions."
495 |                 )
496 |             elif request.confidence == "medium":
497 |                 guidance = (
498 |                     f"Your thinking analysis confidence is MEDIUM. For step {next_step_number}, focus on: "
499 |                     f"deepening insights, exploring alternative approaches, or gathering additional evidence."
500 |                 )
501 |             else:  # low or exploring
502 |                 guidance = (
503 |                     f"Your thinking analysis confidence is {request.confidence.upper()}. For step {next_step_number}, "
504 |                     f"continue investigating: gather more evidence, test hypotheses, or explore different angles."
505 |                 )
506 | 
507 |             # Add specific thinking guidance based on progress
508 |             if request.step_number == 1:
509 |                 guidance += (
510 |                     " Consider: What are the key assumptions? What evidence supports or contradicts initial theories? "
511 |                     "What alternative approaches exist?"
512 |                 )
513 |             elif request.step_number >= request.total_steps // 2:
514 |                 guidance += (
515 |                     " Consider: Synthesis of findings, validation of conclusions, identification of implementation "
516 |                     "challenges, and preparation for expert analysis."
517 |                 )
518 | 
519 |             return guidance
520 |         else:
521 |             return "Thinking analysis is ready for expert validation and final recommendations."
522 | 
523 |     def format_final_response(self, assistant_response: str, request, **kwargs) -> dict:
524 |         """
525 |         Format the final response from the assistant for thinking analysis
526 |         """
527 |         response_data = {
528 |             "thinking_analysis": assistant_response,
529 |             "analysis_metadata": {
530 |                 "total_steps_completed": request.step_number,
531 |                 "final_confidence": request.confidence,
532 |                 "files_analyzed": len(request.relevant_files),
533 |                 "key_insights": len(request.relevant_context),
534 |                 "issues_identified": len(request.issues_found),
535 |             },
536 |         }
537 | 
538 |         # Add completion status
539 |         if request.confidence == "certain":
540 |             response_data["completion_status"] = "analysis_complete_with_certainty"
541 |         else:
542 |             response_data["completion_status"] = "analysis_complete_pending_validation"
543 | 
544 |         return response_data
545 | 
546 |     def format_step_response(
547 |         self,
548 |         assistant_response: str,
549 |         request,
550 |         status: str = "pause_for_thinkdeep",
551 |         continuation_id: Optional[str] = None,
552 |         **kwargs,
553 |     ) -> dict:
554 |         """
555 |         Format intermediate step responses for thinking workflow
556 |         """
557 |         response_data = super().format_step_response(assistant_response, request, status, continuation_id, **kwargs)
558 | 
559 |         # Add thinking-specific step guidance
560 |         step_guidance = self.get_step_guidance_message(request)
561 |         response_data["thinking_guidance"] = step_guidance
562 | 
563 |         # Add analysis progress indicators
564 |         response_data["analysis_progress"] = {
565 |             "step_completed": request.step_number,
566 |             "remaining_steps": max(0, request.total_steps - request.step_number),
567 |             "confidence_trend": request.confidence,
568 |             "investigation_depth": "expanding" if request.next_step_required else "finalizing",
569 |         }
570 | 
571 |         return response_data
572 | 
573 |     # Required abstract methods from BaseTool
574 |     def get_request_model(self):
575 |         """Return the thinkdeep workflow-specific request model."""
576 |         return ThinkDeepWorkflowRequest
577 | 
578 |     async def prepare_prompt(self, request) -> str:
579 |         """Not used - workflow tools use execute_workflow()."""
580 |         return ""  # Workflow tools use execute_workflow() directly
581 | 
```

--------------------------------------------------------------------------------
/tests/test_collaboration.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for dynamic context request and collaboration features
  3 | """
  4 | 
  5 | import json
  6 | import os
  7 | from unittest.mock import Mock, patch
  8 | 
  9 | import pytest
 10 | 
 11 | from tests.mock_helpers import create_mock_provider
 12 | from tools.analyze import AnalyzeTool
 13 | from tools.debug import DebugIssueTool
 14 | from tools.models import FilesNeededRequest, ToolOutput
 15 | 
 16 | 
 17 | class TestDynamicContextRequests:
 18 |     """Test the dynamic context request mechanism"""
 19 | 
 20 |     @pytest.fixture
 21 |     def analyze_tool(self):
 22 |         return AnalyzeTool()
 23 | 
 24 |     @pytest.fixture
 25 |     def debug_tool(self):
 26 |         return DebugIssueTool()
 27 | 
 28 |     @pytest.mark.asyncio
 29 |     @patch("tools.shared.base_tool.BaseTool.get_model_provider")
 30 |     async def test_clarification_request_parsing(self, mock_get_provider, analyze_tool):
 31 |         """Test that tools correctly parse clarification requests"""
 32 |         # Mock model to return a clarification request
 33 |         clarification_json = json.dumps(
 34 |             {
 35 |                 "status": "files_required_to_continue",
 36 |                 "mandatory_instructions": "I need to see the package.json file to understand dependencies",
 37 |                 "files_needed": ["package.json", "package-lock.json"],
 38 |             },
 39 |             ensure_ascii=False,
 40 |         )
 41 | 
 42 |         mock_provider = create_mock_provider()
 43 |         mock_provider.get_provider_type.return_value = Mock(value="google")
 44 |         mock_provider.generate_content.return_value = Mock(
 45 |             content=clarification_json, usage={}, model_name="gemini-2.5-flash", metadata={}
 46 |         )
 47 |         mock_get_provider.return_value = mock_provider
 48 | 
 49 |         result = await analyze_tool.execute(
 50 |             {
 51 |                 "step": "Analyze the dependencies used in this project",
 52 |                 "step_number": 1,
 53 |                 "total_steps": 1,
 54 |                 "next_step_required": False,
 55 |                 "findings": "Initial dependency analysis",
 56 |                 "relevant_files": ["/absolute/path/src/index.js"],
 57 |             }
 58 |         )
 59 | 
 60 |         assert len(result) == 1
 61 | 
 62 |         # Parse the response - analyze tool now uses workflow architecture
 63 |         response_data = json.loads(result[0].text)
 64 |         # Workflow tools may handle provider errors differently than simple tools
 65 |         # They might return error, expert analysis, or clarification requests
 66 |         assert response_data["status"] in ["calling_expert_analysis", "error", "files_required_to_continue"]
 67 | 
 68 |         # Check that expert analysis was performed and contains the clarification
 69 |         if "expert_analysis" in response_data:
 70 |             expert_analysis = response_data["expert_analysis"]
 71 |             # The mock should have returned the clarification JSON
 72 |             if "raw_analysis" in expert_analysis:
 73 |                 analysis_content = expert_analysis["raw_analysis"]
 74 |                 assert "package.json" in analysis_content
 75 |                 assert "dependencies" in analysis_content
 76 | 
 77 |         # For workflow tools, the files_needed logic is handled differently
 78 |         # The test validates that the mocked clarification content was processed
 79 |         assert "step_number" in response_data
 80 |         assert response_data["step_number"] == 1
 81 | 
 82 |     @pytest.mark.asyncio
 83 |     @patch("tools.shared.base_tool.BaseTool.get_model_provider")
 84 |     @patch("utils.conversation_memory.create_thread", return_value="debug-test-uuid")
 85 |     @patch("utils.conversation_memory.add_turn")
 86 |     async def test_normal_response_not_parsed_as_clarification(
 87 |         self, mock_add_turn, mock_create_thread, mock_get_provider, debug_tool
 88 |     ):
 89 |         """Test that normal investigation responses work correctly with new debug tool"""
 90 |         # The new debug tool uses self-investigation pattern
 91 |         result = await debug_tool.execute(
 92 |             {
 93 |                 "step": "Investigating NameError: name 'utils' is not defined",
 94 |                 "step_number": 1,
 95 |                 "total_steps": 3,
 96 |                 "next_step_required": True,
 97 |                 "findings": "The error indicates 'utils' module is not imported or defined",
 98 |                 "files_checked": ["/code/main.py"],
 99 |                 "relevant_files": ["/code/main.py"],
100 |                 "hypothesis": "Missing import statement for utils module",
101 |                 "confidence": "high",
102 |             }
103 |         )
104 | 
105 |         assert len(result) == 1
106 | 
107 |         # Parse the response - new debug tool returns structured JSON
108 |         response_data = json.loads(result[0].text)
109 |         # Debug tool now returns "pause_for_investigation" to force actual investigation
110 |         assert response_data["status"] == "pause_for_investigation"
111 |         assert response_data["step_number"] == 1
112 |         assert response_data["next_step_required"] is True
113 |         assert response_data["investigation_status"]["current_confidence"] == "high"
114 |         assert response_data["investigation_required"] is True
115 |         assert "required_actions" in response_data
116 | 
117 |     @pytest.mark.asyncio
118 |     @patch("tools.shared.base_tool.BaseTool.get_model_provider")
119 |     async def test_malformed_clarification_request_treated_as_normal(self, mock_get_provider, analyze_tool):
120 |         """Test that malformed JSON clarification requests are treated as normal responses"""
121 |         malformed_json = '{"status": "files_required_to_continue", "prompt": "Missing closing brace"'
122 | 
123 |         mock_provider = create_mock_provider()
124 |         mock_provider.get_provider_type.return_value = Mock(value="google")
125 |         mock_provider.generate_content.return_value = Mock(
126 |             content=malformed_json, usage={}, model_name="gemini-2.5-flash", metadata={}
127 |         )
128 |         mock_get_provider.return_value = mock_provider
129 | 
130 |         result = await analyze_tool.execute(
131 |             {
132 |                 "step": "What does this do?",
133 |                 "step_number": 1,
134 |                 "total_steps": 1,
135 |                 "next_step_required": False,
136 |                 "findings": "Initial code analysis",
137 |                 "relevant_files": ["/absolute/path/test.py"],
138 |             }
139 |         )
140 | 
141 |         assert len(result) == 1
142 | 
143 |         # Should be treated as normal response due to JSON parse error
144 |         response_data = json.loads(result[0].text)
145 |         # Workflow tools may handle provider errors differently than simple tools
146 |         # They might return error, expert analysis, or clarification requests
147 |         assert response_data["status"] in ["calling_expert_analysis", "error", "files_required_to_continue"]
148 | 
149 |         # The malformed JSON should appear in the expert analysis content
150 |         if "expert_analysis" in response_data:
151 |             expert_analysis = response_data["expert_analysis"]
152 |             if "raw_analysis" in expert_analysis:
153 |                 analysis_content = expert_analysis["raw_analysis"]
154 |                 # The malformed JSON should be included in the analysis
155 |                 assert "files_required_to_continue" in analysis_content or malformed_json in str(response_data)
156 | 
157 |     @pytest.mark.asyncio
158 |     @patch("tools.shared.base_tool.BaseTool.get_model_provider")
159 |     async def test_clarification_with_suggested_action(self, mock_get_provider, analyze_tool):
160 |         """Test clarification request with suggested next action"""
161 |         import importlib
162 | 
163 |         from providers.registry import ModelProviderRegistry
164 | 
165 |         # Ensure deterministic model configuration for this test regardless of previous suites
166 |         ModelProviderRegistry.reset_for_testing()
167 | 
168 |         original_default = os.environ.get("DEFAULT_MODEL")
169 | 
170 |         try:
171 |             os.environ["DEFAULT_MODEL"] = "gemini-2.5-flash"
172 |             import config
173 | 
174 |             importlib.reload(config)
175 | 
176 |             clarification_json = json.dumps(
177 |                 {
178 |                     "status": "files_required_to_continue",
179 |                     "mandatory_instructions": "I need to see the database configuration to analyze the connection error",
180 |                     "files_needed": ["config/database.yml", "src/db.py"],
181 |                     "suggested_next_action": {
182 |                         "tool": "analyze",
183 |                         "args": {
184 |                             "prompt": "Analyze database connection timeout issue",
185 |                             "relevant_files": [
186 |                                 "/config/database.yml",
187 |                                 "/src/db.py",
188 |                                 "/logs/error.log",
189 |                             ],
190 |                         },
191 |                     },
192 |                 },
193 |                 ensure_ascii=False,
194 |             )
195 | 
196 |             mock_provider = create_mock_provider()
197 |             mock_provider.get_provider_type.return_value = Mock(value="google")
198 |             mock_provider.generate_content.return_value = Mock(
199 |                 content=clarification_json, usage={}, model_name="gemini-2.5-flash", metadata={}
200 |             )
201 |             mock_get_provider.return_value = mock_provider
202 | 
203 |             result = await analyze_tool.execute(
204 |                 {
205 |                     "step": "Analyze database connection timeout issue",
206 |                     "step_number": 1,
207 |                     "total_steps": 1,
208 |                     "next_step_required": False,
209 |                     "findings": "Initial database timeout analysis",
210 |                     "relevant_files": ["/absolute/logs/error.log"],
211 |                 }
212 |             )
213 | 
214 |             assert len(result) == 1
215 | 
216 |             response_data = json.loads(result[0].text)
217 | 
218 |             # Workflow tools should either promote clarification status or handle it in expert analysis
219 |             if response_data["status"] == "files_required_to_continue":
220 |                 # Clarification was properly promoted to main status
221 |                 # Check if mandatory_instructions is at top level or in content
222 |                 if "mandatory_instructions" in response_data:
223 |                     assert "database configuration" in response_data["mandatory_instructions"]
224 |                     assert "files_needed" in response_data
225 |                     assert "config/database.yml" in response_data["files_needed"]
226 |                     assert "src/db.py" in response_data["files_needed"]
227 |                 elif "content" in response_data:
228 |                     # Parse content JSON for workflow tools
229 |                     try:
230 |                         content_json = json.loads(response_data["content"])
231 |                         assert "mandatory_instructions" in content_json
232 |                         assert (
233 |                             "database configuration" in content_json["mandatory_instructions"]
234 |                             or "database" in content_json["mandatory_instructions"]
235 |                         )
236 |                         assert "files_needed" in content_json
237 |                         files_needed_str = str(content_json["files_needed"])
238 |                         assert (
239 |                             "config/database.yml" in files_needed_str
240 |                             or "config" in files_needed_str
241 |                             or "database" in files_needed_str
242 |                         )
243 |                     except json.JSONDecodeError:
244 |                         # Content is not JSON, check if it contains required text
245 |                         content = response_data["content"]
246 |                         assert "database configuration" in content or "config" in content
247 |             elif response_data["status"] == "calling_expert_analysis":
248 |                 # Clarification may be handled in expert analysis section
249 |                 if "expert_analysis" in response_data:
250 |                     expert_analysis = response_data["expert_analysis"]
251 |                     expert_content = str(expert_analysis)
252 |                     assert (
253 |                         "database configuration" in expert_content
254 |                         or "config/database.yml" in expert_content
255 |                         or "files_required_to_continue" in expert_content
256 |                     )
257 |             else:
258 |                 # Some other status - ensure it's a valid workflow response
259 |                 assert "step_number" in response_data
260 | 
261 |             # Check for suggested next action
262 |             if "suggested_next_action" in response_data:
263 |                 action = response_data["suggested_next_action"]
264 |                 assert action["tool"] == "analyze"
265 |         finally:
266 |             if original_default is not None:
267 |                 os.environ["DEFAULT_MODEL"] = original_default
268 |             else:
269 |                 os.environ.pop("DEFAULT_MODEL", None)
270 | 
271 |             import config
272 | 
273 |             importlib.reload(config)
274 |             ModelProviderRegistry.reset_for_testing()
275 | 
276 |     def test_tool_output_model_serialization(self):
277 |         """Test ToolOutput model serialization"""
278 |         output = ToolOutput(
279 |             status="success",
280 |             content="Test content",
281 |             content_type="markdown",
282 |             metadata={"tool_name": "test", "execution_time": 1.5},
283 |         )
284 | 
285 |         json_str = output.model_dump_json()
286 |         parsed = json.loads(json_str)
287 | 
288 |         assert parsed["status"] == "success"
289 |         assert parsed["content"] == "Test content"
290 |         assert parsed["content_type"] == "markdown"
291 |         assert parsed["metadata"]["tool_name"] == "test"
292 | 
293 |     def test_clarification_request_model(self):
294 |         """Test FilesNeededRequest model"""
295 |         request = FilesNeededRequest(
296 |             mandatory_instructions="Need more context",
297 |             files_needed=["file1.py", "file2.py"],
298 |             suggested_next_action={"tool": "analyze", "args": {}},
299 |         )
300 | 
301 |         assert request.mandatory_instructions == "Need more context"
302 |         assert len(request.files_needed) == 2
303 |         assert request.suggested_next_action["tool"] == "analyze"
304 | 
305 |     @pytest.mark.asyncio
306 |     @patch("tools.shared.base_tool.BaseTool.get_model_provider")
307 |     async def test_error_response_format(self, mock_get_provider, analyze_tool):
308 |         """Test error response format"""
309 |         mock_get_provider.side_effect = Exception("API connection failed")
310 | 
311 |         result = await analyze_tool.execute(
312 |             {
313 |                 "step": "Analyze this",
314 |                 "step_number": 1,
315 |                 "total_steps": 1,
316 |                 "next_step_required": False,
317 |                 "findings": "Initial analysis",
318 |                 "relevant_files": ["/absolute/path/test.py"],
319 |             }
320 |         )
321 | 
322 |         assert len(result) == 1
323 | 
324 |         response_data = json.loads(result[0].text)
325 |         # Workflow tools may handle provider errors differently than simple tools
326 |         # They might return error, complete analysis, or even clarification requests
327 |         assert response_data["status"] in ["error", "calling_expert_analysis", "files_required_to_continue"]
328 | 
329 |         # If expert analysis was attempted, it may succeed or fail
330 |         if response_data["status"] == "calling_expert_analysis" and "expert_analysis" in response_data:
331 |             expert_analysis = response_data["expert_analysis"]
332 |             # Could be an error or a successful analysis that requests clarification
333 |             analysis_status = expert_analysis.get("status", "")
334 |             assert (
335 |                 analysis_status in ["analysis_error", "analysis_complete"]
336 |                 or "error" in expert_analysis
337 |                 or "files_required_to_continue" in str(expert_analysis)
338 |             )
339 |         elif response_data["status"] == "error":
340 |             assert "content" in response_data
341 |             assert response_data["content_type"] == "text"
342 | 
343 | 
344 | class TestCollaborationWorkflow:
345 |     """Test complete collaboration workflows"""
346 | 
347 |     def teardown_method(self):
348 |         """Clean up after each test to prevent state pollution."""
349 |         # Clear provider registry singleton
350 |         from providers.registry import ModelProviderRegistry
351 | 
352 |         ModelProviderRegistry._instance = None
353 | 
354 |     @pytest.mark.asyncio
355 |     @patch("tools.shared.base_tool.BaseTool.get_model_provider")
356 |     @patch("tools.workflow.workflow_mixin.BaseWorkflowMixin._call_expert_analysis")
357 |     async def test_dependency_analysis_triggers_clarification(self, mock_expert_analysis, mock_get_provider):
358 |         """Test that asking about dependencies without package files triggers clarification"""
359 |         tool = AnalyzeTool()
360 | 
361 |         # Mock Gemini to request package.json when asked about dependencies
362 |         clarification_json = json.dumps(
363 |             {
364 |                 "status": "files_required_to_continue",
365 |                 "mandatory_instructions": "I need to see the package.json file to analyze npm dependencies",
366 |                 "files_needed": ["package.json", "package-lock.json"],
367 |             },
368 |             ensure_ascii=False,
369 |         )
370 | 
371 |         mock_provider = create_mock_provider()
372 |         mock_provider.get_provider_type.return_value = Mock(value="google")
373 |         mock_provider.generate_content.return_value = Mock(
374 |             content=clarification_json, usage={}, model_name="gemini-2.5-flash", metadata={}
375 |         )
376 |         mock_get_provider.return_value = mock_provider
377 | 
378 |         # Mock expert analysis to avoid actual API calls
379 |         mock_expert_analysis.return_value = {
380 |             "status": "analysis_complete",
381 |             "raw_analysis": "I need to see the package.json file to analyze npm dependencies",
382 |         }
383 | 
384 |         # Ask about dependencies with only source files (using new workflow format)
385 |         result = await tool.execute(
386 |             {
387 |                 "step": "What npm packages and versions does this project use?",
388 |                 "step_number": 1,
389 |                 "total_steps": 1,
390 |                 "next_step_required": False,
391 |                 "findings": "Initial dependency analysis",
392 |                 "relevant_files": ["/absolute/path/src/index.js"],
393 |             }
394 |         )
395 | 
396 |         response = json.loads(result[0].text)
397 | 
398 |         # Workflow tools should either promote clarification status or handle it in expert analysis
399 |         if response["status"] == "files_required_to_continue":
400 |             # Clarification was properly promoted to main status
401 |             assert "mandatory_instructions" in response
402 |             assert "package.json" in response["mandatory_instructions"]
403 |             assert "files_needed" in response
404 |             assert "package.json" in response["files_needed"]
405 |             assert "package-lock.json" in response["files_needed"]
406 |         elif response["status"] == "calling_expert_analysis":
407 |             # Clarification may be handled in expert analysis section
408 |             if "expert_analysis" in response:
409 |                 expert_analysis = response["expert_analysis"]
410 |                 expert_content = str(expert_analysis)
411 |                 assert (
412 |                     "package.json" in expert_content
413 |                     or "dependencies" in expert_content
414 |                     or "files_required_to_continue" in expert_content
415 |                 )
416 |         else:
417 |             # Some other status - ensure it's a valid workflow response
418 |             assert "step_number" in response
419 | 
420 |     @pytest.mark.asyncio
421 |     @patch("tools.shared.base_tool.BaseTool.get_model_provider")
422 |     @patch("tools.workflow.workflow_mixin.BaseWorkflowMixin._call_expert_analysis")
423 |     async def test_multi_step_collaboration(self, mock_expert_analysis, mock_get_provider):
424 |         """Test a multi-step collaboration workflow"""
425 |         tool = AnalyzeTool()
426 | 
427 |         # Step 1: Initial request returns clarification needed
428 |         clarification_json = json.dumps(
429 |             {
430 |                 "status": "files_required_to_continue",
431 |                 "mandatory_instructions": "I need to see the configuration file to understand the connection settings",
432 |                 "files_needed": ["config.py"],
433 |             },
434 |             ensure_ascii=False,
435 |         )
436 | 
437 |         mock_provider = create_mock_provider()
438 |         mock_provider.get_provider_type.return_value = Mock(value="google")
439 |         mock_provider.generate_content.return_value = Mock(
440 |             content=clarification_json, usage={}, model_name="gemini-2.5-flash", metadata={}
441 |         )
442 |         mock_get_provider.return_value = mock_provider
443 | 
444 |         # Mock expert analysis to avoid actual API calls
445 |         mock_expert_analysis.return_value = {
446 |             "status": "analysis_complete",
447 |             "raw_analysis": "I need to see the configuration file to understand the database connection settings",
448 |         }
449 | 
450 |         result1 = await tool.execute(
451 |             {
452 |                 "step": "Analyze database connection timeout issue",
453 |                 "step_number": 1,
454 |                 "total_steps": 1,
455 |                 "next_step_required": False,
456 |                 "findings": "Initial database timeout analysis",
457 |                 "relevant_files": ["/logs/error.log"],
458 |             }
459 |         )
460 | 
461 |         response1 = json.loads(result1[0].text)
462 | 
463 |         # First call should either return clarification request or handle it in expert analysis
464 |         if response1["status"] == "files_required_to_continue":
465 |             # Clarification was properly promoted to main status
466 |             pass  # This is the expected behavior
467 |         elif response1["status"] == "calling_expert_analysis":
468 |             # Clarification may be handled in expert analysis section
469 |             if "expert_analysis" in response1:
470 |                 expert_analysis = response1["expert_analysis"]
471 |                 expert_content = str(expert_analysis)
472 |                 # Should contain some indication of clarification request
473 |                 assert (
474 |                     "config" in expert_content
475 |                     or "files_required_to_continue" in expert_content
476 |                     or "database" in expert_content
477 |                 )
478 |         else:
479 |             # Some other status - ensure it's a valid workflow response
480 |             assert "step_number" in response1
481 | 
482 |         # Step 2: Claude would provide additional context and re-invoke
483 |         # This simulates the second call with more context
484 |         final_response = """
485 |         ## Summary
486 |         The database connection timeout is caused by incorrect host configuration.
487 | 
488 |         ## Hypotheses (Ranked by Likelihood)
489 | 
490 |         ### 1. Incorrect Database Host (Confidence: High)
491 |         **Root Cause:** The config.py file shows the database host is set to 'localhost' but the database is running on a different server.
492 |         """
493 | 
494 |         mock_provider.generate_content.return_value = Mock(
495 |             content=final_response, usage={}, model_name="gemini-2.5-flash", metadata={}
496 |         )
497 | 
498 |         # Update expert analysis mock for second call
499 |         mock_expert_analysis.return_value = {
500 |             "status": "analysis_complete",
501 |             "raw_analysis": final_response,
502 |         }
503 | 
504 |         result2 = await tool.execute(
505 |             {
506 |                 "step": "Analyze database connection timeout issue with config file",
507 |                 "step_number": 1,
508 |                 "total_steps": 1,
509 |                 "next_step_required": False,
510 |                 "findings": "Analysis with configuration context",
511 |                 "relevant_files": ["/absolute/path/config.py", "/logs/error.log"],  # Additional context provided
512 |             }
513 |         )
514 | 
515 |         response2 = json.loads(result2[0].text)
516 | 
517 |         # Workflow tools should either return expert analysis or handle clarification properly
518 |         # Accept multiple valid statuses as the workflow can handle the additional context differently
519 |         # Include 'error' status in case API calls fail in test environment
520 |         assert response2["status"] in [
521 |             "calling_expert_analysis",
522 |             "files_required_to_continue",
523 |             "pause_for_analysis",
524 |             "error",
525 |         ]
526 | 
527 |         # Check that the response contains the expected content regardless of status
528 | 
529 |         # If expert analysis was performed, verify content is there
530 |         if "expert_analysis" in response2:
531 |             expert_analysis = response2["expert_analysis"]
532 |             if "raw_analysis" in expert_analysis:
533 |                 analysis_content = expert_analysis["raw_analysis"]
534 |                 assert (
535 |                     "incorrect host configuration" in analysis_content.lower() or "database" in analysis_content.lower()
536 |                 )
537 |         elif response2["status"] == "files_required_to_continue":
538 |             # If clarification is still being requested, ensure it's reasonable
539 |             # Since we provided config.py and error.log, workflow tool might still need more context
540 |             assert "step_number" in response2  # Should be valid workflow response
541 |         else:
542 |             # For other statuses, ensure basic workflow structure is maintained
543 |             assert "step_number" in response2
544 | 
```

--------------------------------------------------------------------------------
/tests/test_per_tool_model_defaults.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Test per-tool model default selection functionality
  3 | """
  4 | 
  5 | import json
  6 | import os
  7 | import shutil
  8 | import tempfile
  9 | from unittest.mock import MagicMock, patch
 10 | 
 11 | import pytest
 12 | 
 13 | from providers.registry import ModelProviderRegistry, ProviderType
 14 | from tools.analyze import AnalyzeTool
 15 | from tools.chat import ChatTool
 16 | from tools.codereview import CodeReviewTool
 17 | from tools.debug import DebugIssueTool
 18 | from tools.models import ToolModelCategory
 19 | from tools.precommit import PrecommitTool
 20 | from tools.shared.base_tool import BaseTool
 21 | from tools.shared.exceptions import ToolExecutionError
 22 | from tools.thinkdeep import ThinkDeepTool
 23 | 
 24 | 
 25 | class TestToolModelCategories:
 26 |     """Test that each tool returns the correct model category."""
 27 | 
 28 |     def test_thinkdeep_category(self):
 29 |         tool = ThinkDeepTool()
 30 |         assert tool.get_model_category() == ToolModelCategory.EXTENDED_REASONING
 31 | 
 32 |     def test_debug_category(self):
 33 |         tool = DebugIssueTool()
 34 |         assert tool.get_model_category() == ToolModelCategory.EXTENDED_REASONING
 35 | 
 36 |     def test_analyze_category(self):
 37 |         tool = AnalyzeTool()
 38 |         assert tool.get_model_category() == ToolModelCategory.EXTENDED_REASONING
 39 | 
 40 |     def test_precommit_category(self):
 41 |         tool = PrecommitTool()
 42 |         assert tool.get_model_category() == ToolModelCategory.EXTENDED_REASONING
 43 | 
 44 |     def test_chat_category(self):
 45 |         tool = ChatTool()
 46 |         assert tool.get_model_category() == ToolModelCategory.FAST_RESPONSE
 47 | 
 48 |     def test_codereview_category(self):
 49 |         tool = CodeReviewTool()
 50 |         assert tool.get_model_category() == ToolModelCategory.EXTENDED_REASONING
 51 | 
 52 |     def test_base_tool_default_category(self):
 53 |         # Test that BaseTool defaults to BALANCED
 54 |         class TestTool(BaseTool):
 55 |             def get_name(self):
 56 |                 return "test"
 57 | 
 58 |             def get_description(self):
 59 |                 return "test"
 60 | 
 61 |             def get_input_schema(self):
 62 |                 return {}
 63 | 
 64 |             def get_system_prompt(self):
 65 |                 return "test"
 66 | 
 67 |             def get_request_model(self):
 68 |                 return MagicMock
 69 | 
 70 |             async def prepare_prompt(self, request):
 71 |                 return "test"
 72 | 
 73 |         tool = TestTool()
 74 |         assert tool.get_model_category() == ToolModelCategory.BALANCED
 75 | 
 76 | 
 77 | class TestModelSelection:
 78 |     """Test model selection based on tool categories."""
 79 | 
 80 |     def teardown_method(self):
 81 |         """Clean up after each test to prevent state pollution."""
 82 |         ModelProviderRegistry.clear_cache()
 83 |         # Unregister all providers
 84 |         for provider_type in list(ProviderType):
 85 |             ModelProviderRegistry.unregister_provider(provider_type)
 86 | 
 87 |     def test_extended_reasoning_with_openai(self):
 88 |         """Test EXTENDED_REASONING with OpenAI provider."""
 89 |         # Setup with only OpenAI provider
 90 |         ModelProviderRegistry.clear_cache()
 91 |         # First unregister all providers to ensure isolation
 92 |         for provider_type in list(ProviderType):
 93 |             ModelProviderRegistry.unregister_provider(provider_type)
 94 | 
 95 |         with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
 96 |             from providers.openai import OpenAIModelProvider
 97 | 
 98 |             ModelProviderRegistry.register_provider(ProviderType.OPENAI, OpenAIModelProvider)
 99 | 
100 |             model = ModelProviderRegistry.get_preferred_fallback_model(ToolModelCategory.EXTENDED_REASONING)
101 |             # OpenAI prefers GPT-5-Codex for extended reasoning (coding tasks)
102 |             assert model == "gpt-5-codex"
103 | 
104 |     def test_extended_reasoning_with_gemini_only(self):
105 |         """Test EXTENDED_REASONING prefers pro when only Gemini is available."""
106 |         # Clear cache and unregister all providers first
107 |         ModelProviderRegistry.clear_cache()
108 |         for provider_type in list(ProviderType):
109 |             ModelProviderRegistry.unregister_provider(provider_type)
110 | 
111 |         # Register only Gemini provider
112 |         with patch.dict(os.environ, {"GOOGLE_API_KEY": "test-key"}, clear=False):
113 |             from providers.gemini import GeminiModelProvider
114 | 
115 |             ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
116 | 
117 |             model = ModelProviderRegistry.get_preferred_fallback_model(ToolModelCategory.EXTENDED_REASONING)
118 |             # Gemini should return one of its models for extended reasoning
119 |             # The default behavior may return flash when pro is not explicitly preferred
120 |             assert model in ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"]
121 | 
122 |     def test_fast_response_with_openai(self):
123 |         """Test FAST_RESPONSE with OpenAI provider."""
124 |         # Setup with only OpenAI provider
125 |         ModelProviderRegistry.clear_cache()
126 |         # First unregister all providers to ensure isolation
127 |         for provider_type in list(ProviderType):
128 |             ModelProviderRegistry.unregister_provider(provider_type)
129 | 
130 |         with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
131 |             from providers.openai import OpenAIModelProvider
132 | 
133 |             ModelProviderRegistry.register_provider(ProviderType.OPENAI, OpenAIModelProvider)
134 | 
135 |             model = ModelProviderRegistry.get_preferred_fallback_model(ToolModelCategory.FAST_RESPONSE)
136 |             # OpenAI now prefers gpt-5 for fast response (based on our new preference order)
137 |             assert model == "gpt-5"
138 | 
139 |     def test_fast_response_with_gemini_only(self):
140 |         """Test FAST_RESPONSE prefers flash when only Gemini is available."""
141 |         # Clear cache and unregister all providers first
142 |         ModelProviderRegistry.clear_cache()
143 |         for provider_type in list(ProviderType):
144 |             ModelProviderRegistry.unregister_provider(provider_type)
145 | 
146 |         # Register only Gemini provider
147 |         with patch.dict(os.environ, {"GOOGLE_API_KEY": "test-key"}, clear=False):
148 |             from providers.gemini import GeminiModelProvider
149 | 
150 |             ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
151 | 
152 |             model = ModelProviderRegistry.get_preferred_fallback_model(ToolModelCategory.FAST_RESPONSE)
153 |             # Gemini should return one of its models for fast response
154 |             assert model in ["gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.5-pro"]
155 | 
156 |     def test_balanced_category_fallback(self):
157 |         """Test BALANCED category uses existing logic."""
158 |         # Setup with only OpenAI provider
159 |         ModelProviderRegistry.clear_cache()
160 |         # First unregister all providers to ensure isolation
161 |         for provider_type in list(ProviderType):
162 |             ModelProviderRegistry.unregister_provider(provider_type)
163 | 
164 |         with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
165 |             from providers.openai import OpenAIModelProvider
166 | 
167 |             ModelProviderRegistry.register_provider(ProviderType.OPENAI, OpenAIModelProvider)
168 | 
169 |             model = ModelProviderRegistry.get_preferred_fallback_model(ToolModelCategory.BALANCED)
170 |             # OpenAI prefers gpt-5 for balanced (based on our new preference order)
171 |             assert model == "gpt-5"
172 | 
173 |     def test_no_category_uses_balanced_logic(self):
174 |         """Test that no category specified uses balanced logic."""
175 |         # Setup with only Gemini provider
176 |         with patch.dict(os.environ, {"GEMINI_API_KEY": "test-key"}, clear=False):
177 |             from providers.gemini import GeminiModelProvider
178 | 
179 |             ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
180 | 
181 |             model = ModelProviderRegistry.get_preferred_fallback_model()
182 |             # Should pick flash for balanced use
183 |             assert model == "gemini-2.5-flash"
184 | 
185 | 
186 | class TestFlexibleModelSelection:
187 |     """Test that model selection handles various naming scenarios."""
188 | 
189 |     def test_fallback_handles_mixed_model_names(self):
190 |         """Test that fallback selection works with different providers."""
191 |         # Test with different provider configurations
192 |         test_cases = [
193 |             # Case 1: OpenAI provider for extended reasoning
194 |             {
195 |                 "env": {"OPENAI_API_KEY": "test-key"},
196 |                 "provider_type": ProviderType.OPENAI,
197 |                 "category": ToolModelCategory.EXTENDED_REASONING,
198 |                 "expected": "gpt-5-codex",  # GPT-5-Codex prioritized for coding tasks
199 |             },
200 |             # Case 2: Gemini provider for fast response
201 |             {
202 |                 "env": {"GEMINI_API_KEY": "test-key"},
203 |                 "provider_type": ProviderType.GOOGLE,
204 |                 "category": ToolModelCategory.FAST_RESPONSE,
205 |                 "expected": "gemini-2.5-flash",
206 |             },
207 |             # Case 3: OpenAI provider for fast response
208 |             {
209 |                 "env": {"OPENAI_API_KEY": "test-key"},
210 |                 "provider_type": ProviderType.OPENAI,
211 |                 "category": ToolModelCategory.FAST_RESPONSE,
212 |                 "expected": "gpt-5",  # Based on new preference order
213 |             },
214 |         ]
215 | 
216 |         for case in test_cases:
217 |             # Clear registry for clean test
218 |             ModelProviderRegistry.clear_cache()
219 |             # First unregister all providers to ensure isolation
220 |             for provider_type in list(ProviderType):
221 |                 ModelProviderRegistry.unregister_provider(provider_type)
222 | 
223 |             with patch.dict(os.environ, case["env"], clear=False):
224 |                 # Register the appropriate provider
225 |                 if case["provider_type"] == ProviderType.OPENAI:
226 |                     from providers.openai import OpenAIModelProvider
227 | 
228 |                     ModelProviderRegistry.register_provider(ProviderType.OPENAI, OpenAIModelProvider)
229 |                 elif case["provider_type"] == ProviderType.GOOGLE:
230 |                     from providers.gemini import GeminiModelProvider
231 | 
232 |                     ModelProviderRegistry.register_provider(ProviderType.GOOGLE, GeminiModelProvider)
233 | 
234 |                 model = ModelProviderRegistry.get_preferred_fallback_model(case["category"])
235 |                 assert model == case["expected"], f"Failed for case: {case}, got {model}"
236 | 
237 | 
238 | class TestCustomProviderFallback:
239 |     """Test fallback to custom/openrouter providers."""
240 | 
241 |     def test_extended_reasoning_custom_fallback(self):
242 |         """Test EXTENDED_REASONING with custom provider."""
243 |         # Setup with custom provider
244 |         ModelProviderRegistry.clear_cache()
245 |         with patch.dict(os.environ, {"CUSTOM_API_URL": "http://localhost:11434", "CUSTOM_API_KEY": ""}, clear=False):
246 |             from providers.custom import CustomProvider
247 | 
248 |             ModelProviderRegistry.register_provider(ProviderType.CUSTOM, CustomProvider)
249 | 
250 |             provider = ModelProviderRegistry.get_provider(ProviderType.CUSTOM)
251 |             if provider:
252 |                 model = ModelProviderRegistry.get_preferred_fallback_model(ToolModelCategory.EXTENDED_REASONING)
253 |                 # Should get a model from custom provider
254 |                 assert model is not None
255 | 
256 |     def test_extended_reasoning_final_fallback(self):
257 |         """Test EXTENDED_REASONING falls back to default when no providers."""
258 |         # Clear all providers
259 |         ModelProviderRegistry.clear_cache()
260 |         for provider_type in list(
261 |             ModelProviderRegistry._instance._providers.keys() if ModelProviderRegistry._instance else []
262 |         ):
263 |             ModelProviderRegistry.unregister_provider(provider_type)
264 | 
265 |         model = ModelProviderRegistry.get_preferred_fallback_model(ToolModelCategory.EXTENDED_REASONING)
266 |         # Should fall back to hardcoded default
267 |         assert model == "gemini-2.5-flash"
268 | 
269 | 
270 | class TestAutoModeErrorMessages:
271 |     """Test that auto mode error messages include suggested models."""
272 | 
273 |     def teardown_method(self):
274 |         """Clean up after each test to prevent state pollution."""
275 |         # Clear provider registry singleton
276 |         ModelProviderRegistry._instance = None
277 | 
278 |     @pytest.mark.asyncio
279 |     async def test_chat_auto_error_message(self):
280 |         """Test Chat tool suggests appropriate model in auto mode."""
281 |         with patch("config.IS_AUTO_MODE", True):
282 |             with patch("config.DEFAULT_MODEL", "auto"):
283 |                 with patch.object(ModelProviderRegistry, "get_available_models") as mock_get_available:
284 |                     # Mock OpenAI models available
285 |                     mock_get_available.return_value = {
286 |                         "o3": ProviderType.OPENAI,
287 |                         "o3-mini": ProviderType.OPENAI,
288 |                         "o4-mini": ProviderType.OPENAI,
289 |                     }
290 | 
291 |                     # Mock the provider lookup to return None for auto model
292 |                     with patch.object(ModelProviderRegistry, "get_provider_for_model") as mock_get_provider_for:
293 |                         mock_get_provider_for.return_value = None
294 | 
295 |                         tool = ChatTool()
296 |                         temp_dir = tempfile.mkdtemp()
297 |                         try:
298 |                             with pytest.raises(ToolExecutionError) as exc_info:
299 |                                 await tool.execute(
300 |                                     {"prompt": "test", "model": "auto", "working_directory_absolute_path": temp_dir}
301 |                                 )
302 |                         finally:
303 |                             shutil.rmtree(temp_dir, ignore_errors=True)
304 | 
305 |                         error_output = json.loads(exc_info.value.payload)
306 |                         assert error_output["status"] == "error"
307 |                         assert "Model 'auto' is not available" in error_output["content"]
308 | 
309 | 
310 | # Removed TestFileContentPreparation class
311 | # The original test was using MagicMock which caused TypeErrors when comparing with integers
312 | # The test has been removed to avoid mocking issues and encourage real integration testing
313 | 
314 | 
315 | class TestProviderHelperMethods:
316 |     """Test the helper methods for finding models from custom/openrouter."""
317 | 
318 |     def test_extended_reasoning_with_custom_provider(self):
319 |         """Test extended reasoning model selection with custom provider."""
320 |         # Setup with custom provider
321 |         with patch.dict(os.environ, {"CUSTOM_API_URL": "http://localhost:11434", "CUSTOM_API_KEY": ""}, clear=False):
322 |             from providers.custom import CustomProvider
323 | 
324 |             ModelProviderRegistry.register_provider(ProviderType.CUSTOM, CustomProvider)
325 | 
326 |             provider = ModelProviderRegistry.get_provider(ProviderType.CUSTOM)
327 |             if provider:
328 |                 # Custom provider should return a model for extended reasoning
329 |                 model = ModelProviderRegistry.get_preferred_fallback_model(ToolModelCategory.EXTENDED_REASONING)
330 |                 assert model is not None
331 | 
332 |     def test_extended_reasoning_with_openrouter(self):
333 |         """Test extended reasoning model selection with OpenRouter."""
334 |         # Setup with OpenRouter provider
335 |         with patch.dict(os.environ, {"OPENROUTER_API_KEY": "test-key"}, clear=False):
336 |             from providers.openrouter import OpenRouterProvider
337 | 
338 |             ModelProviderRegistry.register_provider(ProviderType.OPENROUTER, OpenRouterProvider)
339 | 
340 |             # OpenRouter should provide a model for extended reasoning
341 |             model = ModelProviderRegistry.get_preferred_fallback_model(ToolModelCategory.EXTENDED_REASONING)
342 |             # Should return first available OpenRouter model
343 |             assert model is not None
344 | 
345 |     def test_fallback_when_no_providers_available(self):
346 |         """Test fallback when no providers are available."""
347 |         # Clear all providers
348 |         ModelProviderRegistry.clear_cache()
349 |         for provider_type in list(
350 |             ModelProviderRegistry._instance._providers.keys() if ModelProviderRegistry._instance else []
351 |         ):
352 |             ModelProviderRegistry.unregister_provider(provider_type)
353 | 
354 |         # Should return hardcoded fallback
355 |         model = ModelProviderRegistry.get_preferred_fallback_model(ToolModelCategory.EXTENDED_REASONING)
356 |         assert model == "gemini-2.5-flash"
357 | 
358 | 
359 | class TestEffectiveAutoMode:
360 |     """Test the is_effective_auto_mode method."""
361 | 
362 |     def test_explicit_auto_mode(self):
363 |         """Test when DEFAULT_MODEL is explicitly 'auto'."""
364 |         with patch("config.DEFAULT_MODEL", "auto"):
365 |             with patch("config.IS_AUTO_MODE", True):
366 |                 tool = ChatTool()
367 |                 assert tool.is_effective_auto_mode() is True
368 | 
369 |     def test_unavailable_model_triggers_auto_mode(self):
370 |         """Test when DEFAULT_MODEL is set but not available."""
371 |         with patch("config.DEFAULT_MODEL", "o3"):
372 |             with patch("config.IS_AUTO_MODE", False):
373 |                 with patch.object(ModelProviderRegistry, "get_provider_for_model") as mock_get_provider:
374 |                     mock_get_provider.return_value = None  # Model not available
375 | 
376 |                     tool = ChatTool()
377 |                     assert tool.is_effective_auto_mode() is True
378 | 
379 |     def test_available_model_no_auto_mode(self):
380 |         """Test when DEFAULT_MODEL is set and available."""
381 |         with patch("config.DEFAULT_MODEL", "pro"):
382 |             with patch("config.IS_AUTO_MODE", False):
383 |                 with patch.object(ModelProviderRegistry, "get_provider_for_model") as mock_get_provider:
384 |                     mock_get_provider.return_value = MagicMock()  # Model is available
385 | 
386 |                     tool = ChatTool()
387 |                     assert tool.is_effective_auto_mode() is False
388 | 
389 | 
390 | class TestRuntimeModelSelection:
391 |     """Test runtime model selection behavior."""
392 | 
393 |     def teardown_method(self):
394 |         """Clean up after each test to prevent state pollution."""
395 |         # Clear provider registry singleton
396 |         ModelProviderRegistry._instance = None
397 | 
398 |     @pytest.mark.asyncio
399 |     async def test_explicit_auto_in_request(self):
400 |         """Test when Claude explicitly passes model='auto'."""
401 |         with patch("config.DEFAULT_MODEL", "pro"):  # DEFAULT_MODEL is a real model
402 |             with patch("config.IS_AUTO_MODE", False):  # Not in auto mode
403 |                 tool = ThinkDeepTool()
404 |                 result = await tool.execute(
405 |                     {
406 |                         "step": "test",
407 |                         "step_number": 1,
408 |                         "total_steps": 1,
409 |                         "next_step_required": False,
410 |                         "findings": "test",
411 |                         "model": "auto",
412 |                     }
413 |                 )
414 | 
415 |                 assert len(result) == 1
416 |                 assert "Model 'auto' is not available" in result[0].text
417 | 
418 |     @pytest.mark.asyncio
419 |     async def test_unavailable_model_in_request(self):
420 |         """Test when Claude passes an unavailable model."""
421 |         with patch("config.DEFAULT_MODEL", "pro"):
422 |             with patch("config.IS_AUTO_MODE", False):
423 |                 with patch.object(ModelProviderRegistry, "get_provider_for_model") as mock_get_provider:
424 |                     # Model is not available
425 |                     mock_get_provider.return_value = None
426 | 
427 |                     tool = ChatTool()
428 |                     temp_dir = tempfile.mkdtemp()
429 |                     try:
430 |                         with pytest.raises(ToolExecutionError) as exc_info:
431 |                             await tool.execute(
432 |                                 {"prompt": "test", "model": "gpt-5-turbo", "working_directory_absolute_path": temp_dir}
433 |                             )
434 |                     finally:
435 |                         shutil.rmtree(temp_dir, ignore_errors=True)
436 | 
437 |                     # Should require model selection
438 |                     error_output = json.loads(exc_info.value.payload)
439 |                     assert error_output["status"] == "error"
440 |                     assert "gpt-5-turbo" in error_output["content"]
441 |                     assert "is not available" in error_output["content"]
442 | 
443 | 
444 | class TestSchemaGeneration:
445 |     """Test schema generation with different configurations."""
446 | 
447 |     def test_schema_with_explicit_auto_mode(self):
448 |         """Test schema when DEFAULT_MODEL='auto'."""
449 |         with patch("config.DEFAULT_MODEL", "auto"):
450 |             with patch("config.IS_AUTO_MODE", True):
451 |                 tool = ChatTool()
452 |                 schema = tool.get_input_schema()
453 | 
454 |                 # Model should be required
455 |                 assert "model" in schema["required"]
456 | 
457 |     def test_schema_with_unavailable_default_model(self):
458 |         """Test schema when DEFAULT_MODEL is set but unavailable."""
459 |         with patch("config.DEFAULT_MODEL", "o3"):
460 |             with patch("config.IS_AUTO_MODE", False):
461 |                 with patch.object(ModelProviderRegistry, "get_provider_for_model") as mock_get_provider:
462 |                     mock_get_provider.return_value = None  # Model not available
463 | 
464 |                     tool = AnalyzeTool()
465 |                     schema = tool.get_input_schema()
466 | 
467 |                     # Model should be required due to unavailable DEFAULT_MODEL
468 |                     assert "model" in schema["required"]
469 | 
470 |     def test_schema_with_available_default_model(self):
471 |         """Test schema when DEFAULT_MODEL is available."""
472 |         with patch("config.DEFAULT_MODEL", "pro"):
473 |             with patch("config.IS_AUTO_MODE", False):
474 |                 with patch.object(ModelProviderRegistry, "get_provider_for_model") as mock_get_provider:
475 |                     mock_get_provider.return_value = MagicMock()  # Model is available
476 | 
477 |                     tool = ThinkDeepTool()
478 |                     schema = tool.get_input_schema()
479 | 
480 |                     # Model should remain optional when DEFAULT_MODEL is available
481 |                     assert "model" not in schema["required"]
482 | 
483 | 
484 | class TestUnavailableModelFallback:
485 |     """Test fallback behavior when DEFAULT_MODEL is not available."""
486 | 
487 |     @pytest.mark.asyncio
488 |     async def test_unavailable_default_model_fallback(self):
489 |         """Test that unavailable DEFAULT_MODEL triggers auto mode behavior."""
490 |         with patch("config.DEFAULT_MODEL", "o3"):  # Set DEFAULT_MODEL to a specific model
491 |             with patch("config.IS_AUTO_MODE", False):  # Not in auto mode
492 |                 with patch.object(ModelProviderRegistry, "get_provider_for_model") as mock_get_provider:
493 |                     # Model is not available (no provider)
494 |                     mock_get_provider.return_value = None
495 | 
496 |                     tool = ThinkDeepTool()
497 |                     result = await tool.execute(
498 |                         {
499 |                             "step": "test",
500 |                             "step_number": 1,
501 |                             "total_steps": 1,
502 |                             "next_step_required": False,
503 |                             "findings": "test",
504 |                         }
505 |                     )  # No model specified
506 | 
507 |                     # Should get model error since fallback model is also unavailable
508 |                     assert len(result) == 1
509 |                     # Workflow tools try fallbacks and report when the fallback model is not available
510 |                     assert "is not available" in result[0].text
511 |                     # Should list available models in the error
512 |                     assert "Available models:" in result[0].text
513 | 
514 |     @pytest.mark.asyncio
515 |     async def test_available_default_model_no_fallback(self):
516 |         """Test that available DEFAULT_MODEL works normally."""
517 |         with patch("config.DEFAULT_MODEL", "pro"):
518 |             with patch("config.IS_AUTO_MODE", False):
519 |                 with patch.object(ModelProviderRegistry, "get_provider_for_model") as mock_get_provider:
520 |                     # Model is available
521 |                     mock_provider = MagicMock()
522 |                     mock_provider.generate_content.return_value = MagicMock(content="Test response", metadata={})
523 |                     mock_get_provider.return_value = mock_provider
524 | 
525 |                     # Mock the provider lookup in BaseTool.get_model_provider
526 |                     with patch.object(BaseTool, "get_model_provider") as mock_get_model_provider:
527 |                         mock_get_model_provider.return_value = mock_provider
528 | 
529 |                         tool = ChatTool()
530 |                         temp_dir = tempfile.mkdtemp()
531 |                         try:
532 |                             result = await tool.execute({"prompt": "test", "working_directory_absolute_path": temp_dir})
533 |                         finally:
534 |                             shutil.rmtree(temp_dir, ignore_errors=True)
535 | 
536 |                         # Should work normally, not require model parameter
537 |                         assert len(result) == 1
538 |                         output = json.loads(result[0].text)
539 |                         assert output["status"] in ["success", "continuation_available"]
540 |                         assert "Test response" in output["content"]
541 | 
```

--------------------------------------------------------------------------------
/docs/advanced-usage.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Advanced Usage Guide
  2 | 
  3 | This guide covers advanced features, configuration options, and workflows for power users of the Zen MCP server.
  4 | 
  5 | ## Table of Contents
  6 | 
  7 | - [Model Configuration](#model-configuration)
  8 | - [Model Usage Restrictions](#model-usage-restrictions)
  9 | - [Thinking Modes](#thinking-modes)
 10 | - [Tool Parameters](#tool-parameters)
 11 | - [Context Revival: AI Memory Beyond Context Limits](#context-revival-ai-memory-beyond-context-limits)
 12 | - [Collaborative Workflows](#collaborative-workflows)
 13 | - [Working with Large Prompts](#working-with-large-prompts)
 14 | - [Vision Support](#vision-support)
 15 | - [Web Search Integration](#web-search-integration)
 16 | - [System Prompts](#system-prompts)
 17 | 
 18 | ## Model Configuration
 19 | 
 20 | **For basic configuration**, see the [Configuration Guide](configuration.md) which covers API keys, model selection, and environment variables.
 21 | 
 22 | This section focuses on **advanced model usage patterns** for power users:
 23 | 
 24 | **Per-Request Model Override:**
 25 | Regardless of your default configuration, you can specify models per request:
 26 | - "Use **pro** for deep security analysis of auth.py"
 27 | - "Use **flash** to quickly format this code"
 28 | - "Use **o3** to debug this logic error"
 29 | - "Review with **o4-mini** for balanced analysis"
 30 | - "Use **gpt4.1** for comprehensive codebase analysis"
 31 | 
 32 | **Claude's Auto Mode Decision Matrix:**
 33 | 
 34 | | Model | Provider | Context | Strengths | Auto Mode Usage |
 35 | |-------|----------|---------|-----------|------------------|
 36 | | **`pro`** (Gemini 2.5 Pro) | Google | 1M tokens | Extended thinking (up to 32K tokens), deep analysis | Complex architecture, security reviews, deep debugging |
 37 | | **`flash`** (Gemini 2.5 Flash) | Google | 1M tokens | Ultra-fast responses with thinking | Quick checks, formatting, simple analysis |
 38 | | **`flash-2.0`** (Gemini 2.0 Flash) | Google | 1M tokens | Latest fast model with audio/video support | Quick analysis with multimodal input |
 39 | | **`flashlite`** (Gemini 2.0 Flash Lite) | Google | 1M tokens | Lightweight text-only model | Fast text processing without vision |
 40 | | **`o3`** | OpenAI | 200K tokens | Strong logical reasoning | Debugging logic errors, systematic analysis |
 41 | | **`o3-mini`** | OpenAI | 200K tokens | Balanced speed/quality | Moderate complexity tasks |
 42 | | **`o4-mini`** | OpenAI | 200K tokens | Latest reasoning model | Optimized for shorter contexts |
 43 | | **`gpt4.1`** | OpenAI | 1M tokens | Latest GPT-4 with extended context | Large codebase analysis, comprehensive reviews |
 44 | | **`gpt5`** (GPT-5) | OpenAI | 400K tokens | Advanced model with reasoning support | Complex problems requiring advanced reasoning |
 45 | | **`gpt5-mini`** (GPT-5 Mini) | OpenAI | 400K tokens | Efficient variant with reasoning | Balanced performance and capability |
 46 | | **`gpt5-nano`** (GPT-5 Nano) | OpenAI | 400K tokens | Fastest, cheapest GPT-5 variant | Summarization and classification tasks |
 47 | | **`grok-4`** | X.AI | 256K tokens | Latest flagship Grok model with reasoning, vision | Complex analysis, reasoning tasks |
 48 | | **`grok-3`** | X.AI | 131K tokens | Advanced reasoning model | Deep analysis, complex problems |
 49 | | **`grok-3-fast`** | X.AI | 131K tokens | Higher performance variant | Fast responses with reasoning |
 50 | | **`llama`** (Llama 3.2) | Custom/Local | 128K tokens | Local inference, privacy | On-device analysis, cost-free processing |
 51 | | **Any model** | OpenRouter | Varies | Access to GPT-4, Claude, Llama, etc. | User-specified or based on task requirements |
 52 | 
 53 | **Mix & Match Providers:** Use multiple providers simultaneously! Set both `OPENROUTER_API_KEY` and `CUSTOM_API_URL` to access 
 54 | cloud models (expensive/powerful) AND local models (free/private) in the same conversation.
 55 | 
 56 | **Model Capabilities:**
 57 | - **Gemini Models**: Support thinking modes (minimal to max), web search, 1M context
 58 |   - **Pro 2.5**: Deep analysis with max 32K thinking tokens
 59 |   - **Flash 2.5**: Ultra-fast with thinking support (24K thinking tokens)
 60 |   - **Flash 2.0**: Latest fast model with audio/video input (24K thinking tokens)
 61 |   - **Flash Lite 2.0**: Text-only lightweight model (no thinking support)
 62 | - **O3/O4 Models**: Excellent reasoning, systematic analysis, 200K context
 63 | - **GPT-4.1**: Extended context window (1M tokens), general capabilities
 64 | - **GPT-5 Series**: Advanced reasoning models, 400K context
 65 |   - **GPT-5**: Full-featured with reasoning support and vision
 66 |   - **GPT-5 Mini**: Balanced efficiency and capability
 67 |   - **GPT-5 Nano**: Optimized for fast, low-cost tasks
 68 | - **Grok-4**: Extended thinking support, vision capabilities, 256K context
 69 | - **Grok-3 Models**: Advanced reasoning, 131K context
 70 | 
 71 | ## Model Usage Restrictions
 72 | 
 73 | **For complete restriction configuration**, see the [Configuration Guide](configuration.md#model-usage-restrictions).
 74 | 
 75 | **Advanced Restriction Strategies:**
 76 | 
 77 | **Cost Control Examples:**
 78 | ```env
 79 | # Development: Allow experimentation
 80 | GOOGLE_ALLOWED_MODELS=flash,pro
 81 | OPENAI_ALLOWED_MODELS=o4-mini,o3-mini
 82 | 
 83 | # Production: Cost-optimized  
 84 | GOOGLE_ALLOWED_MODELS=flash
 85 | OPENAI_ALLOWED_MODELS=o4-mini
 86 | 
 87 | # High-performance: Quality over cost
 88 | GOOGLE_ALLOWED_MODELS=pro
 89 | OPENAI_ALLOWED_MODELS=o3,o4-mini
 90 | ```
 91 | 
 92 | **Important Notes:**
 93 | - Restrictions apply to all usage including auto mode
 94 | - `OPENROUTER_ALLOWED_MODELS` only affects models defined in `conf/openrouter_models.json`
 95 | - Custom local models (from `conf/custom_models.json`) are not affected by OpenRouter restrictions
 96 | 
 97 | ## Thinking Modes
 98 | 
 99 | **Claude automatically manages thinking modes based on task complexity**, but you can also manually control Gemini's reasoning depth to balance between response quality and token consumption. Each thinking mode uses a different amount of tokens, directly affecting API costs and response time.
100 | 
101 | ### Thinking Modes & Token Budgets
102 | 
103 | These only apply to models that support customizing token usage for extended thinking, such as Gemini 2.5 Pro.
104 | 
105 | | Mode | Token Budget | Use Case | Cost Impact |
106 | |------|-------------|----------|-------------|
107 | | `minimal` | 128 tokens | Simple, straightforward tasks | Lowest cost |
108 | | `low` | 2,048 tokens | Basic reasoning tasks | 16x more than minimal |
109 | | `medium` | 8,192 tokens | **Default** - Most development tasks | 64x more than minimal |
110 | | `high` | 16,384 tokens | Complex problems requiring thorough analysis (default for `thinkdeep`) | 128x more than minimal |
111 | | `max` | 32,768 tokens | Exhaustive reasoning | 256x more than minimal |
112 | 
113 | ### How to Use Thinking Modes
114 | 
115 | **Claude automatically selects appropriate thinking modes**, but you can override this by explicitly requesting a specific mode in your prompts. Remember: higher thinking modes = more tokens = higher cost but better quality:
116 | 
117 | #### Optimizing Token Usage & Costs
118 | 
119 | **In most cases, let Claude automatically manage thinking modes** for optimal balance of cost and quality. Override manually when you have specific requirements:
120 | 
121 | **Use lower modes (`minimal`, `low`) to save tokens when:**
122 | - Doing simple formatting or style checks
123 | - Getting quick explanations of basic concepts
124 | - Working with straightforward code
125 | - You need faster responses
126 | - Working within tight token budgets
127 | 
128 | **Use higher modes (`high`, `max`) when quality justifies the cost:**
129 | - Debugging complex issues (worth the extra tokens to find root causes)
130 | - Reviewing security-critical code (cost of tokens < cost of vulnerabilities)
131 | - Analyzing system architecture (comprehensive analysis saves development time)
132 | - Finding subtle bugs or edge cases
133 | - Working on performance optimizations
134 | 
135 | **Token Cost Examples:**
136 | - `minimal` (128 tokens) vs `max` (32,768 tokens) = 256x difference in thinking tokens
137 | - For a simple formatting check, using `minimal` instead of the default `medium` saves ~8,000 thinking tokens
138 | - For critical security reviews, the extra tokens in `high` or `max` mode are a worthwhile investment
139 | 
140 | **Examples by scenario:**
141 | ```
142 | # Quick style check with o3
143 | "Use flash to review formatting in utils.py"
144 | 
145 | # Security audit with o3
146 | "Get o3 to do a security review of auth/ with thinking mode high"
147 | 
148 | # Complex debugging, letting claude pick the best model
149 | "Use zen to debug this race condition with max thinking mode"
150 | 
151 | # Architecture analysis with Gemini 2.5 Pro
152 | "Analyze the entire src/ directory architecture with high thinking using pro"
153 | ```
154 | 
155 | ## Tool Parameters
156 | 
157 | All tools that work with files support **both individual files and entire directories**. The server automatically expands directories, filters for relevant code files, and manages token limits.
158 | 
159 | ### File-Processing Tools
160 | 
161 | **`analyze`** - Analyze files or directories
162 | - `files`: List of file paths or directories (required)
163 | - `question`: What to analyze (required)  
164 | - `model`: auto|pro|flash|flash-2.0|flashlite|o3|o3-mini|o4-mini|gpt4.1|gpt5|gpt5-mini|gpt5-nano (default: server default)
165 | - `analysis_type`: architecture|performance|security|quality|general
166 | - `output_format`: summary|detailed|actionable
167 | - `thinking_mode`: minimal|low|medium|high|max (default: medium, Gemini only)
168 | - **Web search capability**: The assistant now automatically requests web searches when it needs current documentation or best practices—no parameter required
169 | 
170 | ```
171 | "Analyze the src/ directory for architectural patterns" (auto mode picks best model)
172 | "Use flash to quickly analyze main.py and tests/ to understand test coverage" 
173 | "Use o3 for logical analysis of the algorithm in backend/core.py"
174 | "Use pro for deep analysis of the entire backend/ directory structure"
175 | ```
176 | 
177 | **`codereview`** - Review code files or directories
178 | - `files`: List of file paths or directories (required)
179 | - `model`: auto|pro|flash|flash-2.0|flashlite|o3|o3-mini|o4-mini|gpt4.1|gpt5|gpt5-mini|gpt5-nano (default: server default)
180 | - `review_type`: full|security|performance|quick
181 | - `focus_on`: Specific aspects to focus on
182 | - `standards`: Coding standards to enforce
183 | - `severity_filter`: critical|high|medium|all
184 | - `thinking_mode`: minimal|low|medium|high|max (default: medium, Gemini only)
185 | 
186 | ```
187 | "Review the entire api/ directory for security issues" (auto mode picks best model)
188 | "Use pro to review auth/ for deep security analysis"
189 | "Use o3 to review logic in algorithms/ for correctness"
190 | "Use flash to quickly review src/ with focus on performance, only show critical issues"
191 | ```
192 | 
193 | **`debug`** - Debug with file context
194 | - `error_description`: Description of the issue (required)
195 | - `model`: auto|pro|flash|flash-2.0|flashlite|o3|o3-mini|o4-mini|gpt4.1|gpt5|gpt5-mini|gpt5-nano (default: server default)
196 | - `error_context`: Stack trace or logs
197 | - `files`: Files or directories related to the issue
198 | - `runtime_info`: Environment details
199 | - `previous_attempts`: What you've tried
200 | - `thinking_mode`: minimal|low|medium|high|max (default: medium, Gemini only)
201 | - **Web search capability**: Automatically initiates searches for relevant error messages or recent fixes when needed
202 | 
203 | ```
204 | "Debug this logic error with context from backend/" (auto mode picks best model)
205 | "Use o3 to debug this algorithm correctness issue"
206 | "Use pro to debug this complex architecture problem"
207 | ```
208 | 
209 | **`thinkdeep`** - Extended analysis with file context
210 | - `current_analysis`: Your current thinking (required)
211 | - `model`: auto|pro|flash|flash-2.0|flashlite|o3|o3-mini|o4-mini|gpt4.1|gpt5|gpt5-mini|gpt5-nano (default: server default)
212 | - `problem_context`: Additional context
213 | - `focus_areas`: Specific aspects to focus on
214 | - `files`: Files or directories for context
215 | - `thinking_mode`: minimal|low|medium|high|max (default: max, Gemini only)
216 | - **Web search capability**: Automatically calls for research when architecture references or external insights are required
217 | 
218 | ```
219 | "Think deeper about my design with reference to src/models/" (auto mode picks best model)
220 | "Use pro to think deeper about this architecture with extended thinking"
221 | "Use o3 to think deeper about the logical flow in this algorithm"
222 | ```
223 | 
224 | **`testgen`** - Comprehensive test generation with edge case coverage
225 | - `files`: Code files or directories to generate tests for (required)
226 | - `prompt`: Description of what to test, testing objectives, and scope (required)
227 | - `model`: auto|pro|flash|flash-2.0|flashlite|o3|o3-mini|o4-mini|gpt4.1|gpt5|gpt5-mini|gpt5-nano (default: server default)
228 | - `test_examples`: Optional existing test files as style/pattern reference
229 | - `thinking_mode`: minimal|low|medium|high|max (default: medium, Gemini only)
230 | 
231 | ```
232 | "Generate tests for User.login() method with edge cases" (auto mode picks best model)
233 | "Use pro to generate comprehensive tests for src/payment.py with max thinking mode"
234 | "Use o3 to generate tests for algorithm correctness in sort_functions.py"
235 | "Generate tests following patterns from tests/unit/ for new auth module"
236 | ```
237 | 
238 | **`refactor`** - Intelligent code refactoring with decomposition focus
239 | - `files`: Code files or directories to analyze for refactoring opportunities (required)
240 | - `prompt`: Description of refactoring goals, context, and specific areas of focus (required)
241 | - `refactor_type`: codesmells|decompose|modernize|organization (required)
242 | - `model`: auto|pro|flash|flash-2.0|flashlite|o3|o3-mini|o4-mini|gpt4.1|gpt5|gpt5-mini|gpt5-nano (default: server default)
243 | - `focus_areas`: Specific areas to focus on (e.g., 'performance', 'readability', 'maintainability', 'security')
244 | - `style_guide_examples`: Optional existing code files to use as style/pattern reference
245 | - `thinking_mode`: minimal|low|medium|high|max (default: medium, Gemini only)
246 | - `continuation_id`: Thread continuation ID for multi-turn conversations
247 | 
248 | ```
249 | "Analyze legacy codebase for decomposition opportunities" (auto mode picks best model)
250 | "Use pro to identify code smells in the authentication module with max thinking mode"
251 | "Use pro to modernize this JavaScript code following examples/modern-patterns.js"
252 | "Refactor src/ for better organization, focus on maintainability and readability"
253 | ```
254 | 
255 | ## Context Revival: AI Memory Beyond Context Limits
256 | 
257 | **The Zen MCP Server's most revolutionary feature** is its ability to maintain conversation context even after Claude's memory resets. This enables truly persistent AI collaboration across multiple sessions and context boundaries.
258 | 
259 | ### **The Breakthrough**
260 | 
261 | Even when Claude's context resets or compacts, conversations can continue seamlessly because other models (O3, Gemini) have access to the complete conversation history stored in memory and can "remind" Claude of everything that was discussed.
262 | 
263 | ### Key Benefits
264 | 
265 | - **Persistent conversations** across Claude's context resets
266 | - **Cross-tool continuation** with full context preservation
267 | - **Multi-session workflows** that maintain complete history
268 | - **True AI orchestration** where models can build on each other's work
269 | - **Seamless handoffs** between different tools and models
270 | 
271 | ### Quick Example
272 | 
273 | ```
274 | Session 1: "Design a RAG system with gemini pro"
275 | [Claude's context resets]
276 | Session 2: "Continue our RAG discussion with o3"
277 | → O3 receives the full history and reminds Claude of everything discussed
278 | ```
279 | 
280 | **📖 [Read the complete Context Revival guide](context-revival.md)** for detailed examples, technical architecture, configuration options, and best practices.
281 | 
282 | **See also:** [AI-to-AI Collaboration Guide](ai-collaboration.md) for multi-model coordination and conversation threading.
283 | 
284 | ## Collaborative Workflows
285 | 
286 | ### Design → Review → Implement
287 | ```
288 | Think hard about designing and developing a fun calculator app in swift. Review your design plans with o3, taking in
289 | their suggestions but keep the feature-set realistic and doable without adding bloat. Begin implementing and in between
290 | implementation, get a codereview done by Gemini Pro and chat with Flash if you need to for creative directions.   
291 | ```
292 | 
293 | ### Code → Review → Fix
294 | ```
295 | Implement a new screen where the locations taken from the database display on a map, with pins falling from
296 | the top and landing with animation. Once done, codereview with gemini pro and o3 both and ask them to critique your
297 | work. Fix medium to critical bugs / concerns / issues and show me the final product
298 | ```
299 | 
300 | ### Debug → Analyze → Solution → Precommit Check → Publish
301 | ```
302 | Take a look at these log files saved under subfolder/diagnostics.log there's a bug where the user says the app
303 | crashes at launch. Think hard and go over each line, tallying it with corresponding code within the project. After
304 | you've performed initial investigation, ask gemini pro to analyze the log files and the related code where you 
305 | suspect lies the bug and then formulate and implement a bare minimal fix. Must not regress. Perform a precommit
306 | with zen in the end using gemini pro to confirm we're okay to publish the fix 
307 | ```
308 | 
309 | ### Refactor → Review → Implement → Test
310 | ```
311 | Use zen to analyze this legacy authentication module for decomposition opportunities. The code is getting hard to 
312 | maintain and we need to break it down. Use gemini pro with high thinking mode to identify code smells and suggest 
313 | a modernization strategy. After reviewing the refactoring plan, implement the changes step by step and then 
314 | generate comprehensive tests with zen to ensure nothing breaks.
315 | ```
316 | 
317 | ### Tool Selection Guidance
318 | To help choose the right tool for your needs:
319 | 
320 | **Decision Flow:**
321 | 1. **Have a specific error/exception?** → Use `debug`
322 | 2. **Want to find bugs/issues in code?** → Use `codereview`
323 | 3. **Want to understand how code works?** → Use `analyze`
324 | 4. **Need comprehensive test coverage?** → Use `testgen`
325 | 5. **Want to refactor/modernize code?** → Use `refactor`
326 | 6. **Have analysis that needs extension/validation?** → Use `thinkdeep`
327 | 7. **Want to brainstorm or discuss?** → Use `chat`
328 | 
329 | **Key Distinctions:**
330 | - `analyze` vs `codereview`: analyze explains, codereview prescribes fixes
331 | - `chat` vs `thinkdeep`: chat is open-ended, thinkdeep extends specific analysis
332 | - `debug` vs `codereview`: debug diagnoses runtime errors, review finds static issues
333 | - `testgen` vs `debug`: testgen creates test suites, debug just finds issues and recommends solutions
334 | - `refactor` vs `codereview`: refactor suggests structural improvements, codereview finds bugs/issues
335 | - `refactor` vs `analyze`: refactor provides actionable refactoring steps, analyze provides understanding
336 | 
337 | ## Vision Support
338 | 
339 | The Zen MCP server supports vision-capable models for analyzing images, diagrams, screenshots, and visual content. Vision support works seamlessly with all tools and conversation threading.
340 | 
341 | **Supported Models:**
342 | - **Gemini 2.5 Pro & Flash**: Excellent for diagrams, architecture analysis, UI mockups (up to 20MB total)
343 | - **OpenAI O3/O4 series**: Strong for visual debugging, error screenshots (up to 20MB total)
344 | - **Claude models via OpenRouter**: Good for code screenshots, visual analysis (up to 5MB total)
345 | - **Custom models**: Support varies by model, with 40MB maximum enforced for abuse prevention
346 | 
347 | **Usage Examples:**
348 | ```bash
349 | # Debug with error screenshots
350 | "Use zen to debug this error with the stack trace screenshot and error.py"
351 | 
352 | # Architecture analysis with diagrams  
353 | "Analyze this system architecture diagram with gemini pro for bottlenecks"
354 | 
355 | # UI review with mockups
356 | "Chat with flash about this UI mockup - is the layout intuitive?"
357 | 
358 | # Code review with visual context
359 | "Review this authentication code along with the error dialog screenshot"
360 | ```
361 | 
362 | **Image Formats Supported:**
363 | - **Images**: JPG, PNG, GIF, WebP, BMP, SVG, TIFF
364 | - **Documents**: PDF (where supported by model)
365 | - **Data URLs**: Base64-encoded images from Claude
366 | 
367 | **Key Features:**
368 | - **Automatic validation**: File type, magic bytes, and size validation
369 | - **Conversation context**: Images persist across tool switches and continuation
370 | - **Budget management**: Automatic dropping of old images when limits exceeded
371 | - **Model capability-aware**: Only sends images to vision-capable models
372 | 
373 | **Best Practices:**
374 | - Describe images when including them: "screenshot of login error", "system architecture diagram"
375 | - Use appropriate models: Gemini for complex diagrams, O3 for debugging visuals
376 | - Consider image sizes: Larger images consume more of the model's capacity
377 | 
378 | ## Working with Large Prompts
379 | 
380 | The MCP protocol has a combined request+response limit of approximately 25K tokens. This server intelligently works around this limitation by automatically handling large prompts as files:
381 | 
382 | **How it works:**
383 | 1. When you send a prompt larger than the configured limit (default: 50K characters ~10-12K tokens), the server detects this
384 | 2. It responds with a special status asking Claude to save the prompt to a file named `prompt.txt`
385 | 3. Claude saves the prompt and resends the request with the file path instead
386 | 4. The server reads the file content directly into Gemini's 1M token context
387 | 5. The full MCP token capacity is preserved for the response
388 | 
389 | **Example scenario:**
390 | ```
391 | # You have a massive code review request with detailed context
392 | User: "Use gemini to review this code: [50,000+ character detailed analysis]"
393 | 
394 | # Server detects the large prompt and responds:
395 | Zen MCP: "The prompt is too large for MCP's token limits (>50,000 characters). 
396 | Please save the prompt text to a temporary file named 'prompt.txt' and resend 
397 | the request with an empty prompt string and the absolute file path included 
398 | in the absolute_file_paths parameter, along with any other files you wish to share as context."
399 | 
400 | # Claude automatically handles this:
401 | - Saves your prompt to /tmp/prompt.txt
402 | - Resends: "Use gemini to review this code" with absolute_file_paths=["/tmp/prompt.txt", "/path/to/code.py"]
403 | 
404 | # Server processes the large prompt through Gemini's 1M context
405 | # Returns comprehensive analysis within MCP's response limits
406 | ```
407 | 
408 | This feature ensures you can send arbitrarily large prompts to Gemini without hitting MCP's protocol limitations, while maximizing the available space for detailed responses.
409 | 
410 | ## Web Search Integration
411 | 
412 | **Smart web search recommendations for enhanced analysis**
413 | 
414 | Web search is now enabled by default for all tools. Instead of performing searches directly, Gemini intelligently analyzes when additional information from the web would enhance its response and provides specific search recommendations for Claude to execute.
415 | 
416 | **How it works:**
417 | 1. Gemini analyzes the request and identifies areas where current documentation, API references, or community solutions would be valuable
418 | 2. It provides its analysis based on its training data
419 | 3. If web searches would strengthen the analysis, Gemini includes a "Recommended Web Searches for Claude" section
420 | 4. Claude can then perform these searches and incorporate the findings
421 | 
422 | **Example:**
423 | ```
424 | User: "Use gemini to debug this FastAPI async error"
425 | 
426 | Gemini's Response:
427 | [... debugging analysis ...]
428 | 
429 | **Recommended Web Searches for Claude:**
430 | - "FastAPI async def vs def performance 2024" - to verify current best practices for async endpoints
431 | - "FastAPI BackgroundTasks memory leak" - to check for known issues with the version you're using
432 | - "FastAPI lifespan context manager pattern" - to explore proper resource management patterns
433 | 
434 | Claude can then search for these specific topics and provide you with the most current information.
435 | ```
436 | 
437 | **Benefits:**
438 | - Always access to latest documentation and best practices
439 | - Gemini focuses on reasoning about what information would help
440 | - Claude maintains control over actual web searches
441 | - More collaborative approach between the two AI assistants
442 | - Reduces hallucination by encouraging verification of assumptions
443 | 
444 | **Web search control:**
445 | Web search is enabled by default, allowing models to request Claude perform searches for current documentation and solutions. If you prefer the model to work only with its training data, you can disable web search:
446 | ```
447 | "Use gemini to review this code and confirm whether any new framework changes affect the recommendation"
448 | ```
449 | 
450 | ## System Prompts
451 | 
452 | The server uses carefully crafted system prompts to give each tool specialized expertise:
453 | 
454 | ### Prompt Architecture
455 | - **Centralized Prompts**: Each tool's system prompt lives in `systemprompts/` (for example, `systemprompts/chat_prompt.py`)
456 | - **Tool Integration**: Each tool inherits from `BaseTool` and implements `get_system_prompt()`
457 | - **Prompt Flow**: `User Request → Tool Selection → System Prompt + Context → Model Response`
458 | 
459 | ### Specialized Expertise
460 | Each tool has a unique system prompt that defines its role and approach:
461 | - **`thinkdeep`**: Acts as a senior development partner, challenging assumptions and finding edge cases
462 | - **`codereview`**: Expert code reviewer with security/performance focus, uses severity levels
463 | - **`debug`**: Systematic debugger providing root cause analysis and prevention strategies
464 | - **`analyze`**: Code analyst focusing on architecture, patterns, and actionable insights
465 | 
466 | ### Customization
467 | To modify tool behavior, you can:
468 | 1. Edit the prompt file in `systemprompts/` (and export it via `systemprompts/__init__.py`) for global changes
469 | 2. Override `get_system_prompt()` in a tool class for tool-specific changes
470 | 3. Use the `temperature` parameter to adjust response style (0.2 for focused, 0.7 for creative)
471 | 
```

--------------------------------------------------------------------------------
/simulator_tests/test_debug_certain_confidence.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Debug Tool Certain Confidence Simulator Test
  4 | 
  5 | Tests the debug tool's 'certain' confidence feature in a realistic simulation:
  6 | - Multi-step investigation leading to certain confidence
  7 | - Validation that expert analysis is skipped for obvious bugs
  8 | - Verification that certain confidence is always trusted
  9 | - Ensures token optimization works correctly for minimal fixes
 10 | """
 11 | 
 12 | import json
 13 | from typing import Optional
 14 | 
 15 | from tools.shared.exceptions import ToolExecutionError
 16 | 
 17 | from .conversation_base_test import ConversationBaseTest
 18 | 
 19 | 
 20 | class DebugCertainConfidenceTest(ConversationBaseTest):
 21 |     """Test debug tool's certain confidence optimization feature"""
 22 | 
 23 |     @property
 24 |     def test_name(self) -> str:
 25 |         return "debug_certain_confidence"
 26 | 
 27 |     @property
 28 |     def test_description(self) -> str:
 29 |         return "Debug tool certain confidence optimization validation"
 30 | 
 31 |     def run_test(self) -> bool:
 32 |         """Test debug tool certain confidence capabilities"""
 33 |         # Set up the test environment
 34 |         self.setUp()
 35 | 
 36 |         try:
 37 |             self.logger.info("Test: Debug tool certain confidence validation")
 38 | 
 39 |             # Create test files with obvious bugs for certain scenarios
 40 |             self._create_obvious_bug_scenarios()
 41 | 
 42 |             # Test 1: Obvious import error with certain confidence
 43 |             if not self._test_obvious_import_error_certain():
 44 |                 return False
 45 | 
 46 |             # Test 2: Certain confidence is always trusted
 47 |             if not self._test_certain_always_trusted():
 48 |                 return False
 49 | 
 50 |             # Test 3: Regular high confidence still triggers expert analysis
 51 |             if not self._test_regular_high_confidence_expert_analysis():
 52 |                 return False
 53 | 
 54 |             # Test 4: Multi-step investigation ending in certain
 55 |             if not self._test_multi_step_investigation_certain():
 56 |                 return False
 57 | 
 58 |             self.logger.info("  ✅ All debug certain confidence tests passed")
 59 |             return True
 60 | 
 61 |         except Exception as e:
 62 |             self.logger.error(f"Debug certain confidence test failed: {e}")
 63 |             return False
 64 | 
 65 |     def _create_obvious_bug_scenarios(self):
 66 |         """Create test files with obvious bugs perfect for certain confidence"""
 67 | 
 68 |         # Scenario 1: Missing import statement (very obvious)
 69 |         missing_import_code = """#!/usr/bin/env python3
 70 | import os
 71 | import sys
 72 | # import hashlib  # <-- Missing import!
 73 | 
 74 | class UserAuth:
 75 |     def __init__(self, secret_key):
 76 |         self.secret_key = secret_key
 77 | 
 78 |     def hash_password(self, password):
 79 |         # This will fail with NameError: name 'hashlib' is not defined
 80 |         salt = os.urandom(32)
 81 |         return hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
 82 | 
 83 |     def verify_password(self, password, stored_hash):
 84 |         # This function also uses hashlib
 85 |         return hashlib.pbkdf2_hmac('sha256', password.encode(), stored_hash[:32], 100000) == stored_hash[32:]
 86 | """
 87 | 
 88 |         # Scenario 2: Typo in method name (obvious once spotted)
 89 |         typo_bug_code = """#!/usr/bin/env python3
 90 | class Calculator:
 91 |     def __init__(self):
 92 |         self.history = []
 93 | 
 94 |     def add_numbers(self, a, b):
 95 |         result = a + b
 96 |         self.history.append(f"{a} + {b} = {result}")
 97 |         return result
 98 | 
 99 |     def calculate_total(self, numbers):
100 |         total = 0
101 |         for num in numbers:
102 |             # Typo: should be add_numbers, not add_number
103 |             total = self.add_number(total, num)  # NameError: no method 'add_number'
104 |         return total
105 | """
106 | 
107 |         # Scenario 3: Indentation error (Python syntax error)
108 |         indentation_error_code = """#!/usr/bin/env python3
109 | def process_data(data_list):
110 |     results = []
111 |     for item in data_list:
112 |         if item > 0:
113 |             processed = item * 2
114 |         results.append(processed)  # IndentationError: unindent does not match any outer indentation level
115 |     return results
116 | 
117 | def main():
118 |     data = [1, 2, 3, 4, 5]
119 |     print(process_data(data))
120 | """
121 | 
122 |         # Create test files
123 |         self.missing_import_file = self.create_additional_test_file("user_auth.py", missing_import_code)
124 |         self.typo_bug_file = self.create_additional_test_file("calculator.py", typo_bug_code)
125 |         self.indentation_file = self.create_additional_test_file("data_processor.py", indentation_error_code)
126 | 
127 |         self.logger.info("  ✅ Created obvious bug scenarios:")
128 |         self.logger.info(f"    - Missing import: {self.missing_import_file}")
129 |         self.logger.info(f"    - Method typo: {self.typo_bug_file}")
130 |         self.logger.info(f"    - Indentation error: {self.indentation_file}")
131 | 
132 |         # Create error logs for context
133 |         import_error_log = """ERROR: User authentication failing during login
134 | Traceback (most recent call last):
135 |   File "user_auth.py", line 12, in hash_password
136 |     return hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
137 | NameError: name 'hashlib' is not defined
138 | 
139 | This happens every time a user tries to log in. The error occurs in the password hashing function.
140 | """
141 | 
142 |         self.error_log_file = self.create_additional_test_file("error.log", import_error_log)
143 |         self.logger.info(f"    - Error log: {self.error_log_file}")
144 | 
145 |     def _test_obvious_import_error_certain(self) -> bool:
146 |         """Test certain confidence with obvious missing import error"""
147 |         try:
148 |             self.logger.info("  1.1: Testing obvious import error with certain confidence")
149 | 
150 |             # Step 1: Initial investigation
151 |             self.logger.info("    1.1.1: Step 1 - Initial problem description")
152 |             response1, continuation_id = self.call_mcp_tool_direct(
153 |                 "debug",
154 |                 {
155 |                     "step": "Investigating NameError in user authentication - users cannot log in due to 'name hashlib is not defined' error.",
156 |                     "step_number": 1,
157 |                     "total_steps": 2,
158 |                     "next_step_required": True,
159 |                     "findings": "NameError occurs in hash_password method when trying to use hashlib.pbkdf2_hmac. Error happens on every login attempt.",
160 |                     "files_checked": [self.error_log_file],
161 |                     "relevant_files": [self.error_log_file],
162 |                     "hypothesis": "Missing import statement for hashlib module",
163 |                     "confidence": "medium",
164 |                 },
165 |             )
166 | 
167 |             if not response1 or not continuation_id:
168 |                 self.logger.error("Failed to get initial investigation response")
169 |                 return False
170 | 
171 |             response1_data = self._parse_debug_response(response1)
172 |             if not self._validate_investigation_response(response1_data, 1, True, "pause_for_investigation"):
173 |                 return False
174 | 
175 |             self.logger.info(f"    ✅ Step 1 successful, continuation_id: {continuation_id}")
176 | 
177 |             # Step 2: Examine code and identify obvious fix - use certain confidence
178 |             self.logger.info("    1.1.2: Step 2 - Found exact issue and simple fix (certain)")
179 |             response2, _ = self.call_mcp_tool_direct(
180 |                 "debug",
181 |                 {
182 |                     "step": "Found the exact issue and the minimal fix required",
183 |                     "step_number": 2,
184 |                     "total_steps": 2,
185 |                     "next_step_required": False,  # Final step
186 |                     "findings": "Missing 'import hashlib' statement at the top of user_auth.py file. The error occurs because hashlib is used in hash_password() method on line 12 but never imported. Simple one-line fix: add 'import hashlib' after line 2.",
187 |                     "files_checked": [self.error_log_file, self.missing_import_file],
188 |                     "relevant_files": [self.missing_import_file],
189 |                     "relevant_context": ["UserAuth.hash_password", "UserAuth.verify_password"],
190 |                     "hypothesis": "Missing 'import hashlib' statement causes NameError when hash_password method executes",
191 |                     "confidence": "certain",  # Use certain - should skip expert analysis
192 |                     "continuation_id": continuation_id,
193 |                     "model": "flash",  # Specify model for consistency
194 |                 },
195 |             )
196 | 
197 |             if not response2:
198 |                 self.logger.error("Failed to complete investigation with certain confidence")
199 |                 return False
200 | 
201 |             response2_data = self._parse_debug_response(response2)
202 |             if not response2_data:
203 |                 return False
204 | 
205 |             # Validate certain response structure
206 |             expected_status = "certain_confidence_proceed_with_fix"
207 |             if response2_data.get("status") != expected_status:
208 |                 self.logger.error(f"Expected status '{expected_status}', got '{response2_data.get('status')}'")
209 |                 return False
210 | 
211 |             if not response2_data.get("investigation_complete"):
212 |                 self.logger.error("Expected investigation_complete=true for certain confidence")
213 |                 return False
214 | 
215 |             if not response2_data.get("skip_expert_analysis"):
216 |                 self.logger.error("Expected skip_expert_analysis=true for certain confidence")
217 |                 return False
218 | 
219 |             # Verify expert analysis is marked as skipped
220 |             expert_analysis = response2_data.get("expert_analysis", {})
221 |             if expert_analysis.get("status") != "skipped_due_to_certain_confidence":
222 |                 self.logger.error("Expert analysis should be marked as skipped for certain confidence")
223 |                 return False
224 | 
225 |             # Check for proper investigation summary
226 |             complete_investigation = response2_data.get("complete_investigation", {})
227 |             if complete_investigation.get("confidence_level") != "certain":
228 |                 self.logger.error("Expected confidence_level='certain' in complete investigation")
229 |                 return False
230 | 
231 |             if complete_investigation.get("steps_taken") != 2:
232 |                 self.logger.error("Expected steps_taken=2 in complete investigation")
233 |                 return False
234 | 
235 |             # Verify next steps guidance
236 |             next_steps = response2_data.get("next_steps", "")
237 |             if "CERTAIN confidence" not in next_steps:
238 |                 self.logger.error("Expected 'CERTAIN confidence' in next_steps guidance")
239 |                 return False
240 | 
241 |             if "minimal fix" not in next_steps:
242 |                 self.logger.error("Expected 'minimal fix' guidance in next_steps")
243 |                 return False
244 | 
245 |             self.logger.info("    ✅ Certain confidence skipped expert analysis correctly")
246 |             return True
247 | 
248 |         except Exception as e:
249 |             self.logger.error(f"Obvious import error certain test failed: {e}")
250 |             return False
251 | 
252 |     def _test_certain_always_trusted(self) -> bool:
253 |         """Test that certain confidence is always trusted regardless of complexity"""
254 |         try:
255 |             self.logger.info("  1.2: Testing that certain confidence is always trusted")
256 | 
257 |             # Single step investigation with certain - should always be trusted
258 |             self.logger.info("    1.2.1: Direct certain confidence (always trusted)")
259 |             response, _ = self.call_mcp_tool_direct(
260 |                 "debug",
261 |                 {
262 |                     "step": "Found the exact root cause and minimal fix for this complex issue",
263 |                     "step_number": 1,
264 |                     "total_steps": 1,
265 |                     "next_step_required": False,  # Final step
266 |                     "findings": "After thorough investigation, identified that the issue is caused by method name typo in Calculator.calculate_total() - calls self.add_number() instead of self.add_numbers(). Simple fix: change line 14 from 'add_number' to 'add_numbers'.",
267 |                     "files_checked": [self.typo_bug_file],
268 |                     "relevant_files": [self.typo_bug_file],
269 |                     "relevant_context": ["Calculator.calculate_total", "Calculator.add_numbers"],
270 |                     "hypothesis": "Method name typo in calculate_total() calls non-existent add_number() instead of add_numbers()",
271 |                     "confidence": "certain",  # Should always be trusted
272 |                     "model": "flash",
273 |                 },
274 |             )
275 | 
276 |             if not response:
277 |                 self.logger.error("Failed to get certain confidence response")
278 |                 return False
279 | 
280 |             response_data = self._parse_debug_response(response)
281 |             if not response_data:
282 |                 return False
283 | 
284 |             # Verify certain is trusted regardless of complexity
285 |             if response_data.get("status") != "certain_confidence_proceed_with_fix":
286 |                 self.logger.error("Certain confidence should always be trusted")
287 |                 return False
288 | 
289 |             if not response_data.get("skip_expert_analysis"):
290 |                 self.logger.error("Expert analysis should be skipped for certain confidence")
291 |                 return False
292 | 
293 |             # Ensure expert analysis is marked as skipped
294 |             expert_analysis = response_data.get("expert_analysis", {})
295 |             if expert_analysis.get("status") != "skipped_due_to_certain_confidence":
296 |                 self.logger.error("Expert analysis status should indicate certain skip")
297 |                 return False
298 | 
299 |             self.logger.info("    ✅ Certain confidence always trusted correctly")
300 |             return True
301 | 
302 |         except Exception as e:
303 |             self.logger.error(f"Certain always trusted test failed: {e}")
304 |             return False
305 | 
306 |     def _test_regular_high_confidence_expert_analysis(self) -> bool:
307 |         """Test that regular 'high' confidence still triggers expert analysis"""
308 |         try:
309 |             self.logger.info("  1.3: Testing that regular 'high' confidence triggers expert analysis")
310 | 
311 |             # Investigation with regular high confidence (not certain)
312 |             self.logger.info("    1.3.1: High confidence (not certain) - should trigger expert analysis")
313 |             response, _ = self.call_mcp_tool_direct(
314 |                 "debug",
315 |                 {
316 |                     "step": "Identified likely root cause with strong evidence",
317 |                     "step_number": 1,
318 |                     "total_steps": 1,
319 |                     "next_step_required": False,  # Final step
320 |                     "findings": "IndentationError in data_processor.py line 8 - results.append(processed) is incorrectly indented. Should align with the 'if' statement above it.",
321 |                     "files_checked": [self.indentation_file],
322 |                     "relevant_files": [self.indentation_file],
323 |                     "relevant_context": ["process_data"],
324 |                     "hypothesis": "Incorrect indentation causes IndentationError in process_data function",
325 |                     "confidence": "high",  # Regular high confidence, NOT certain
326 |                     "model": "flash",
327 |                 },
328 |             )
329 | 
330 |             if not response:
331 |                 self.logger.error("Failed to get high confidence response")
332 |                 return False
333 | 
334 |             response_data = self._parse_debug_response(response)
335 |             if not response_data:
336 |                 return False
337 | 
338 |             # Verify that regular high confidence triggers expert analysis
339 |             if response_data.get("status") != "calling_expert_analysis":
340 |                 self.logger.error(
341 |                     f"Expected 'calling_expert_analysis' for high confidence, got '{response_data.get('status')}'"
342 |                 )
343 |                 return False
344 | 
345 |             if response_data.get("skip_expert_analysis"):
346 |                 self.logger.error("Expert analysis should NOT be skipped for regular high confidence")
347 |                 return False
348 | 
349 |             # Verify expert analysis was called
350 |             expert_analysis = response_data.get("expert_analysis", {})
351 |             if not expert_analysis:
352 |                 self.logger.error("Expected expert analysis for regular high confidence")
353 |                 return False
354 | 
355 |             # Check that expert analysis has content
356 |             if "status" not in expert_analysis:
357 |                 self.logger.error("Expert analysis should have status field")
358 |                 return False
359 | 
360 |             self.logger.info("    ✅ Regular high confidence triggers expert analysis correctly")
361 |             return True
362 | 
363 |         except Exception as e:
364 |             self.logger.error(f"Regular high confidence test failed: {e}")
365 |             return False
366 | 
367 |     def _test_multi_step_investigation_certain(self) -> bool:
368 |         """Test multi-step investigation that ends with certain confidence"""
369 |         try:
370 |             self.logger.info("  1.4: Testing multi-step investigation ending with certain")
371 | 
372 |             # Step 1: Start investigation
373 |             self.logger.info("    1.4.1: Step 1 - Initial investigation")
374 |             response1, continuation_id = self.call_mcp_tool_direct(
375 |                 "debug",
376 |                 {
377 |                     "step": "Investigating Python syntax error in data processing module",
378 |                     "step_number": 1,
379 |                     "total_steps": 3,
380 |                     "next_step_required": True,
381 |                     "findings": "IndentationError reported when running data_processor.py - 'unindent does not match any outer indentation level'",
382 |                     "files_checked": [self.indentation_file],
383 |                     "relevant_files": [],
384 |                     "hypothesis": "Indentation inconsistency in Python code",
385 |                     "confidence": "low",
386 |                 },
387 |             )
388 | 
389 |             if not response1 or not continuation_id:
390 |                 self.logger.error("Failed to start multi-step investigation")
391 |                 return False
392 | 
393 |             # Step 2: Examine code structure
394 |             self.logger.info("    1.4.2: Step 2 - Code examination")
395 |             response2, _ = self.call_mcp_tool_direct(
396 |                 "debug",
397 |                 {
398 |                     "step": "Examining the indentation structure in process_data function",
399 |                     "step_number": 2,
400 |                     "total_steps": 3,
401 |                     "next_step_required": True,
402 |                     "findings": "Found the issue: line 8 'results.append(processed)' is indented incorrectly. It should align with the 'if' statement, not be at the same level as the 'for' loop.",
403 |                     "files_checked": [self.indentation_file],
404 |                     "relevant_files": [self.indentation_file],
405 |                     "relevant_context": ["process_data"],
406 |                     "hypothesis": "Line 8 has incorrect indentation level causing IndentationError",
407 |                     "confidence": "medium",
408 |                     "continuation_id": continuation_id,
409 |                 },
410 |             )
411 | 
412 |             if not response2:
413 |                 self.logger.error("Failed to continue to step 2")
414 |                 return False
415 | 
416 |             # Step 3: Confirm fix with certain confidence
417 |             self.logger.info("    1.4.3: Step 3 - Confirmed fix (certain)")
418 |             response3, _ = self.call_mcp_tool_direct(
419 |                 "debug",
420 |                 {
421 |                     "step": "Confirmed the exact issue and simple fix",
422 |                     "step_number": 3,
423 |                     "total_steps": 3,
424 |                     "next_step_required": False,  # Final step
425 |                     "findings": "Confirmed: line 8 'results.append(processed)' needs to be indented 4 more spaces to align with line 6 'if item > 0:'. This is a simple indentation fix.",
426 |                     "files_checked": [self.indentation_file],
427 |                     "relevant_files": [self.indentation_file],
428 |                     "relevant_context": ["process_data"],
429 |                     "hypothesis": "IndentationError on line 8 due to incorrect indentation level - needs 4 more spaces",
430 |                     "confidence": "certain",  # Final step with certain
431 |                     "continuation_id": continuation_id,
432 |                     "model": "flash",
433 |                 },
434 |             )
435 | 
436 |             if not response3:
437 |                 self.logger.error("Failed to complete multi-step investigation")
438 |                 return False
439 | 
440 |             response3_data = self._parse_debug_response(response3)
441 |             if not response3_data:
442 |                 return False
443 | 
444 |             # Validate multi-step certain response
445 |             if response3_data.get("status") != "certain_confidence_proceed_with_fix":
446 |                 self.logger.error("Expected certain status for final step")
447 |                 return False
448 | 
449 |             if not response3_data.get("skip_expert_analysis"):
450 |                 self.logger.error("Expected expert analysis to be skipped for certain")
451 |                 return False
452 | 
453 |             # Verify investigation preserves steps (at least the current step)
454 |             complete_investigation = response3_data.get("complete_investigation", {})
455 |             steps_taken = complete_investigation.get("steps_taken", 0)
456 |             if steps_taken < 1:
457 |                 self.logger.error("Expected at least 1 step in complete investigation")
458 |                 return False
459 | 
460 |             # Check that work summary includes progression
461 |             work_summary = complete_investigation.get("work_summary", "")
462 |             if "Total steps:" not in work_summary and "Steps taken:" not in work_summary:
463 |                 self.logger.error("Work summary should show steps information")
464 |                 return False
465 | 
466 |             self.logger.info("    ✅ Multi-step investigation with certain ending successful")
467 |             return True
468 | 
469 |         except Exception as e:
470 |             self.logger.error(f"Multi-step investigation certain test failed: {e}")
471 |             return False
472 | 
473 |     def call_mcp_tool_direct(self, tool_name: str, params: dict) -> tuple[Optional[str], Optional[str]]:
474 |         """Call an MCP tool directly in-process to maintain conversation memory"""
475 |         try:
476 |             # Get the tool instance
477 |             if tool_name not in self._tools:
478 |                 self.logger.error(f"Tool '{tool_name}' not found in available tools")
479 |                 return None, None
480 | 
481 |             tool = self._tools[tool_name]
482 | 
483 |             # Execute the tool with proper async handling
484 |             loop = self._get_event_loop()
485 | 
486 |             # Call the tool's execute method
487 |             try:
488 |                 result = loop.run_until_complete(tool.execute(params))
489 |             except ToolExecutionError as exc:
490 |                 response_text = exc.payload
491 |                 continuation_id = self._extract_debug_continuation_id(response_text)
492 |                 return response_text, continuation_id
493 | 
494 |             if not result or len(result) == 0:
495 |                 self.logger.error(f"Tool '{tool_name}' returned empty result")
496 |                 return None, None
497 | 
498 |             # Extract the text content from the result
499 |             response_text = result[0].text if hasattr(result[0], "text") else str(result[0])
500 | 
501 |             # Extract continuation_id from debug response if present
502 |             continuation_id = self._extract_debug_continuation_id(response_text)
503 | 
504 |             return response_text, continuation_id
505 | 
506 |         except Exception as e:
507 |             self.logger.error(f"Failed to call tool '{tool_name}' directly: {e}")
508 |             return None, None
509 | 
510 |     def _extract_debug_continuation_id(self, response_text: str) -> Optional[str]:
511 |         """Extract continuation_id from debug response"""
512 |         try:
513 |             response_data = json.loads(response_text)
514 |             return response_data.get("continuation_id")
515 |         except json.JSONDecodeError as e:
516 |             self.logger.debug(f"Failed to parse response for debug continuation_id: {e}")
517 |             return None
518 | 
519 |     def _parse_debug_response(self, response_text: str) -> dict:
520 |         """Parse debug tool JSON response"""
521 |         try:
522 |             return json.loads(response_text)
523 |         except json.JSONDecodeError as e:
524 |             self.logger.error(f"Failed to parse debug response as JSON: {e}")
525 |             self.logger.error(f"Response text: {response_text[:500]}...")
526 |             return {}
527 | 
528 |     def _validate_investigation_response(
529 |         self,
530 |         response_data: dict,
531 |         expected_step: int,
532 |         expected_next_required: bool,
533 |         expected_status: str,
534 |     ) -> bool:
535 |         """Validate debug investigation response structure"""
536 |         try:
537 |             # Check status
538 |             if response_data.get("status") != expected_status:
539 |                 self.logger.error(f"Expected status '{expected_status}', got '{response_data.get('status')}'")
540 |                 return False
541 | 
542 |             # Check step number
543 |             if response_data.get("step_number") != expected_step:
544 |                 self.logger.error(f"Expected step_number {expected_step}, got {response_data.get('step_number')}")
545 |                 return False
546 | 
547 |             # Check next_step_required
548 |             if response_data.get("next_step_required") != expected_next_required:
549 |                 self.logger.error(
550 |                     f"Expected next_step_required {expected_next_required}, got {response_data.get('next_step_required')}"
551 |                 )
552 |                 return False
553 | 
554 |             # Basic structure checks
555 |             if "investigation_status" not in response_data:
556 |                 self.logger.error("Missing investigation_status in response")
557 |                 return False
558 | 
559 |             if not response_data.get("next_steps"):
560 |                 self.logger.error("Missing next_steps guidance in response")
561 |                 return False
562 | 
563 |             return True
564 | 
565 |         except Exception as e:
566 |             self.logger.error(f"Error validating investigation response: {e}")
567 |             return False
568 | 
```
Page 15/25FirstPrevNextLast