diff --git a/lib/cli/src/crewai_cli/constants.py b/lib/cli/src/crewai_cli/constants.py index a5f9371ff..e6f762af1 100644 --- a/lib/cli/src/crewai_cli/constants.py +++ b/lib/cli/src/crewai_cli/constants.py @@ -132,6 +132,10 @@ PROVIDERS: list[str] = [ MODELS: dict[str, list[str]] = { "openai": [ + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + "gpt-5.6", "gpt-5.5", "gpt-5.5-pro", "gpt-5.4", diff --git a/lib/cli/src/crewai_cli/create_json_crew.py b/lib/cli/src/crewai_cli/create_json_crew.py index ccbefa5df..a4b04975f 100644 --- a/lib/cli/src/crewai_cli/create_json_crew.py +++ b/lib/cli/src/crewai_cli/create_json_crew.py @@ -49,11 +49,15 @@ _PROVIDERS: list[tuple[str, str]] = [ # live from the vendor's own API via ``model_catalog.get_provider_models``; # this list is the hand-verified backstop used when no API key is available. # Keep entries to real, current model ids — last verified against each vendor's -# official model docs on 2026-07-05. +# official model docs on 2026-08-17. _PROVIDER_MODELS: dict[str, list[tuple[str, str]]] = { "openai": [ ("gpt-5.5", "GPT-5.5"), ("gpt-5.5-pro", "GPT-5.5 Pro"), + ("gpt-5.6-sol", "GPT-5.6 Sol"), + ("gpt-5.6-terra", "GPT-5.6 Terra"), + ("gpt-5.6-luna", "GPT-5.6 Luna"), + ("gpt-5.6", "GPT-5.6"), ("gpt-5.4", "GPT-5.4"), ("gpt-5.4-mini", "GPT-5.4 Mini"), ("gpt-5.2", "GPT-5.2"), diff --git a/lib/cli/tests/test_constants.py b/lib/cli/tests/test_constants.py index 527ae1dec..29ec2d55f 100644 --- a/lib/cli/tests/test_constants.py +++ b/lib/cli/tests/test_constants.py @@ -18,3 +18,10 @@ def test_huggingface_models(): """Test that Huggingface models are properly configured.""" assert "huggingface" in MODELS assert len(MODELS["huggingface"]) > 0 + + +def test_openai_models_include_gpt56_family() -> None: + """Curated OpenAI list should include Sol, Terra, Luna, and the alias.""" + openai_models = MODELS["openai"] + for model in ("gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.6"): + assert model in openai_models diff --git a/lib/crewai/src/crewai/llm.py b/lib/crewai/src/crewai/llm.py index 81807c920..371aadf87 100644 --- a/lib/crewai/src/crewai/llm.py +++ b/lib/crewai/src/crewai/llm.py @@ -170,6 +170,7 @@ LLM_CONTEXT_WINDOW_SIZES: Final[dict[str, int]] = { "gpt-4o": 128000, "gpt-4o-mini": 200000, "gpt-5.4-mini": 200000, + "gpt-5.6": 1050000, # sol, terra, luna, and the gpt-5.6 alias "gpt-4-turbo": 128000, "gpt-4.1": 1047576, # Based on official docs "gpt-4.1-mini-2025-04-14": 1047576, @@ -2447,6 +2448,18 @@ class LLM(BaseLLM): logging.error(f"Failed to get supported params: {e!s}") return True # Default to True + def _context_window_model_name(self) -> str: + """Return the model id used for context-window lookup. + + LiteLLM keeps provider-qualified names such as ``openai/gpt-5.6-luna``. + Strip a recognized provider prefix so those resolve through the same + mapping as the bare model id. Unrecognized prefixes are left intact. + """ + prefix, separator, remainder = self.model.partition("/") + if separator and remainder and prefix.lower() in SUPPORTED_NATIVE_PROVIDERS: + return remainder + return self.model + def get_context_window_size(self) -> int: """ Returns the context window size, using 75% of the maximum to avoid @@ -2470,8 +2483,9 @@ class LLM(BaseLLM): self.context_window_size = int( DEFAULT_CONTEXT_WINDOW_SIZE * CONTEXT_WINDOW_USAGE_RATIO ) + model_name = self._context_window_model_name() for key, value in LLM_CONTEXT_WINDOW_SIZES.items(): - if self.model.startswith(key): + if model_name.startswith(key) or self.model.startswith(key): self.context_window_size = int(value * CONTEXT_WINDOW_USAGE_RATIO) return self.context_window_size diff --git a/lib/crewai/src/crewai/llms/providers/azure/completion.py b/lib/crewai/src/crewai/llms/providers/azure/completion.py index 4597fd623..0f7cbd4fc 100644 --- a/lib/crewai/src/crewai/llms/providers/azure/completion.py +++ b/lib/crewai/src/crewai/llms/providers/azure/completion.py @@ -1302,20 +1302,21 @@ class AzureCompletion(BaseLLM): f"Context window for {key} must be between {min_context} and {max_context}" ) + # Longest prefix first. Always insert new keys in that order so + # startswith prefers gpt-5.6 over gpt-5, gpt-4o-mini over gpt-4o, etc. context_windows = { - "gpt-4": 8192, - "gpt-4o": 128000, - "gpt-4o-mini": 200000, - "gpt-5.4-mini": 200000, - "gpt-4-turbo": 128000, - "gpt-35-turbo": 16385, - "gpt-3.5-turbo": 16385, "text-embedding": 8191, + "gpt-3.5-turbo": 16385, + "gpt-5.4-mini": 200000, + "gpt-35-turbo": 16385, + "gpt-4o-mini": 200000, + "gpt-4-turbo": 128000, + "gpt-5.6": 1050000, + "gpt-4o": 128000, + "gpt-4": 8192, } - for model_prefix, size in sorted( - context_windows.items(), key=lambda x: len(x[0]), reverse=True - ): + for model_prefix, size in context_windows.items(): if self.model.startswith(model_prefix): return int(size * CONTEXT_WINDOW_USAGE_RATIO) diff --git a/lib/crewai/src/crewai/llms/providers/openai/completion.py b/lib/crewai/src/crewai/llms/providers/openai/completion.py index 4e7079266..2b667e168 100644 --- a/lib/crewai/src/crewai/llms/providers/openai/completion.py +++ b/lib/crewai/src/crewai/llms/providers/openai/completion.py @@ -2664,23 +2664,25 @@ class OpenAICompletion(BaseLLM): f"Context window for {key} must be between {min_context} and {max_context}" ) - # Context window sizes for OpenAI models + # Longest prefix first. Always insert new keys in that order so + # startswith prefers gpt-5.6 over gpt-5, gpt-4o-mini over gpt-4o, etc. context_windows = { - "gpt-4": 8192, - "gpt-4o": 128000, - "gpt-4o-mini": 200000, - "gpt-5.4-mini": 200000, - "gpt-4-turbo": 128000, - "gpt-4.1": 1047576, "gpt-4.1-mini-2025-04-14": 1047576, "gpt-4.1-nano-2025-04-14": 1047576, - "gpt-5": 1047576, + "gpt-5.4-mini": 200000, + "gpt-4-turbo": 128000, + "gpt-4o-mini": 200000, "gpt-5-mini": 1047576, "gpt-5-nano": 1047576, "o1-preview": 128000, + "gpt-5.6": 1050000, "o1-mini": 128000, "o3-mini": 200000, "o4-mini": 200000, + "gpt-4.1": 1047576, + "gpt-4o": 128000, + "gpt-5": 1047576, + "gpt-4": 8192, } for model_prefix, size in context_windows.items(): diff --git a/lib/crewai/tests/llms/azure/test_azure.py b/lib/crewai/tests/llms/azure/test_azure.py index 753de4a37..746d719aa 100644 --- a/lib/crewai/tests/llms/azure/test_azure.py +++ b/lib/crewai/tests/llms/azure/test_azure.py @@ -5,7 +5,7 @@ from unittest.mock import patch, MagicMock, Mock from urllib.parse import urlparse import pytest -from crewai.llm import LLM +from crewai.llm import CONTEXT_WINDOW_USAGE_RATIO, LLM from crewai.crew import Crew from crewai.agent import Agent from crewai.task import Task @@ -653,6 +653,26 @@ def test_azure_context_window_size(): assert context_size_gpt4o > context_size_gpt4 # GPT-4o has larger context +@pytest.mark.parametrize( + "model", + [ + "azure/gpt-5.6", + "azure/gpt-5.6-sol", + "azure/gpt-5.6-terra", + "azure/gpt-5.6-luna", + ], +) +def test_azure_gpt56_family_uses_official_context_window(model: str) -> None: + """Azure must not fall back to the 8k default for GPT-5.6 deployments.""" + llm = LLM(model=model) + assert llm.get_context_window_size() == int(1_050_000 * CONTEXT_WINDOW_USAGE_RATIO) + + +def test_azure_gpt54_mini_keeps_its_window() -> None: + llm = LLM(model="azure/gpt-5.4-mini") + assert llm.get_context_window_size() == int(200000 * CONTEXT_WINDOW_USAGE_RATIO) + + def test_azure_message_formatting(): """ Test that messages are properly formatted for Azure API diff --git a/lib/crewai/tests/llms/openai/test_openai.py b/lib/crewai/tests/llms/openai/test_openai.py index 925a941cb..3dec38f37 100644 --- a/lib/crewai/tests/llms/openai/test_openai.py +++ b/lib/crewai/tests/llms/openai/test_openai.py @@ -6,7 +6,7 @@ from unittest.mock import patch, MagicMock import openai import pytest -from crewai.llm import LLM +from crewai.llm import CONTEXT_WINDOW_USAGE_RATIO, LLM from crewai.llms.providers.openai.completion import OpenAICompletion, ResponsesAPIResult from crewai.crew import Crew from crewai.agent import Agent @@ -1841,6 +1841,30 @@ def test_openai_gpt5_still_applies_stop_words_client_side(): assert "I need to search" in result +@pytest.mark.parametrize( + "model", + ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"], +) +def test_openai_gpt56_family_uses_official_context_window(model: str) -> None: + """Native OpenAI must not inherit the shorter gpt-5 window for GPT-5.6.""" + llm = OpenAICompletion(model=model) + assert llm.get_context_window_size() == int(1_050_000 * CONTEXT_WINDOW_USAGE_RATIO) + + +def test_openai_prefixed_gpt56_luna_uses_official_context_window() -> None: + llm = LLM(model="openai/gpt-5.6-luna") + assert isinstance(llm, OpenAICompletion) + assert llm.model == "gpt-5.6-luna" + assert llm.get_context_window_size() == int(1_050_000 * CONTEXT_WINDOW_USAGE_RATIO) + + +def test_openai_gpt5_and_gpt54_mini_keep_their_windows() -> None: + gpt5 = OpenAICompletion(model="gpt-5") + gpt54_mini = OpenAICompletion(model="gpt-5.4-mini") + assert gpt5.get_context_window_size() == int(1_047_576 * CONTEXT_WINDOW_USAGE_RATIO) + assert gpt54_mini.get_context_window_size() == int(200000 * CONTEXT_WINDOW_USAGE_RATIO) + + def test_openai_stop_words_still_applied_to_regular_responses(): """ Test that stop words ARE still applied for regular (non-structured) responses. diff --git a/lib/crewai/tests/test_llm.py b/lib/crewai/tests/test_llm.py index 08d80bbae..61c6362b2 100644 --- a/lib/crewai/tests/test_llm.py +++ b/lib/crewai/tests/test_llm.py @@ -11,7 +11,7 @@ from crewai.events.event_types import ( ToolUsageFinishedEvent, ToolUsageStartedEvent, ) -from crewai.llm import CONTEXT_WINDOW_USAGE_RATIO, LLM +from crewai.llm import CONTEXT_WINDOW_USAGE_RATIO, DEFAULT_CONTEXT_WINDOW_SIZE, LLM from crewai.llms.providers.anthropic.completion import AnthropicCompletion from crewai.utilities.token_counter_callback import TokenCalcHandler from pydantic import BaseModel @@ -343,6 +343,49 @@ def test_context_window_validation(): assert "must be between 1024 and 2097152" in str(excinfo.value) +@pytest.mark.parametrize( + "model", + ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"], +) +def test_gpt56_family_uses_official_context_window(model: str) -> None: + """GPT-5.6 Sol, Terra, Luna, and the alias share a 1.05M window.""" + llm = LLM(model=model, is_litellm=True) + assert llm.get_context_window_size() == int(1_050_000 * CONTEXT_WINDOW_USAGE_RATIO) + + +def test_gpt56_does_not_override_gpt54_mini_window() -> None: + """A more specific older prefix must keep its own window.""" + llm = LLM(model="gpt-5.4-mini", is_litellm=True) + assert llm.get_context_window_size() == int(200000 * CONTEXT_WINDOW_USAGE_RATIO) + + +@pytest.mark.parametrize( + "model", + [ + "openai/gpt-5.6", + "openai/gpt-5.6-sol", + "openai/gpt-5.6-terra", + "openai/gpt-5.6-luna", + ], +) +def test_gpt56_family_context_window_with_provider_prefix(model: str) -> None: + """LiteLLM keeps the provider prefix on self.model; lookup must still hit gpt-5.6.""" + llm = LLM(model=model, is_litellm=True) + assert llm.model == model + assert llm._context_window_model_name() == model.partition("/")[2] + assert llm.get_context_window_size() == int(1_050_000 * CONTEXT_WINDOW_USAGE_RATIO) + + +def test_unrecognized_provider_prefix_is_not_stripped() -> None: + """Unknown prefixes stay on the lookup name and do not inherit the gpt-5.6 window.""" + llm = LLM(model="acme/gpt-5.6-luna", is_litellm=True) + assert llm.model == "acme/gpt-5.6-luna" + assert llm._context_window_model_name() == "acme/gpt-5.6-luna" + assert llm.get_context_window_size() == int( + DEFAULT_CONTEXT_WINDOW_SIZE * CONTEXT_WINDOW_USAGE_RATIO + ) + + @pytest.fixture def get_weather_tool_schema(): return {