|
108 | 108 | "default": {"structured_output": True}, |
109 | 109 | } |
110 | 110 |
|
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]] = {} |
127 | 112 |
|
128 | 113 |
|
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``. |
131 | 116 |
|
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. |
136 | 123 | """ |
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 |
140 | 126 |
|
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] |
155 | 129 |
|
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 |
162 | 150 |
|
163 | 151 |
|
164 | 152 | 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: |
693 | 681 | Resolution order: |
694 | 682 | 1. Agent-specific ``structured_output_override`` in inference_services |
695 | 683 | 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) |
698 | 688 | """ |
699 | 689 | override = self._get_agent_config("structured_output_override") |
700 | 690 | if override is not None: |
701 | 691 | return bool(override) |
702 | 692 |
|
703 | 693 | 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)) |
705 | 696 | if capability is None: |
706 | 697 | return True |
707 | 698 | return capability |
|
0 commit comments