From 4912f464c30dea22b346d0b304563f8bab7b8f88 Mon Sep 17 00:00:00 2001 From: Vlad Ciobanu <95963142+vl3c@users.noreply.github.com> Date: Sat, 11 Jul 2026 23:52:16 +0300 Subject: [PATCH] Update AI model registry to July 2026 lineups Refresh static/ai_model.py with the current OpenAI, Anthropic, and OpenRouter model lineups, verified against official sources and the live OpenRouter models API. OpenAI: add the GPT-5.6 family (Sol/Terra/Luna) and GPT-5.5; keep GPT-5.2, the GPT-4.1 family, and GPT-4o mini; remove deprecated or retired o3, o4-mini, gpt-5-chat-latest, gpt-5.2-chat-latest, gpt-5-nano, gpt-4o, and gpt-3.5-turbo. The default model is now gpt-5.5. Anthropic: replace the 4.5 generation with Claude Fable 5, Opus 4.8, and Sonnet 5, plus Haiku 4.5. These adaptive-thinking models reject the temperature parameter with a 400, so flag them is_reasoning_model and add AnthropicAPI._apply_temperature() to omit temperature for them while still sending it for Haiku 4.5. OpenRouter: refresh the paid tier (Gemini 3.1 Pro / 3.5 Flash, DeepSeek V4 Pro, Qwen3.7 Max, GLM 5.2, Grok 4.5, MiniMax M3, Claude Sonnet 5) and the free tier (Gemma 4, gpt-oss 20b/120b, Qwen3 Next, Nemotron 3 Nano, Tencent Hy3, Llama 3.3); drop models no longer offered. Point the tool-search fallback at gpt-4.1-nano, update the provider default models, and refresh model references across tests and docs. --- README.md | 8 +- cli/README.md | 2 +- cli/chat.py | 4 +- documentation/Reference Manual.txt | 10 +- server_tests/test_ai_model.py | 102 ++++++------ server_tests/test_openai_api_base.py | 12 +- server_tests/test_openai_responses_api.py | 38 ++--- server_tests/test_prompt_pipeline.py | 4 +- server_tests/test_provider_connections.py | 4 +- server_tests/test_routes.py | 20 +-- server_tests/test_search_tool_wiring_smoke.py | 2 +- server_tests/test_tool_search_service.py | 12 +- static/ai_model.py | 154 ++++++++---------- static/app_manager.py | 2 +- static/openai_responses_api.py | 4 +- static/providers/anthropic_api.py | 23 ++- static/providers/openrouter_api.py | 6 +- static/routes.py | 4 +- static/tool_search_service.py | 5 +- templates/index.html | 2 +- 20 files changed, 213 insertions(+), 205 deletions(-) diff --git a/README.md b/README.md index 90e495a7..9b7e1d7f 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ MatHud pairs an interactive drawing canvas with an AI assistant to help visualiz ``` 4. Provide at least one AI provider API key by setting environment variables or creating `.env` in the project root: ```env - OPENAI_API_KEY=sk-... # OpenAI models (GPT-4o, GPT-5, o3, etc.) + OPENAI_API_KEY=sk-... # OpenAI models (GPT-5.6, GPT-5.5, GPT-4.1, etc.) ANTHROPIC_API_KEY=sk-ant-... # Anthropic models (Claude Opus/Sonnet/Haiku 4.5) OPENROUTER_API_KEY=sk-or-... # OpenRouter models (Gemini, DeepSeek, Llama, etc.) ``` @@ -190,9 +190,9 @@ MatHud supports three AI providers. The model dropdown dynamically shows only mo | Provider | Environment Variable | Models | |----------|---------------------|--------| -| **OpenAI** | `OPENAI_API_KEY` | GPT-5.2, GPT-5, GPT-4.1, GPT-4o, o3, o4-mini, GPT-3.5 Turbo, etc. | -| **Anthropic** | `ANTHROPIC_API_KEY` | Claude Opus 4.5, Claude Sonnet 4.5, Claude Haiku 4.5 | -| **OpenRouter** | `OPENROUTER_API_KEY` | Gemini 3 Pro/Flash, Gemini 2.5 Pro, DeepSeek V3.2, Grok, Llama, Gemma, and more (paid and free tiers) | +| **OpenAI** | `OPENAI_API_KEY` | GPT-5.6 (Sol/Terra/Luna), GPT-5.5, GPT-5.2, GPT-4.1 family, GPT-4o mini | +| **Anthropic** | `ANTHROPIC_API_KEY` | Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5, Claude Haiku 4.5 | +| **OpenRouter** | `OPENROUTER_API_KEY` | Gemini 3.1 Pro/3.5 Flash, DeepSeek V4 Pro, Qwen, GLM, Grok, MiniMax, Llama, Gemma, and more (paid and free tiers) | Models without vision support are labeled "(text only)" in the dropdown. When no API keys are configured, the dropdown shows "No API keys configured". diff --git a/cli/README.md b/cli/README.md index bb5367d3..965213ee 100644 --- a/cli/README.md +++ b/cli/README.md @@ -142,7 +142,7 @@ python -m cli.main chat new [--port PORT] ``` **Options:** -- `--model, -m`: AI model (e.g., gpt-4o, claude-3-5-sonnet) +- `--model, -m`: AI model (e.g., gpt-5.5, claude-sonnet-5) - `--vision, -v`: Include canvas snapshot - `--no-stream`: Wait for complete response - `--json`: Output as JSON diff --git a/cli/chat.py b/cli/chat.py index f7c2576f..84390d0b 100644 --- a/cli/chat.py +++ b/cli/chat.py @@ -187,7 +187,7 @@ def chat() -> None: @click.option( "--model", "-m", - help="AI model to use (e.g., gpt-4o, claude-3-5-sonnet)", + help="AI model to use (e.g., gpt-5.5, claude-sonnet-5)", ) @click.option( "--vision", @@ -224,7 +224,7 @@ def send( mathud chat send "Draw a circle with center A and radius 50" --vision - mathud chat send "What is the derivative of x^2?" --model gpt-4o + mathud chat send "What is the derivative of x^2?" --model gpt-5.5 """ if not check_server(DEFAULT_HOST, port): click.echo(click.style(f"Server is not running on port {port}", fg="red"), err=True) diff --git a/documentation/Reference Manual.txt b/documentation/Reference Manual.txt index 3626edec..7d283f90 100644 --- a/documentation/Reference Manual.txt +++ b/documentation/Reference Manual.txt @@ -3561,7 +3561,7 @@ Dependencies: **Key Features:** - **MathJax Configuration**: LaTeX rendering with inline `\(...\)` and display `$$...$$` math -- **AI Model Selection**: Dropdown with GPT-4 family and GPT-3.5-turbo options +- **AI Model Selection**: Provider-grouped dropdown populated from `/api/available_models` - **Vision Toggle**: Conditional enabling based on model capabilities - **Responsive Design**: Flexible dual-pane layout for different screen sizes @@ -3766,7 +3766,7 @@ Dependencies: Provides tool discovery via fast local keyword/category matching (default) or AI-powered semantic matching. The search mode is selected by the `TOOL_SEARCH_MODE` environment variable: - `local` — keyword + category index, no API call (~0.01ms per query) -- `api` — AI-powered semantic search via gpt-5-nano +- `api` — AI-powered semantic search via gpt-4.1-nano - `hybrid` (default) — local first, falls back to API when confidence is low **Architecture:** @@ -3958,9 +3958,9 @@ Manages model-specific capabilities (vision support, reasoning mode, provider as and provides factory methods for creating model instances from identifiers. Supported Providers: - - PROVIDER_OPENAI ("openai"): GPT-5.2, GPT-5, GPT-4.1, GPT-4o, o3, o4-mini, GPT-3.5 Turbo, etc. - - PROVIDER_ANTHROPIC ("anthropic"): Claude Opus 4.5, Claude Sonnet 4.5, Claude Haiku 4.5 - - PROVIDER_OPENROUTER ("openrouter"): Gemini, DeepSeek, Grok, Llama, Gemma, and more (paid and free tiers) + - PROVIDER_OPENAI ("openai"): GPT-5.6 (Sol/Terra/Luna), GPT-5.5, GPT-5.2, GPT-4.1 family, GPT-4o mini + - PROVIDER_ANTHROPIC ("anthropic"): Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5, Claude Haiku 4.5 + - PROVIDER_OPENROUTER ("openrouter"): Gemini, DeepSeek, Qwen, GLM, Grok, MiniMax, Llama, Gemma, and more (paid and free tiers) ``` **Key Methods:** diff --git a/server_tests/test_ai_model.py b/server_tests/test_ai_model.py index 6d958ff6..b72ebf1e 100644 --- a/server_tests/test_ai_model.py +++ b/server_tests/test_ai_model.py @@ -2,14 +2,20 @@ Tests for the AIModel configuration module. Tests model configurations, factory methods, and capability flags including -the new is_reasoning_model flag for Responses API routing. +the is_reasoning_model flag used for Responses API routing (OpenAI) and for +omitting the temperature parameter on adaptive-thinking Claude models. """ from __future__ import annotations import unittest -from static.ai_model import AIModel +from static.ai_model import ( + PROVIDER_ANTHROPIC, + PROVIDER_OPENAI, + PROVIDER_OPENROUTER, + AIModel, +) class TestAIModel(unittest.TestCase): @@ -27,46 +33,40 @@ def test_model_initialization_defaults(self) -> None: model = AIModel(identifier="test-model", has_vision=False) self.assertFalse(model.is_reasoning_model) - def test_from_identifier_gpt5_chat_latest(self) -> None: - """Test GPT-5-chat-latest is a reasoning model with vision.""" - model = AIModel.from_identifier("gpt-5-chat-latest") - self.assertEqual(model.id, "gpt-5-chat-latest") + def test_from_identifier_gpt56_sol(self) -> None: + """Test GPT-5.6 Sol is a reasoning model with vision.""" + model = AIModel.from_identifier("gpt-5.6-sol") + self.assertEqual(model.id, "gpt-5.6-sol") self.assertTrue(model.has_vision) self.assertTrue(model.is_reasoning_model) - def test_from_identifier_gpt52_chat_latest(self) -> None: - """Test GPT-5.2-chat-latest is a reasoning model with vision.""" - model = AIModel.from_identifier("gpt-5.2-chat-latest") - self.assertEqual(model.id, "gpt-5.2-chat-latest") + def test_from_identifier_gpt55_medium_reasoning(self) -> None: + """Test GPT-5.5 (the default) has medium reasoning effort configured.""" + model = AIModel.from_identifier("gpt-5.5") + self.assertEqual(model.id, "gpt-5.5") self.assertTrue(model.has_vision) self.assertTrue(model.is_reasoning_model) + self.assertEqual(model.reasoning_effort, "medium") def test_from_identifier_gpt52_medium_reasoning(self) -> None: - """Test GPT-5.2 has medium reasoning effort configured.""" + """Test GPT-5.2 remains a reasoning model with medium reasoning effort.""" model = AIModel.from_identifier("gpt-5.2") self.assertEqual(model.id, "gpt-5.2") self.assertTrue(model.has_vision) self.assertTrue(model.is_reasoning_model) self.assertEqual(model.reasoning_effort, "medium") - def test_from_identifier_o3(self) -> None: - """Test o3 is a reasoning model with vision.""" - model = AIModel.from_identifier("o3") - self.assertEqual(model.id, "o3") + def test_from_identifier_gpt56_luna(self) -> None: + """Test GPT-5.6 Luna is a reasoning model with vision.""" + model = AIModel.from_identifier("gpt-5.6-luna") + self.assertEqual(model.id, "gpt-5.6-luna") self.assertTrue(model.has_vision) self.assertTrue(model.is_reasoning_model) - def test_from_identifier_o4_mini(self) -> None: - """Test o4-mini is a reasoning model with vision.""" - model = AIModel.from_identifier("o4-mini") - self.assertEqual(model.id, "o4-mini") - self.assertTrue(model.has_vision) - self.assertTrue(model.is_reasoning_model) - - def test_from_identifier_gpt4o(self) -> None: - """Test GPT-4o is a standard model (not reasoning) with vision.""" - model = AIModel.from_identifier("gpt-4o") - self.assertEqual(model.id, "gpt-4o") + def test_from_identifier_gpt41(self) -> None: + """Test GPT-4.1 is a standard model with vision.""" + model = AIModel.from_identifier("gpt-4.1") + self.assertEqual(model.id, "gpt-4.1") self.assertTrue(model.has_vision) self.assertFalse(model.is_reasoning_model) @@ -77,19 +77,13 @@ def test_from_identifier_gpt4o_mini(self) -> None: self.assertTrue(model.has_vision) self.assertFalse(model.is_reasoning_model) - def test_from_identifier_gpt41(self) -> None: - """Test GPT-4.1 is a standard model with vision.""" - model = AIModel.from_identifier("gpt-4.1") - self.assertEqual(model.id, "gpt-4.1") - self.assertTrue(model.has_vision) - self.assertFalse(model.is_reasoning_model) - - def test_from_identifier_gpt35_turbo(self) -> None: - """Test GPT-3.5-turbo is a standard model without vision.""" - model = AIModel.from_identifier("gpt-3.5-turbo") - self.assertEqual(model.id, "gpt-3.5-turbo") + def test_from_identifier_deepseek_no_vision(self) -> None: + """Test a text-only OpenRouter model reports no vision support.""" + model = AIModel.from_identifier("deepseek/deepseek-v4-pro") + self.assertEqual(model.id, "deepseek/deepseek-v4-pro") self.assertFalse(model.has_vision) self.assertFalse(model.is_reasoning_model) + self.assertEqual(model.provider, PROVIDER_OPENROUTER) def test_from_identifier_unknown_model(self) -> None: """Test unknown model gets default config (no vision, not reasoning).""" @@ -99,39 +93,53 @@ def test_from_identifier_unknown_model(self) -> None: self.assertFalse(model.is_reasoning_model) def test_get_default_model(self) -> None: - """Test default model is GPT-5.2 with medium reasoning.""" + """Test default model is GPT-5.5 with medium reasoning.""" model = AIModel.get_default_model() self.assertEqual(model.id, AIModel.DEFAULT_MODEL) - self.assertEqual(model.id, "gpt-5.2") + self.assertEqual(model.id, "gpt-5.5") self.assertTrue(model.is_reasoning_model) self.assertEqual(model.reasoning_effort, "medium") def test_str_representation(self) -> None: """Test string representation returns model identifier.""" - model = AIModel.from_identifier("gpt-4o") - self.assertEqual(str(model), "gpt-4o") + model = AIModel.from_identifier("gpt-4.1") + self.assertEqual(str(model), "gpt-4.1") def test_all_reasoning_models_identified(self) -> None: - """Test that all reasoning models are correctly identified.""" - reasoning_models = ["gpt-5-chat-latest", "gpt-5.2-chat-latest", "gpt-5.2", "o3", "o4-mini"] + """Test that all OpenAI Responses-API models are flagged as reasoning models.""" + reasoning_models = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.2"] for model_id in reasoning_models: model = AIModel.from_identifier(model_id) self.assertTrue(model.is_reasoning_model, f"{model_id} should be a reasoning model") + self.assertEqual(model.provider, PROVIDER_OPENAI) def test_all_standard_models_identified(self) -> None: - """Test that all standard models are correctly identified as non-reasoning.""" + """Test that all OpenAI Chat Completions models are non-reasoning.""" standard_models = [ "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", - "gpt-4o", "gpt-4o-mini", - "gpt-5-nano", - "gpt-3.5-turbo", ] for model_id in standard_models: model = AIModel.from_identifier(model_id) self.assertFalse(model.is_reasoning_model, f"{model_id} should NOT be a reasoning model") + self.assertEqual(model.provider, PROVIDER_OPENAI) + + def test_anthropic_adaptive_thinking_models_flagged_reasoning(self) -> None: + """Adaptive-thinking Claude models are flagged is_reasoning_model so the provider + omits the temperature parameter (which they reject with a 400).""" + for model_id in ["claude-fable-5", "claude-opus-4-8", "claude-sonnet-5"]: + model = AIModel.from_identifier(model_id) + self.assertEqual(model.provider, PROVIDER_ANTHROPIC) + self.assertTrue(model.is_reasoning_model, f"{model_id} should be flagged is_reasoning_model") + self.assertTrue(model.has_vision) + + def test_anthropic_haiku_accepts_temperature(self) -> None: + """Claude Haiku 4.5 still accepts temperature, so it stays non-reasoning.""" + model = AIModel.from_identifier("claude-haiku-4-5") + self.assertEqual(model.provider, PROVIDER_ANTHROPIC) + self.assertFalse(model.is_reasoning_model) def test_model_configs_completeness(self) -> None: """Test that MODEL_CONFIGS has both required keys for all models.""" diff --git a/server_tests/test_openai_api_base.py b/server_tests/test_openai_api_base.py index 8bb9d750..4bbf39e3 100644 --- a/server_tests/test_openai_api_base.py +++ b/server_tests/test_openai_api_base.py @@ -60,9 +60,9 @@ def test_initialization_default_model(self, mock_openai: Mock) -> None: @patch("static.openai_api_base.OpenAI") def test_initialization_custom_model(self, mock_openai: Mock) -> None: """Test API initializes with custom model.""" - custom_model = AIModel.from_identifier("gpt-4o") + custom_model = AIModel.from_identifier("gpt-4.1") api = OpenAIAPIBase(model=custom_model) - self.assertEqual(api.model.id, "gpt-4o") + self.assertEqual(api.model.id, "gpt-4.1") @patch("static.openai_api_base.OpenAI") def test_initialization_messages(self, mock_openai: Mock) -> None: @@ -97,16 +97,16 @@ def test_reset_conversation(self, mock_openai: Mock) -> None: def test_set_model(self, mock_openai: Mock) -> None: """Test set_model changes the model.""" api = OpenAIAPIBase() - api.set_model("gpt-4o") - self.assertEqual(api.model.id, "gpt-4o") + api.set_model("gpt-4.1") + self.assertEqual(api.model.id, "gpt-4.1") self.assertFalse(api.model.is_reasoning_model) @patch("static.openai_api_base.OpenAI") def test_set_model_to_reasoning(self, mock_openai: Mock) -> None: """Test set_model to a reasoning model.""" api = OpenAIAPIBase() - api.set_model("o3") - self.assertEqual(api.model.id, "o3") + api.set_model("gpt-5.5") + self.assertEqual(api.model.id, "gpt-5.5") self.assertTrue(api.model.is_reasoning_model) @patch("static.openai_api_base.OpenAI") diff --git a/server_tests/test_openai_responses_api.py b/server_tests/test_openai_responses_api.py index 2ea67243..103baa10 100644 --- a/server_tests/test_openai_responses_api.py +++ b/server_tests/test_openai_responses_api.py @@ -654,7 +654,7 @@ class TestOpenAIResponsesAPIIntegration(unittest.TestCase): """Integration tests that actually call the OpenAI Responses API. These tests require a valid OPENAI_API_KEY environment variable - and use reasoning models (GPT-5, o3, o4-mini). + and use reasoning models (GPT-5.6, GPT-5.5). They are skipped if the key is not available. """ @@ -675,8 +675,8 @@ def setUp(self) -> None: def test_integration_response_stream_format(self) -> None: """Test actual Responses API call returns correct format (minimal tokens).""" api = OpenAIResponsesAPI() - # Use o4-mini as it's a reasoning model, limit tokens - api.set_model("o4-mini") + # Use gpt-5.5 as it's a reasoning model, limit tokens + api.set_model("gpt-5.5") api.max_tokens = 20 prompt = json.dumps({"user_message": "Say: OK", "use_vision": False}) @@ -699,7 +699,7 @@ def test_integration_response_stream_format(self) -> None: def test_integration_reasoning_tokens(self) -> None: """Test that reasoning models stream reasoning tokens (minimal tokens).""" api = OpenAIResponsesAPI() - api.set_model("o4-mini") + api.set_model("gpt-5.5") api.max_tokens = 30 prompt = json.dumps({"user_message": "2+2=?", "use_vision": False}) @@ -725,7 +725,7 @@ def test_integration_reasoning_tokens(self) -> None: def test_integration_event_types_are_valid(self) -> None: """Test that all streamed events have valid types (minimal tokens).""" api = OpenAIResponsesAPI() - api.set_model("o4-mini") + api.set_model("gpt-5.5") api.max_tokens = 10 prompt = json.dumps({"user_message": "1", "use_vision": False}) @@ -757,38 +757,38 @@ def tearDown(self) -> None: def test_set_model_to_reasoning(self, mock_openai: Mock) -> None: """Test setting model to a reasoning model.""" api = OpenAIResponsesAPI() - api.set_model("o3") + api.set_model("gpt-5.6-sol") - self.assertEqual(api.model.id, "o3") + self.assertEqual(api.model.id, "gpt-5.6-sol") self.assertTrue(api.model.is_reasoning_model) @patch("static.openai_api_base.OpenAI") - def test_set_model_to_o4_mini(self, mock_openai: Mock) -> None: - """Test setting model to o4-mini.""" + def test_set_model_to_gpt56_luna(self, mock_openai: Mock) -> None: + """Test setting model to gpt-5.6-luna.""" api = OpenAIResponsesAPI() - api.set_model("o4-mini") + api.set_model("gpt-5.6-luna") - self.assertEqual(api.model.id, "o4-mini") + self.assertEqual(api.model.id, "gpt-5.6-luna") self.assertTrue(api.model.is_reasoning_model) self.assertTrue(api.model.has_vision) @patch("static.openai_api_base.OpenAI") - def test_set_model_to_gpt5(self, mock_openai: Mock) -> None: - """Test setting model to GPT-5-chat-latest.""" + def test_set_model_to_gpt56_terra(self, mock_openai: Mock) -> None: + """Test setting model to gpt-5.6-terra.""" api = OpenAIResponsesAPI() - api.set_model("gpt-5-chat-latest") + api.set_model("gpt-5.6-terra") - self.assertEqual(api.model.id, "gpt-5-chat-latest") + self.assertEqual(api.model.id, "gpt-5.6-terra") self.assertTrue(api.model.is_reasoning_model) self.assertTrue(api.model.has_vision) @patch("static.openai_api_base.OpenAI") - def test_set_model_to_gpt52_chat_latest(self, mock_openai: Mock) -> None: - """Test setting model to GPT-5.2-chat-latest.""" + def test_set_model_to_gpt55(self, mock_openai: Mock) -> None: + """Test setting model to GPT-5.5.""" api = OpenAIResponsesAPI() - api.set_model("gpt-5.2-chat-latest") + api.set_model("gpt-5.5") - self.assertEqual(api.model.id, "gpt-5.2-chat-latest") + self.assertEqual(api.model.id, "gpt-5.5") self.assertTrue(api.model.is_reasoning_model) self.assertTrue(api.model.has_vision) diff --git a/server_tests/test_prompt_pipeline.py b/server_tests/test_prompt_pipeline.py index 877a7d2d..facb757a 100644 --- a/server_tests/test_prompt_pipeline.py +++ b/server_tests/test_prompt_pipeline.py @@ -319,7 +319,7 @@ def tearDown(self) -> None: os.environ.pop(key, None) clear_search_cache() - # -- reasoning model (o3): uses create_response_stream, consumed via /send_message -- + # -- reasoning model (gpt-5.5): uses create_response_stream, consumed via /send_message -- @patch.object(OpenAIResponsesAPI, "create_response_stream") def test_reasoning_model_derivative(self, mock_stream: Mock) -> None: @@ -330,7 +330,7 @@ def test_reasoning_model_derivative(self, mock_stream: Mock) -> None: _tool_call("derive", {"expression": "x^3", "variable": "x"}), ], ) - resp = self.client.post("/send_message", json=_make_payload("derivative of x^3", "o3")) + resp = self.client.post("/send_message", json=_make_payload("derivative of x^3", "gpt-5.5")) data = json.loads(resp.data) self.assertEqual(resp.status_code, 200) diff --git a/server_tests/test_provider_connections.py b/server_tests/test_provider_connections.py index 6c82f952..6a4742d2 100644 --- a/server_tests/test_provider_connections.py +++ b/server_tests/test_provider_connections.py @@ -104,7 +104,7 @@ def test_anthropic_simple_completion(self) -> None: discover_providers() # Use the cheapest Anthropic model (Haiku), with NO tools - model = AIModel.from_identifier("claude-haiku-4-5-20251001") + model = AIModel.from_identifier("claude-haiku-4-5") api = AnthropicAPI(model=model, max_tokens=50, tools=[]) # Simple prompt that should get a short response @@ -209,7 +209,7 @@ def test_anthropic_streaming(self) -> None: discover_providers() - model = AIModel.from_identifier("claude-haiku-4-5-20251001") + model = AIModel.from_identifier("claude-haiku-4-5") api = AnthropicAPI(model=model, max_tokens=50, tools=[]) prompt = '{"user_message": "Reply with only the word: hello"}' diff --git a/server_tests/test_routes.py b/server_tests/test_routes.py index e765f640..db5f6827 100644 --- a/server_tests/test_routes.py +++ b/server_tests/test_routes.py @@ -298,12 +298,12 @@ def test_standard_model_uses_completions_api(self) -> None: def test_reasoning_model_identified(self) -> None: """Test that reasoning models are correctly identified.""" # Set a reasoning model - self.app.ai_api.set_model("o3") - self.app.responses_api.set_model("o3") + self.app.ai_api.set_model("gpt-5.5") + self.app.responses_api.set_model("gpt-5.5") # Check both APIs have the model set - self.assertEqual(self.app.ai_api.get_model().id, "o3") - self.assertEqual(self.app.responses_api.get_model().id, "o3") + self.assertEqual(self.app.ai_api.get_model().id, "gpt-5.5") + self.assertEqual(self.app.responses_api.get_model().id, "gpt-5.5") self.assertTrue(self.app.ai_api.get_model().is_reasoning_model) @patch.object(OpenAIChatCompletionsAPI, "create_chat_completion_stream") @@ -347,14 +347,14 @@ def test_stream_route_uses_responses_for_reasoning_model(self, mock_set_model: M ) # Pre-set the model to a reasoning model - self.app.ai_api.model = self.app.ai_api.model.from_identifier("o3") + self.app.ai_api.model = self.app.ai_api.model.from_identifier("gpt-5.5") test_message = { "message": json.dumps( { "user_message": "test", "use_vision": False, - "ai_model": "o3", # Reasoning model + "ai_model": "gpt-5.5", # Reasoning model } ), "svg_state": None, @@ -369,8 +369,8 @@ def test_model_selector_options(self) -> None: """Test that all expected models are configured.""" from static.ai_model import AIModel - reasoning_models = ["gpt-5-chat-latest", "gpt-5.2-chat-latest", "gpt-5.2", "o3", "o4-mini"] - standard_models = ["gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini", "gpt-5-nano", "gpt-3.5-turbo"] + reasoning_models = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.2"] + standard_models = ["gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", "gpt-4o-mini"] for model_id in reasoning_models: model = AIModel.from_identifier(model_id) @@ -446,10 +446,10 @@ def test_reasoning_stream_includes_reasoning_events(self, mock_set_model: Mock, ) # Pre-set the model to a reasoning model - self.app.ai_api.model = self.app.ai_api.model.from_identifier("o3") + self.app.ai_api.model = self.app.ai_api.model.from_identifier("gpt-5.5") test_message = { - "message": json.dumps({"user_message": "test", "use_vision": False, "ai_model": "o3"}), + "message": json.dumps({"user_message": "test", "use_vision": False, "ai_model": "gpt-5.5"}), "svg_state": None, } diff --git a/server_tests/test_search_tool_wiring_smoke.py b/server_tests/test_search_tool_wiring_smoke.py index c6dd1719..447bd8a9 100644 --- a/server_tests/test_search_tool_wiring_smoke.py +++ b/server_tests/test_search_tool_wiring_smoke.py @@ -95,7 +95,7 @@ def test_non_stream_reasoning_path_calls_intercept( { "user_message": "solve x^2-1=0", "use_vision": False, - "ai_model": "o3", + "ai_model": "gpt-5.5", } ), "svg_state": None, diff --git a/server_tests/test_tool_search_service.py b/server_tests/test_tool_search_service.py index b748656b..8ccc84e2 100644 --- a/server_tests/test_tool_search_service.py +++ b/server_tests/test_tool_search_service.py @@ -281,12 +281,12 @@ def test_search_openai_reasoning_model_uses_max_completion_tokens( ) -> None: """OpenAI reasoning models should use max_completion_tokens.""" self._setup_mock_response(mock_client, '["create_circle"]') - model = AIModel.from_identifier("o4-mini") + model = AIModel.from_identifier("gpt-5.5") service.search_tools("draw", model=model) call_args = mock_client.chat.completions.create.call_args - assert call_args.kwargs.get("model") == "o4-mini" + assert call_args.kwargs.get("model") == "gpt-5.5" assert call_args.kwargs.get("max_completion_tokens") == 500 assert "max_tokens" not in call_args.kwargs @@ -314,24 +314,24 @@ def test_search_uses_reasoning_default_model_when_none(self, mock_client: MagicM self._setup_mock_response(mock_client, '["create_circle"]') service = ToolSearchService( client=mock_client, - default_model=AIModel.from_identifier("o3"), + default_model=AIModel.from_identifier("gpt-5.5"), ) service.search_tools("draw") call_args = mock_client.chat.completions.create.call_args - assert call_args.kwargs.get("model") == "o3" + assert call_args.kwargs.get("model") == "gpt-5.5" assert call_args.kwargs.get("max_completion_tokens") == 500 assert "max_tokens" not in call_args.kwargs def test_search_uses_default_model_when_none(self, service: ToolSearchService, mock_client: MagicMock) -> None: - """search_tools should use gpt-5-nano when no model specified.""" + """search_tools should use gpt-4.1-nano when no model specified.""" self._setup_mock_response(mock_client, '["create_circle"]') service.search_tools("draw") call_args = mock_client.chat.completions.create.call_args - assert call_args.kwargs.get("model") == "gpt-5-nano" + assert call_args.kwargs.get("model") == "gpt-4.1-nano" class TestSearchToolsFormatted: diff --git a/static/ai_model.py b/static/ai_model.py index b3b3cf7d..b2c9b12d 100644 --- a/static/ai_model.py +++ b/static/ai_model.py @@ -32,39 +32,37 @@ class ModelConfig(TypedDict, total=False): ModelConfigDict = Dict[str, ModelConfig] ModelIdentifier = Literal[ # OpenAI models + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + "gpt-5.5", + "gpt-5.2", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", - "gpt-4o", "gpt-4o-mini", - "gpt-5-chat-latest", - "gpt-5.2-chat-latest", - "gpt-5.2", - "gpt-5-nano", - "gpt-3.5-turbo", - "o3", - "o4-mini", # Anthropic models - "claude-opus-4-5-20251101", - "claude-sonnet-4-5-20250929", - "claude-haiku-4-5-20251001", + "claude-fable-5", + "claude-opus-4-8", + "claude-sonnet-5", + "claude-haiku-4-5", # OpenRouter models (paid) - "google/gemini-2.5-pro", - "google/gemini-3-pro-preview", - "google/gemini-3-flash-preview", - "deepseek/deepseek-v3.2", - "x-ai/grok-code-fast-1", - "z-ai/glm-4.7", - "minimax/minimax-m2.1", - "anthropic/claude-sonnet-4.5", + "google/gemini-3.1-pro-preview", + "google/gemini-3.5-flash", + "anthropic/claude-sonnet-5", + "deepseek/deepseek-v4-pro", + "qwen/qwen3.7-max", + "z-ai/glm-5.2", + "x-ai/grok-4.5", + "minimax/minimax-m3", # OpenRouter models (free) "meta-llama/llama-3.3-70b-instruct:free", - "google/gemma-3-27b-it:free", + "google/gemma-4-31b-it:free", "openai/gpt-oss-20b:free", "openai/gpt-oss-120b:free", "qwen/qwen3-next-80b-a3b-instruct:free", - "z-ai/glm-4.5-air:free", "nvidia/nemotron-3-nano-30b-a3b:free", + "tencent/hy3:free", ] @@ -81,36 +79,37 @@ class AIModel: # OpenAI Models # =================== # Reasoning models (use Responses API) - "gpt-5-chat-latest": { + "gpt-5.6-sol": { "has_vision": True, "is_reasoning_model": True, "provider": PROVIDER_OPENAI, - "display_name": "GPT-5 Chat Latest", + "display_name": "GPT-5.6 Sol", }, - "gpt-5.2-chat-latest": { + "gpt-5.6-terra": { "has_vision": True, "is_reasoning_model": True, "provider": PROVIDER_OPENAI, - "display_name": "GPT-5.2 Chat Latest", + "display_name": "GPT-5.6 Terra", }, - "gpt-5.2": { + "gpt-5.6-luna": { "has_vision": True, "is_reasoning_model": True, - "reasoning_effort": "medium", "provider": PROVIDER_OPENAI, - "display_name": "GPT-5.2 (Medium Reasoning)", + "display_name": "GPT-5.6 Luna", }, - "o3": { + "gpt-5.5": { "has_vision": True, "is_reasoning_model": True, + "reasoning_effort": "medium", "provider": PROVIDER_OPENAI, - "display_name": "o3 (Reasoning)", + "display_name": "GPT-5.5 (Medium Reasoning)", }, - "o4-mini": { + "gpt-5.2": { "has_vision": True, "is_reasoning_model": True, + "reasoning_effort": "medium", "provider": PROVIDER_OPENAI, - "display_name": "o4-mini (Reasoning)", + "display_name": "GPT-5.2 (Medium Reasoning)", }, # Standard models (use Chat Completions API) "gpt-4.1": { @@ -131,46 +130,37 @@ class AIModel: "provider": PROVIDER_OPENAI, "display_name": "GPT-4.1 Nano", }, - "gpt-4o": { - "has_vision": True, - "is_reasoning_model": False, - "provider": PROVIDER_OPENAI, - "display_name": "GPT-4o", - }, "gpt-4o-mini": { "has_vision": True, "is_reasoning_model": False, "provider": PROVIDER_OPENAI, "display_name": "GPT-4o Mini", }, - "gpt-5-nano": { - "has_vision": True, - "is_reasoning_model": False, - "provider": PROVIDER_OPENAI, - "display_name": "GPT-5 Nano", - }, - "gpt-3.5-turbo": { - "has_vision": False, - "is_reasoning_model": False, - "provider": PROVIDER_OPENAI, - "display_name": "GPT-3.5 Turbo", - }, # =================== # Anthropic Models # =================== - "claude-opus-4-5-20251101": { + # Adaptive-thinking Claude models (Fable 5, Opus 4.8, Sonnet 5) reject the + # temperature sampling parameter, so they are flagged is_reasoning_model=True; + # AnthropicAPI._apply_temperature() uses that flag to omit temperature for them. + "claude-fable-5": { "has_vision": True, - "is_reasoning_model": False, + "is_reasoning_model": True, "provider": PROVIDER_ANTHROPIC, - "display_name": "Claude Opus 4.5", + "display_name": "Claude Fable 5", }, - "claude-sonnet-4-5-20250929": { + "claude-opus-4-8": { "has_vision": True, - "is_reasoning_model": False, + "is_reasoning_model": True, "provider": PROVIDER_ANTHROPIC, - "display_name": "Claude Sonnet 4.5", + "display_name": "Claude Opus 4.8", }, - "claude-haiku-4-5-20251001": { + "claude-sonnet-5": { + "has_vision": True, + "is_reasoning_model": True, + "provider": PROVIDER_ANTHROPIC, + "display_name": "Claude Sonnet 5", + }, + "claude-haiku-4-5": { "has_vision": True, "is_reasoning_model": False, "provider": PROVIDER_ANTHROPIC, @@ -179,53 +169,53 @@ class AIModel: # =================== # OpenRouter Models (Paid) # =================== - "google/gemini-2.5-pro": { + "google/gemini-3.1-pro-preview": { "has_vision": True, "is_reasoning_model": False, "provider": PROVIDER_OPENROUTER, - "display_name": "Gemini 2.5 Pro", + "display_name": "Gemini 3.1 Pro", }, - "google/gemini-3-pro-preview": { + "google/gemini-3.5-flash": { "has_vision": True, "is_reasoning_model": False, "provider": PROVIDER_OPENROUTER, - "display_name": "Gemini 3 Pro Preview", + "display_name": "Gemini 3.5 Flash", }, - "google/gemini-3-flash-preview": { + "anthropic/claude-sonnet-5": { "has_vision": True, "is_reasoning_model": False, "provider": PROVIDER_OPENROUTER, - "display_name": "Gemini 3 Flash Preview", + "display_name": "Claude Sonnet 5 (OpenRouter)", }, - "deepseek/deepseek-v3.2": { + "deepseek/deepseek-v4-pro": { "has_vision": False, "is_reasoning_model": False, "provider": PROVIDER_OPENROUTER, - "display_name": "DeepSeek V3.2", + "display_name": "DeepSeek V4 Pro", }, - "x-ai/grok-code-fast-1": { + "qwen/qwen3.7-max": { "has_vision": False, "is_reasoning_model": False, "provider": PROVIDER_OPENROUTER, - "display_name": "Grok Code Fast", + "display_name": "Qwen3.7 Max", }, - "z-ai/glm-4.7": { + "z-ai/glm-5.2": { "has_vision": False, "is_reasoning_model": False, "provider": PROVIDER_OPENROUTER, - "display_name": "GLM 4.7", + "display_name": "GLM 5.2", }, - "minimax/minimax-m2.1": { - "has_vision": False, + "x-ai/grok-4.5": { + "has_vision": True, "is_reasoning_model": False, "provider": PROVIDER_OPENROUTER, - "display_name": "MiniMax M2.1", + "display_name": "Grok 4.5", }, - "anthropic/claude-sonnet-4.5": { + "minimax/minimax-m3": { "has_vision": True, "is_reasoning_model": False, "provider": PROVIDER_OPENROUTER, - "display_name": "Claude Sonnet 4.5 (OpenRouter)", + "display_name": "MiniMax M3", }, # =================== # OpenRouter Models (Free) @@ -236,11 +226,11 @@ class AIModel: "provider": PROVIDER_OPENROUTER, "display_name": "Llama 3.3 70B", }, - "google/gemma-3-27b-it:free": { + "google/gemma-4-31b-it:free": { "has_vision": True, "is_reasoning_model": False, "provider": PROVIDER_OPENROUTER, - "display_name": "Gemma 3 27B", + "display_name": "Gemma 4 31B", }, "openai/gpt-oss-20b:free": { "has_vision": False, @@ -258,23 +248,23 @@ class AIModel: "has_vision": False, "is_reasoning_model": False, "provider": PROVIDER_OPENROUTER, - "display_name": "Qwen3 80B", + "display_name": "Qwen3 Next 80B", }, - "z-ai/glm-4.5-air:free": { + "nvidia/nemotron-3-nano-30b-a3b:free": { "has_vision": False, "is_reasoning_model": False, "provider": PROVIDER_OPENROUTER, - "display_name": "GLM 4.5 Air", + "display_name": "Nemotron 3 Nano 30B", }, - "nvidia/nemotron-3-nano-30b-a3b:free": { + "tencent/hy3:free": { "has_vision": False, "is_reasoning_model": False, "provider": PROVIDER_OPENROUTER, - "display_name": "Nemotron 3 30B", + "display_name": "Tencent Hy3", }, } - DEFAULT_MODEL = "gpt-5.2" + DEFAULT_MODEL = "gpt-5.5" def __init__( self, diff --git a/static/app_manager.py b/static/app_manager.py index 47acfda7..f79ba4c8 100644 --- a/static/app_manager.py +++ b/static/app_manager.py @@ -73,7 +73,7 @@ class AppManager: Managed Dependencies: - OpenAIChatCompletionsAPI: Chat Completions API for standard models - - OpenAIResponsesAPI: Responses API for reasoning models (GPT-5, o3, o4-mini) + - OpenAIResponsesAPI: Responses API for reasoning models (GPT-5.6, GPT-5.5, GPT-5.2) - WorkspaceManager: File system operations and workspace organization - LogManager: Application-wide logging and debugging support - Route Registration: RESTful API endpoint configuration diff --git a/static/openai_responses_api.py b/static/openai_responses_api.py index 3ba8718e..311cfc07 100644 --- a/static/openai_responses_api.py +++ b/static/openai_responses_api.py @@ -1,7 +1,7 @@ """ MatHud OpenAI Responses API -Responses API implementation for reasoning models (GPT-5, o3, o4-mini). +Responses API implementation for reasoning models (GPT-5.6, GPT-5.5, GPT-5.2). Streams reasoning tokens during the thinking phase. """ @@ -20,7 +20,7 @@ class OpenAIResponsesAPI(OpenAIAPIBase): - """OpenAI Responses API for reasoning models (GPT-5, o3, o4-mini). + """OpenAI Responses API for reasoning models (GPT-5.6, GPT-5.5, GPT-5.2). Uses `previous_response_id` for multi-turn conversations, allowing OpenAI to manage conversation state server-side. This properly handles images diff --git a/static/providers/anthropic_api.py b/static/providers/anthropic_api.py index a2a9fa60..7771892e 100644 --- a/static/providers/anthropic_api.py +++ b/static/providers/anthropic_api.py @@ -54,7 +54,7 @@ def __init__( """Initialize Anthropic API client. Args: - model: AI model to use. Defaults to Claude Sonnet 4.5. + model: AI model to use. Defaults to Claude Sonnet 5. temperature: Sampling temperature. tools: Custom tool definitions. max_tokens: Maximum tokens in response. @@ -72,7 +72,7 @@ def __init__( # Initialize base class (uses OpenAI client for compatibility, but we won't use it) # We override the key methods to use Anthropic instead - self.model: AIModel = model if model is not None else AIModel.from_identifier("claude-sonnet-4-5-20250929") + self.model: AIModel = model if model is not None else AIModel.from_identifier("claude-sonnet-5") self.temperature = temperature self.max_tokens = max_tokens self._tool_mode: ToolMode = tool_mode @@ -237,6 +237,17 @@ def _parse_and_prepare_message(self, full_prompt: str) -> Optional[Dict[str, Any message_content = self._prepare_message_content(full_prompt) return {"role": "user", "content": message_content} + def _apply_temperature(self, request_kwargs: Dict[str, Any]) -> None: + """Add the temperature parameter only for models that accept it. + + Adaptive-thinking Claude models (Claude Fable 5, Opus 4.8, Sonnet 5, and later) + reject the ``temperature`` sampling parameter with a 400 error. Those models are + flagged ``is_reasoning_model`` in the registry, so only send ``temperature`` for + non-reasoning models (e.g. Claude Haiku 4.5) that still support it. + """ + if not self.model.is_reasoning_model: + request_kwargs["temperature"] = self.temperature + def create_chat_completion(self, full_prompt: str) -> Any: """Create chat completion with Anthropic API.""" user_message = self._parse_and_prepare_message(full_prompt) @@ -248,13 +259,13 @@ def create_chat_completion(self, full_prompt: str) -> Any: anthropic_tools = self._convert_tools_to_anthropic() # Anthropic API doesn't accept empty tools list - must be None or non-empty - create_kwargs = { + create_kwargs: Dict[str, Any] = { "model": self.model.id, "max_tokens": self.max_tokens, "system": self._system_prompt, "messages": anthropic_messages, - "temperature": self.temperature, } + self._apply_temperature(create_kwargs) if anthropic_tools: create_kwargs["tools"] = anthropic_tools @@ -344,13 +355,13 @@ def create_chat_completion_stream(self, full_prompt: str) -> Iterator[StreamEven anthropic_tools = self._convert_tools_to_anthropic() # Anthropic API doesn't accept empty tools list - must be None or non-empty - stream_kwargs = { + stream_kwargs: Dict[str, Any] = { "model": self.model.id, "max_tokens": self.max_tokens, "system": self._system_prompt, "messages": anthropic_messages, - "temperature": self.temperature, } + self._apply_temperature(stream_kwargs) if anthropic_tools: stream_kwargs["tools"] = anthropic_tools diff --git a/static/providers/openrouter_api.py b/static/providers/openrouter_api.py index dc32a69a..25dc1a04 100644 --- a/static/providers/openrouter_api.py +++ b/static/providers/openrouter_api.py @@ -54,7 +54,7 @@ def __init__( """Initialize OpenRouter API client. Args: - model: AI model to use. Defaults to Gemini 2.5 Pro. + model: AI model to use. Defaults to Gemini 3.1 Pro. temperature: Sampling temperature. tools: Custom tool definitions. max_tokens: Maximum tokens in response. @@ -70,8 +70,8 @@ def __init__( }, ) - # Set model (default to Gemini 2.5 Pro if not specified) - self.model: AIModel = model if model is not None else AIModel.from_identifier("google/gemini-2.5-pro") + # Set model (default to Gemini 3.1 Pro if not specified) + self.model: AIModel = model if model is not None else AIModel.from_identifier("google/gemini-3.1-pro-preview") self.temperature = temperature self.max_tokens = max_tokens self._tool_mode: ToolMode = tool_mode diff --git a/static/routes.py b/static/routes.py index 7ae92005..cc37880f 100644 --- a/static/routes.py +++ b/static/routes.py @@ -752,8 +752,8 @@ def send_message_stream() -> ResponseReturnValue: """Stream AI response tokens for the provided message payload. Routes to appropriate API based on model type: - - Reasoning models (GPT-5, o3, o4-mini): Uses Responses API with reasoning streaming - - Standard models (GPT-4.1, GPT-4o): Uses Chat Completions API + - Reasoning models (GPT-5.6, GPT-5.5, GPT-5.2): Uses Responses API with reasoning streaming + - Standard models (GPT-4.1 family, GPT-4o mini): Uses Chat Completions API Returns a newline-delimited JSON stream with events of shape: {"type":"reasoning","text":"..."}\n for reasoning tokens (reasoning models only) diff --git a/static/tool_search_service.py b/static/tool_search_service.py index 9b52be56..604355e0 100644 --- a/static/tool_search_service.py +++ b/static/tool_search_service.py @@ -549,7 +549,6 @@ def search_tools_local( scores: Dict[str, float] = defaultdict(float) # 1. Exact tool name match - query_lower = query.lower().strip() for token in query_tokens: if token in _ALL_TOOL_NAMES: scores[token] += 8.0 @@ -882,9 +881,9 @@ def _search_tools_api( Returns: List of matching tool definitions, ordered by relevance. """ - # Use provided model, instance default, or fallback to gpt-4.1-mini. + # Use provided model, instance default, or fallback to gpt-4.1-nano. if model is None: - model = self.default_model or AIModel.from_identifier("gpt-5-nano") + model = self.default_model or AIModel.from_identifier("gpt-4.1-nano") # Build the prompt tool_descriptions = self.build_tool_descriptions() diff --git a/templates/index.html b/templates/index.html index 1daf3180..85f12e74 100644 --- a/templates/index.html +++ b/templates/index.html @@ -334,7 +334,7 @@ ]; var hasModels = false; - var defaultModel = 'gpt-5.2'; + var defaultModel = 'gpt-5.5'; var defaultFound = false; for (var i = 0; i < groups.length; i++) {