diff --git a/backend/app/crud/model_config.py b/backend/app/crud/model_config.py index afa3f27c2..2cc2b3ef8 100644 --- a/backend/app/crud/model_config.py +++ b/backend/app/crud/model_config.py @@ -286,6 +286,19 @@ def is_reasoning_model(session: Session, provider: Provider, model_name: str) -> return "effort" in model.config +def is_summary_model(session: Session, provider: Provider, model_name: str) -> bool: + """Return True if the model is configured with a summary `effort` control. + + A model is considered summary-capable if its `config` JSON contains an + `summary` key; models that instead expose a `temperature` key are treated + as standard chat models. + """ + model = get_model_config(session=session, provider=provider, model_name=model_name) + if model is None or not isinstance(model.config, dict): + return False + return "summary" in model.config + + def estimate_model_cost( session: Session, provider: Provider, diff --git a/backend/app/services/llm/mappers.py b/backend/app/services/llm/mappers.py index 0850906a0..7000dda76 100644 --- a/backend/app/services/llm/mappers.py +++ b/backend/app/services/llm/mappers.py @@ -70,7 +70,9 @@ def map_kaapi_to_openai_params( - instructions → instructions - knowledge_base_ids → tools[file_search].vector_store_ids - max_num_results → tools[file_search].max_num_results (fallback default) - - reasoning → reasoning.effort (if reasoning supported by model else suppressed) + - effort → reasoning.effort (if reasoning supported by model else suppressed) + - reasoning → legacy alias for effort, used only when effort is absent + - summary → reasoning.summary (if reasoning supported by model else suppressed) - temperature → temperature (if reasoning not supported by model else suppressed) Returns: @@ -83,6 +85,8 @@ def map_kaapi_to_openai_params( model = kaapi_params.get("model") reasoning = kaapi_params.get("reasoning") + effort = kaapi_params.get("effort") + summary = kaapi_params.get("summary") temperature = kaapi_params.get("temperature") instructions = kaapi_params.get("instructions") knowledge_base_ids = kaapi_params.get("knowledge_base_ids") @@ -92,10 +96,19 @@ def map_kaapi_to_openai_params( session=session, provider="openai", model_name=model ) + # 'effort' is the canonical knob; 'reasoning' is a legacy alias kept for + # backward compatibility and only consulted when 'effort' is absent + effort_value = effort if effort is not None else reasoning + # Handle reasoning vs temperature mutual exclusivity if support_reasoning: - if reasoning is not None: - openai_params["reasoning"] = {"effort": reasoning} + if effort_value is not None or summary is not None: + openai_params["reasoning"] = {} + if effort_value is not None: + openai_params["reasoning"] = {"effort": effort_value} + + if summary is not None: + openai_params["reasoning"]["summary"] = summary if temperature is not None: warnings.append( @@ -103,10 +116,10 @@ def map_kaapi_to_openai_params( "supports reasoning, and temperature is ignored when reasoning is enabled." ) else: - if reasoning is not None: + if effort_value is not None: warnings.append( - "Parameter 'reasoning' was suppressed because the selected model " - "does not support reasoning." + "Parameters 'reasoning'/'effort' were suppressed because the " + "selected model does not support reasoning." ) if temperature is not None: diff --git a/backend/app/tests/crud/test_model_config.py b/backend/app/tests/crud/test_model_config.py index 65041caa9..5422b07ae 100644 --- a/backend/app/tests/crud/test_model_config.py +++ b/backend/app/tests/crud/test_model_config.py @@ -330,3 +330,79 @@ def test_validate_blob_text_model_accepted_for_text_type( _patch_validators(monkeypatch, row=row, supported=True) blob = _make_blob("openai", "text", {"model": "gpt-4o"}) model_config_crud.validate_blob_model_or_raise(session=None, blob=blob) # type: ignore[arg-type] + + +def _patch_model_config( + monkeypatch: pytest.MonkeyPatch, + config: Any, +) -> None: + model = SimpleNamespace(config=config) + monkeypatch.setattr( + model_config_crud, + "get_model_config", + lambda session, provider, model_name: model, + ) + + +def test_is_summary_model_true_when_summary_in_config( + monkeypatch: pytest.MonkeyPatch, +) -> None: + _patch_model_config(monkeypatch, config={"summary": "auto"}) + + assert ( + model_config_crud.is_summary_model( + session=None, # type: ignore[arg-type] + provider="openai", + model_name="gpt-5", + ) + is True + ) + + +def test_is_summary_model_false_when_summary_absent( + monkeypatch: pytest.MonkeyPatch, +) -> None: + _patch_model_config(monkeypatch, config={"temperature": 0.7}) + + assert ( + model_config_crud.is_summary_model( + session=None, # type: ignore[arg-type] + provider="openai", + model_name="gpt-4o", + ) + is False + ) + + +def test_is_summary_model_false_for_missing_model( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + model_config_crud, + "get_model_config", + lambda session, provider, model_name: None, + ) + + assert ( + model_config_crud.is_summary_model( + session=None, # type: ignore[arg-type] + provider="openai", + model_name="does-not-exist", + ) + is False + ) + + +def test_is_summary_model_false_for_non_dict_config( + monkeypatch: pytest.MonkeyPatch, +) -> None: + _patch_model_config(monkeypatch, config=["invalid"]) + + assert ( + model_config_crud.is_summary_model( + session=None, # type: ignore[arg-type] + provider="openai", + model_name="gpt-4o", + ) + is False + ) diff --git a/backend/app/tests/services/llm/test_mappers.py b/backend/app/tests/services/llm/test_mappers.py index dd86e633b..7991fbdb4 100644 --- a/backend/app/tests/services/llm/test_mappers.py +++ b/backend/app/tests/services/llm/test_mappers.py @@ -116,6 +116,21 @@ def test_reasoning_suppressed_for_non_reasoning_models(self, db: Session): assert "reasoning" in warnings[0].lower() assert "does not support reasoning" in warnings[0] + def test_summary_mapping_for_reasoning_models(self, db: Session): + """Test summary maps into reasoning block alongside effort for reasoning models.""" + kaapi_params = TextLLMParams( + model="gpt-5", + effort="high", + summary="detailed", + ) + + result, warnings = map_kaapi_to_openai_params( + session=db, kaapi_params=kaapi_params.model_dump(exclude_none=True) + ) + + assert result["model"] == "gpt-5" + assert result["reasoning"] == {"effort": "high", "summary": "detailed"} + class TestMapKaapiToGoogleParams: """Test cases for map_kaapi_to_google_params function with completion_type."""