Skip to content

Commit 548910b

Browse files
committed
Resolve capability-table path through Galaxy's config system
The original loader walked CWD-relative paths and an `__file__`-derived sample path -- both fragile under Galaxy's various install shapes (per John's review on #22609: package installs flatten the tree, CWD isn't always the Galaxy root, and the existing config-resolution machinery is the standard tool for this). Introduce ``agent_model_capabilities_file`` as a first-class config option with ``path_resolves_to: config_dir`` and add it to ``add_sample_file_to_defaults`` so Galaxy's own config loader resolves it the same way it does ``tool_data_table_config_path`` and friends: admin override in ``config_dir`` if present, otherwise the shipped sample under ``sample_config_dir``. The agents code just reads ``config.agent_model_capabilities_file`` and feeds the result into ``_load_model_capabilities()``, which is now per-path-keyed in its cache and falls back to the built-in defaults when the path is missing, unset, or unparseable. The schema description for ``inference_services`` no longer duplicates the capability-table prose -- it points at the new option instead. ``galaxy.yml.sample`` was regenerated via ``make config-rebuild``.
1 parent 1799338 commit 548910b

5 files changed

Lines changed: 89 additions & 70 deletions

File tree

lib/galaxy/agents/base.py

Lines changed: 39 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -108,57 +108,45 @@
108108
"default": {"structured_output": True},
109109
}
110110

111-
# Search order for the capability table. First hit wins.
112-
_MODEL_CAPABILITIES_BASENAME = "agent_model_capabilities.yml"
113-
_MODEL_CAPABILITIES_SEARCH_PATHS = (
114-
# Admin-edited copy in the runtime config directory.
115-
os.path.join("config", _MODEL_CAPABILITIES_BASENAME),
116-
# The shipped sample (what most installs will end up reading).
117-
os.path.join(
118-
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
119-
"config",
120-
"sample",
121-
_MODEL_CAPABILITIES_BASENAME + ".sample",
122-
),
123-
os.path.join("config", _MODEL_CAPABILITIES_BASENAME + ".sample"),
124-
)
125-
126-
_model_capabilities_cache: Optional[dict[str, Any]] = None
111+
_model_capabilities_cache: dict[str, dict[str, Any]] = {}
127112

128113

129-
def _load_model_capabilities(force_reload: bool = False) -> dict[str, Any]:
130-
"""Return the parsed model-capabilities table, loading it on first use.
114+
def _load_model_capabilities(path: Optional[str], force_reload: bool = False) -> dict[str, Any]:
115+
"""Return the parsed model-capabilities table for ``path``.
131116
132-
Looks at a small set of standard locations and parses the first one that
133-
exists. Falls back to a sane hardcoded default if nothing is found or the
134-
file fails to parse -- we'd rather keep agents working than block on a
135-
missing config file.
117+
``path`` should come from ``config.agent_model_capabilities_file``, which
118+
Galaxy resolves at startup (admin override in ``config_dir`` if present,
119+
otherwise the shipped sample under ``sample_config_dir``). Falls back to a
120+
sane hardcoded default when the input is missing, not a string, or points
121+
at a file we can't parse -- we'd rather keep agents working than block on
122+
a misconfigured deployment.
136123
"""
137-
global _model_capabilities_cache
138-
if _model_capabilities_cache is not None and not force_reload:
139-
return _model_capabilities_cache
124+
if not isinstance(path, str) or not path:
125+
return _DEFAULT_MODEL_CAPABILITIES
140126

141-
for path in _MODEL_CAPABILITIES_SEARCH_PATHS:
142-
if not os.path.exists(path):
143-
continue
144-
try:
145-
with open(path) as fh:
146-
parsed = yaml.safe_load(fh) or {}
147-
except (OSError, yaml.YAMLError) as exc:
148-
log.warning("Could not parse model capabilities at %s: %s", path, exc)
149-
continue
150-
if not isinstance(parsed, dict):
151-
log.warning("Ignoring model capabilities at %s: not a mapping", path)
152-
continue
153-
_model_capabilities_cache = parsed
154-
return _model_capabilities_cache
127+
if not force_reload and path in _model_capabilities_cache:
128+
return _model_capabilities_cache[path]
155129

156-
log.warning(
157-
"Model capabilities file not found (looked in %s); falling back to built-in defaults.",
158-
", ".join(_MODEL_CAPABILITIES_SEARCH_PATHS),
159-
)
160-
_model_capabilities_cache = _DEFAULT_MODEL_CAPABILITIES
161-
return _model_capabilities_cache
130+
if not os.path.exists(path):
131+
log.warning("Model capabilities file not found at %s; using built-in defaults.", path)
132+
_model_capabilities_cache[path] = _DEFAULT_MODEL_CAPABILITIES
133+
return _DEFAULT_MODEL_CAPABILITIES
134+
135+
try:
136+
with open(path) as fh:
137+
parsed = yaml.safe_load(fh) or {}
138+
except (OSError, yaml.YAMLError) as exc:
139+
log.warning("Could not parse model capabilities at %s: %s; using built-in defaults.", path, exc)
140+
_model_capabilities_cache[path] = _DEFAULT_MODEL_CAPABILITIES
141+
return _DEFAULT_MODEL_CAPABILITIES
142+
143+
if not isinstance(parsed, dict):
144+
log.warning("Ignoring model capabilities at %s: not a mapping; using built-in defaults.", path)
145+
_model_capabilities_cache[path] = _DEFAULT_MODEL_CAPABILITIES
146+
return _DEFAULT_MODEL_CAPABILITIES
147+
148+
_model_capabilities_cache[path] = parsed
149+
return parsed
162150

163151

164152
def _capability_for_model(model_name: str, capability: str, table: dict[str, Any]) -> Optional[bool]:
@@ -693,15 +681,18 @@ def _supports_structured_output(self) -> bool:
693681
Resolution order:
694682
1. Agent-specific ``structured_output_override`` in inference_services
695683
2. Global ``default.structured_output_override`` in inference_services
696-
3. Glob match against the admin capability table, or shipped sample
697-
4. The selected table's ``default`` block (true if absent)
684+
3. Glob match in the capability table at ``config.agent_model_capabilities_file``
685+
(Galaxy resolves this to the admin override in ``config_dir`` if present,
686+
otherwise the shipped sample under ``sample_config_dir``)
687+
4. The capability table's ``default`` block (true if absent)
698688
"""
699689
override = self._get_agent_config("structured_output_override")
700690
if override is not None:
701691
return bool(override)
702692

703693
model_name = self._get_agent_config("model", "")
704-
capability = _capability_for_model(model_name, "structured_output", _load_model_capabilities())
694+
capabilities_path = getattr(self.deps.config, "agent_model_capabilities_file", None)
695+
capability = _capability_for_model(model_name, "structured_output", _load_model_capabilities(capabilities_path))
705696
if capability is None:
706697
return True
707698
return capability

lib/galaxy/config/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,7 @@ class GalaxyAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin):
725725
}
726726

727727
add_sample_file_to_defaults = {
728+
"agent_model_capabilities_file",
728729
"build_sites_config_file",
729730
"datatypes_config_file",
730731
"tool_data_table_config_path",

lib/galaxy/config/sample/galaxy.yml.sample

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3040,14 +3040,23 @@ galaxy:
30403040
# path to replace all LLM calls with deterministic responses for
30413041
# testing: inference_services: { static_responses:
30423042
# test/integration/static_agents.yml } Per-agent or default-block
3043-
# ``structured_output_override: true|false`` beats the model capability
3044-
# table. Admins can place overrides in config/agent_model_capabilities.yml;
3045-
# otherwise Galaxy reads the shipped sample from
3046-
# lib/galaxy/config/sample/agent_model_capabilities.yml.sample. The table is
3047-
# consulted to decide whether the configured model can produce tool-calling /
3048-
# JSON-mode output.
3043+
# ``structured_output_override: true|false`` beats the model
3044+
# capability table -- see ``agent_model_capabilities_file`` for the
3045+
# table's location and contents.
30493046
#inference_services: null
30503047

3048+
# YAML file with capability hints for agent inference models. Maps
3049+
# fnmatch-style globs against model names to features such as
3050+
# structured-output (tool-calling / JSON-mode) support. Galaxy ships a
3051+
# sample populated with common model families; admins can drop a file
3052+
# named ``agent_model_capabilities.yml`` in ``config_dir`` to override
3053+
# the shipped table for private models. ``inference_services``
3054+
# ``structured_output_override`` overrides this table for a specific
3055+
# agent or default block.
3056+
# The value of this option will be resolved with respect to
3057+
# <config_dir>.
3058+
#agent_model_capabilities_file: agent_model_capabilities.yml
3059+
30513060
# Allow the display of tool recommendations in workflow editor and
30523061
# after tool execution. If it is enabled and set to true, please
30533062
# enable 'tool_recommendation_model_path' as well

lib/galaxy/config/schemas/config_schema.yml

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4148,11 +4148,23 @@ mapping:
41484148
deterministic responses for testing:
41494149
inference_services: { static_responses: test/integration/static_agents.yml }
41504150
Per-agent or default-block ``structured_output_override: true|false``
4151-
beats the model capability table. Admins can place overrides in
4152-
config/agent_model_capabilities.yml; otherwise Galaxy reads the
4153-
shipped sample from lib/galaxy/config/sample/agent_model_capabilities.yml.sample.
4154-
The table is consulted to decide whether the configured model can
4155-
produce tool-calling / JSON-mode output.
4151+
beats the model capability table -- see ``agent_model_capabilities_file``
4152+
for the table's location and contents.
4153+
4154+
agent_model_capabilities_file:
4155+
type: str
4156+
default: agent_model_capabilities.yml
4157+
path_resolves_to: config_dir
4158+
required: false
4159+
desc: |
4160+
YAML file with capability hints for agent inference models. Maps
4161+
fnmatch-style globs against model names to features such as
4162+
structured-output (tool-calling / JSON-mode) support. Galaxy ships a
4163+
sample populated with common model families; admins can drop a file
4164+
named ``agent_model_capabilities.yml`` in ``config_dir`` to override
4165+
the shipped table for private models. ``inference_services``
4166+
``structured_output_override`` overrides this table for a specific
4167+
agent or default block.
41564168
41574169
enable_tool_recommendations:
41584170
type: bool

test/unit/app/test_agents.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ def setup_method(self):
7676
self.mock_config.ai_api_key = "test-key"
7777
self.mock_config.ai_model = "llama-4-scout"
7878
self.mock_config.ai_api_base_url = "http://localhost:4000/v1/"
79+
# Point at the shipped capability sample so _supports_structured_output
80+
# exercises the real table rather than the built-in fallback.
81+
self.mock_config.agent_model_capabilities_file = os.path.join(
82+
os.path.dirname(agents_base.__file__),
83+
"..",
84+
"config",
85+
"sample",
86+
"agent_model_capabilities.yml.sample",
87+
)
7988

8089
self.mock_user = mock.Mock()
8190
self.mock_user.id = 1
@@ -870,7 +879,7 @@ def test_supports_structured_output_falls_back_to_default(self):
870879

871880
def test_capability_table_glob_matching(self):
872881
"""Globs should match wildcard suffixes (e.g. gpt-4-turbo)."""
873-
table = _load_model_capabilities()
882+
table = _load_model_capabilities(self.mock_config.agent_model_capabilities_file)
874883
assert _capability_for_model("gpt-4-turbo", "structured_output", table) is True
875884
assert _capability_for_model("gpt-4o-mini", "structured_output", table) is True
876885
assert _capability_for_model("claude-3-5-sonnet", "structured_output", table) is True
@@ -881,20 +890,17 @@ def test_capability_table_glob_matching(self):
881890
assert _capability_for_model("deepseek-r1", "structured_output", table) is False
882891
assert _capability_for_model("deepseek-v3", "structured_output", table) is False
883892

884-
def test_capability_table_loads_with_missing_file(self, monkeypatch):
885-
"""Force every search path to miss; loader should warn and use defaults."""
886-
monkeypatch.setattr(agents_base, "_MODEL_CAPABILITIES_SEARCH_PATHS", ())
887-
monkeypatch.setattr(agents_base, "_model_capabilities_cache", None)
888-
889-
table = agents_base._load_model_capabilities(force_reload=True)
890-
891-
# Should be the hardcoded fallback.
893+
def test_capability_table_falls_back_when_file_is_missing(self):
894+
"""Pointing at a non-existent path should fall back to the built-in defaults."""
895+
table = _load_model_capabilities("/nonexistent/path/agent_model_capabilities.yml", force_reload=True)
892896
assert table is agents_base._DEFAULT_MODEL_CAPABILITIES
893897
assert _capability_for_model("deepseek-r1", "structured_output", table) is False
894898
assert _capability_for_model("gpt-4o", "structured_output", table) is True
895899

896-
# Reset cache so subsequent tests pick up the real file again.
897-
monkeypatch.setattr(agents_base, "_model_capabilities_cache", None)
900+
def test_capability_table_falls_back_when_path_is_unset(self):
901+
"""A None or non-string path (e.g. unset config option) yields the built-in defaults."""
902+
assert _load_model_capabilities(None) is agents_base._DEFAULT_MODEL_CAPABILITIES
903+
assert _load_model_capabilities("") is agents_base._DEFAULT_MODEL_CAPABILITIES
898904

899905

900906
@pytestmark_live_llm

0 commit comments

Comments
 (0)