1616import importlib
1717import inspect
1818import os
19+ import re
20+ from pathlib import Path
1921from typing import TYPE_CHECKING
2022
21- from naas_abi_core .models .Model import Model , ModelDefinition
23+ from naas_abi_core .models .Model import CanonicalModelId , Model , ModelDefinition
2224from naas_abi_core .module .ModuleUtils import find_class_module_root_path
2325from naas_abi_core .utils .Logger import logger
2426
2527if TYPE_CHECKING :
2628 from naas_abi_core .services .model_registry .ModelRegistryPort import IModelRegistry
2729
2830
31+ # Matches ``CANONICAL_ID = CanonicalModelId.FOO`` /
32+ # ``CANONICAL_ID = CanonicalModelId["FOO"]`` / ``CANONICAL_ID = "foo"`` so a
33+ # model file's canonical id can be resolved *without importing it* (importing a
34+ # model file constructs its provider client, which can be slow).
35+ _CANONICAL_ID_ASSIGN_RE = re .compile (r"CANONICAL_ID\s*=" )
36+ _CANONICAL_ID_VALUE_RE = re .compile (
37+ r"CANONICAL_ID\s*=\s*(?:"
38+ r"CanonicalModelId\.([A-Za-z0-9_]+)" # enum-member access
39+ r"|CanonicalModelId\[\s*[\"']([^\"']+)[\"']\s*\]" # enum-subscript by name
40+ r"|[\"']([^\"']+)[\"']" # bare string literal
41+ r")"
42+ )
43+
44+
45+ def _static_canonical_ids (source : str ) -> tuple [set [str ], bool ]:
46+ """Best-effort extraction of the canonical-id strings declared via
47+ ``CANONICAL_ID = ...`` in a model source file, without importing it.
48+
49+ Returns ``(canonical_ids, fully_resolved)``. ``fully_resolved`` is ``False``
50+ when at least one ``CANONICAL_ID`` assignment could not be resolved to a
51+ concrete string — callers must not skip a file that isn't fully resolved.
52+ """
53+ ids : set [str ] = set ()
54+ resolved = 0
55+ for member , subscript , literal in _CANONICAL_ID_VALUE_RE .findall (source ):
56+ if member :
57+ try :
58+ ids .add (CanonicalModelId [member ].value )
59+ resolved += 1
60+ except KeyError :
61+ # Unknown member name — leave unresolved so we import to be safe.
62+ pass
63+ elif subscript :
64+ ids .add (subscript )
65+ resolved += 1
66+ elif literal :
67+ ids .add (literal )
68+ resolved += 1
69+ total = len (_CANONICAL_ID_ASSIGN_RE .findall (source ))
70+ return ids , resolved == total
71+
72+
2973class ModuleModelLoader :
3074 @classmethod
31- def load_models (cls , class_ : type , registry : "IModelRegistry" ) -> int :
75+ def load_models (
76+ cls ,
77+ class_ : type ,
78+ registry : "IModelRegistry" ,
79+ include_models : list [str ] | None = None ,
80+ ) -> int :
3281 """Recursively walk ``<module_root>/models/`` and register every
3382 ``ModelDefinition`` subclass found, including those in provider
3483 subdirectories (e.g. ``models/openai/``, ``models/anthropic/``).
84+
85+ When ``include_models`` is provided, only models whose ``CANONICAL_ID``
86+ is in that list are registered; every other model file is skipped
87+ *without importing it* — importing a model file constructs its provider
88+ client (e.g. a Bedrock client), which can add seconds per file. Files
89+ whose canonical id cannot be resolved statically are imported anyway, so
90+ the filter never drops a model it can't positively identify.
91+
3592 Returns the count of successful registrations."""
3693 module_root_path = find_class_module_root_path (class_ )
3794 models_path = module_root_path / "models"
@@ -40,6 +97,7 @@ def load_models(cls, class_: type, registry: "IModelRegistry") -> int:
4097 return 0
4198
4299 logger .debug (f"Loading models from { models_path } " )
100+ include_set = set (include_models ) if include_models is not None else None
43101 top_package = class_ .__module__ .split ("." )[0 ]
44102 registered = 0
45103
@@ -64,6 +122,25 @@ def load_models(cls, class_: type, registry: "IModelRegistry") -> int:
64122 f"{ class_ .__module__ } .models.{ subpackage } .{ file [:- 3 ]} "
65123 )
66124
125+ # Honour include_models without paying the import cost: if the
126+ # file's canonical id(s) can be resolved statically and none are
127+ # in the include set, skip before importing.
128+ if include_set is not None :
129+ try :
130+ source = (Path (dirpath ) / file ).read_text (encoding = "utf-8" )
131+ except OSError :
132+ source = None
133+ if source is not None :
134+ ids , fully_resolved = _static_canonical_ids (source )
135+ if fully_resolved and ids and ids .isdisjoint (include_set ):
136+ logger .debug (
137+ "ModuleModelLoader: skipping %s — canonical id(s) "
138+ "%s not in include_models." ,
139+ model_module_path ,
140+ sorted (ids ),
141+ )
142+ continue
143+
67144 try :
68145 model_module = importlib .import_module (model_module_path )
69146 except Exception as exc :
0 commit comments