From 654e3d4dbe85744788cf6a5723d3448ec4b138d3 Mon Sep 17 00:00:00 2001 From: Piyush Mondal Date: Mon, 19 Oct 2026 13:49:28 +0000 Subject: [PATCH 1/2] fix(adk): handle Ollama options written as strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Options come from the CRD as strings, but the coercion was too strict. num_ctx: "2048.0" crashed the agent with a ValueError, and booleans only worked when spelled exactly "true" — "1" or "yes" silently turned off. Now int options accept "2048.0", bools accept the usual 1/0, yes/no, on/off, and a value we can't parse is passed through with a warning instead of crashing. Signed-off-by: Piyush Mondal --- .../kagent-adk/src/kagent/adk/types.py | 51 +++++++++++++++---- .../tests/unittests/models/test_ollama.py | 51 +++++++++++++++++++ 2 files changed, 93 insertions(+), 9 deletions(-) diff --git a/python/packages/kagent-adk/src/kagent/adk/types.py b/python/packages/kagent-adk/src/kagent/adk/types.py index df2c3e5307..11e03af1d5 100644 --- a/python/packages/kagent-adk/src/kagent/adk/types.py +++ b/python/packages/kagent-adk/src/kagent/adk/types.py @@ -167,6 +167,25 @@ def header_provider(readonly_context: Optional[ReadonlyContext]) -> dict[str, st return header_provider +_BOOL_TRUE = {"1", "true", "yes", "on"} +_BOOL_FALSE = {"0", "false", "no", "off"} + + +def _coerce_option_bool(value: str) -> bool: + """Parse a CRD string into a bool, accepting the common truthy/falsy spellings. + + The previous implementation treated only the exact string ``"true"`` as True, + so ``"1"``, ``"yes"`` or ``"True"`` silently became False. Anything outside the + recognized spellings raises ValueError so the caller can fall back and warn. + """ + normalized = value.strip().lower() + if normalized in _BOOL_TRUE: + return True + if normalized in _BOOL_FALSE: + return False + raise ValueError(f"invalid boolean option value: {value!r}") + + def _convert_ollama_options(options: dict[str, str] | None) -> dict[str, Any]: """Convert Ollama options from string values to their correct types. @@ -204,15 +223,29 @@ def _convert_ollama_options(options: dict[str, str] | None) -> dict[str, Any]: else: target_type = annotation - # Convert based on the target type - if target_type is int: - converted[key] = int(value) - elif target_type is float: - converted[key] = float(value) - elif target_type is bool: - converted[key] = value.lower() == "true" - else: - # Keep as string for other types (e.g., Sequence[str]) + # Convert based on the target type. Values come from CRD strings, so + # coercion is tolerant: a malformed value is kept as a string and + # left for the Ollama SDK to report, rather than raising here and + # failing the whole agent build. + try: + if target_type is bool: + converted[key] = _coerce_option_bool(value) + elif target_type is int: + # Accept integral floats such as "64000.0" that JSON/YAML + # tooling can produce for an int-typed option. + converted[key] = int(float(value)) + elif target_type is float: + converted[key] = float(value) + else: + # Keep as string for other types (e.g., Sequence[str]) + converted[key] = value + except (TypeError, ValueError): + logger.warning( + "Could not convert Ollama option %r=%r to %s; forwarding the raw value.", + key, + value, + getattr(target_type, "__name__", target_type), + ) converted[key] = value else: # Unknown option - keep as string diff --git a/python/packages/kagent-adk/tests/unittests/models/test_ollama.py b/python/packages/kagent-adk/tests/unittests/models/test_ollama.py index f33815ed91..56066c7cad 100644 --- a/python/packages/kagent-adk/tests/unittests/models/test_ollama.py +++ b/python/packages/kagent-adk/tests/unittests/models/test_ollama.py @@ -231,3 +231,54 @@ def test_create_llm_from_ollama_model_config(self): # Options are type-coerced by _convert_ollama_options before reaching create_ollama_llm assert result.ollama_options["temperature"] == 0.8 assert result.ollama_options["top_p"] == 0.9 + + +class TestConvertOllamaOptions: + """Coercion of CRD string options into the Ollama SDK's typed values.""" + + def test_int_option_coerced(self): + from kagent.adk.types import _convert_ollama_options + + assert _convert_ollama_options({"num_ctx": "64000"}) == {"num_ctx": 64000} + + def test_int_option_accepts_integral_float_string(self): + """A value like "64000.0" (which JSON/YAML tooling can produce for an int + field) must not crash the whole agent build.""" + from kagent.adk.types import _convert_ollama_options + + assert _convert_ollama_options({"num_ctx": "64000.0"}) == {"num_ctx": 64000} + + def test_float_option_coerced(self): + from kagent.adk.types import _convert_ollama_options + + assert _convert_ollama_options({"temperature": "0.7"}) == {"temperature": 0.7} + + @pytest.mark.parametrize("value", ["1", "true", "True", "YES", "on", " true "]) + def test_bool_option_truthy_spellings(self, value): + from kagent.adk.types import _convert_ollama_options + + assert _convert_ollama_options({"low_vram": value}) == {"low_vram": True} + + @pytest.mark.parametrize("value", ["0", "false", "False", "no", "off"]) + def test_bool_option_falsy_spellings(self, value): + from kagent.adk.types import _convert_ollama_options + + assert _convert_ollama_options({"low_vram": value}) == {"low_vram": False} + + def test_unparseable_value_is_forwarded_not_raised(self): + """A malformed value is kept as a string for the SDK to report, rather + than raising and failing agent construction.""" + from kagent.adk.types import _convert_ollama_options + + assert _convert_ollama_options({"num_ctx": "not-a-number"}) == {"num_ctx": "not-a-number"} + + def test_unknown_option_kept_as_string(self): + from kagent.adk.types import _convert_ollama_options + + assert _convert_ollama_options({"custom_flag": "value"}) == {"custom_flag": "value"} + + def test_empty_and_none(self): + from kagent.adk.types import _convert_ollama_options + + assert _convert_ollama_options(None) == {} + assert _convert_ollama_options({}) == {} From 270594adb643c15ef23592eaa64fec182b57ef58 Mon Sep 17 00:00:00 2001 From: Piyush Mondal Date: Mon, 19 Oct 2026 15:22:47 +0000 Subject: [PATCH 2/2] fix(adk): reject non-integral and non-finite int options Address review: int(float(value)) truncated "2048.5" to 2048 and raised an uncaught OverflowError on "inf"/"1e400". Only accept a plain integer or an exactly-integral finite float; forward anything else with a warning. Signed-off-by: Piyush Mondal --- .../kagent-adk/src/kagent/adk/types.py | 27 ++++++++++++++++--- .../tests/unittests/models/test_ollama.py | 15 +++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/python/packages/kagent-adk/src/kagent/adk/types.py b/python/packages/kagent-adk/src/kagent/adk/types.py index 11e03af1d5..718a067090 100644 --- a/python/packages/kagent-adk/src/kagent/adk/types.py +++ b/python/packages/kagent-adk/src/kagent/adk/types.py @@ -1,5 +1,6 @@ import json import logging +import math from typing import Any, Callable, Optional, Union import httpx @@ -186,6 +187,26 @@ def _coerce_option_bool(value: str) -> bool: raise ValueError(f"invalid boolean option value: {value!r}") +def _coerce_option_int(value: str) -> int: + """Parse a CRD string into an int. + + Accepts a plain integer ("2048") and a float that is finite and exactly + integral ("2048.0"), which JSON/YAML tooling can produce for an int-typed + option. Rejects a non-integral float ("2048.5") rather than truncating it, + and rejects "inf"/"nan", so the caller falls back and warns instead of + silently losing precision or raising OverflowError. + """ + stripped = value.strip() + try: + return int(stripped) + except ValueError: + pass + number = float(stripped) + if not math.isfinite(number) or not number.is_integer(): + raise ValueError(f"not an integer option value: {value!r}") + return int(number) + + def _convert_ollama_options(options: dict[str, str] | None) -> dict[str, Any]: """Convert Ollama options from string values to their correct types. @@ -231,15 +252,13 @@ def _convert_ollama_options(options: dict[str, str] | None) -> dict[str, Any]: if target_type is bool: converted[key] = _coerce_option_bool(value) elif target_type is int: - # Accept integral floats such as "64000.0" that JSON/YAML - # tooling can produce for an int-typed option. - converted[key] = int(float(value)) + converted[key] = _coerce_option_int(value) elif target_type is float: converted[key] = float(value) else: # Keep as string for other types (e.g., Sequence[str]) converted[key] = value - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): logger.warning( "Could not convert Ollama option %r=%r to %s; forwarding the raw value.", key, diff --git a/python/packages/kagent-adk/tests/unittests/models/test_ollama.py b/python/packages/kagent-adk/tests/unittests/models/test_ollama.py index 56066c7cad..41ef4849df 100644 --- a/python/packages/kagent-adk/tests/unittests/models/test_ollama.py +++ b/python/packages/kagent-adk/tests/unittests/models/test_ollama.py @@ -272,6 +272,21 @@ def test_unparseable_value_is_forwarded_not_raised(self): assert _convert_ollama_options({"num_ctx": "not-a-number"}) == {"num_ctx": "not-a-number"} + def test_non_integral_float_is_not_truncated(self): + """A fractional value for an int option is a mistake; forward it rather + than silently truncating "2048.5" to 2048.""" + from kagent.adk.types import _convert_ollama_options + + assert _convert_ollama_options({"num_ctx": "2048.5"}) == {"num_ctx": "2048.5"} + + @pytest.mark.parametrize("value", ["inf", "-inf", "nan", "1e400"]) + def test_non_finite_int_value_is_forwarded(self, value): + """int(float("inf")) raises OverflowError, so these must fall back to the + raw value instead of crashing agent construction.""" + from kagent.adk.types import _convert_ollama_options + + assert _convert_ollama_options({"num_ctx": value}) == {"num_ctx": value} + def test_unknown_option_kept_as_string(self): from kagent.adk.types import _convert_ollama_options