|
51 | 51 | ] |
52 | 52 |
|
53 | 53 |
|
54 | | -def discovery_targets(extra_hosts=()): |
| 54 | +def discovery_targets(extra_hosts=(), extra_ports=()): |
55 | 55 | """(host, port, adapter types) triples to probe: localhost on every port, |
56 | 56 | then each extra host on the same port list. Localhost comes first so a |
57 | | - local engine always wins its own port over a remote one.""" |
| 57 | + local engine always wins its own port over a remote one. |
| 58 | +
|
| 59 | + `extra_ports` are non-default ports ([discovery] ports in speculum.toml). |
| 60 | + A port the table already knows keeps its ordered types; a new one is |
| 61 | + probed with every specific adapter, generic `openai` last.""" |
| 62 | + ports = list(DISCOVERY) |
| 63 | + known = {p for p, _types in DISCOVERY} |
| 64 | + for p in extra_ports or (): |
| 65 | + try: |
| 66 | + p = int(p) |
| 67 | + except (TypeError, ValueError): |
| 68 | + continue |
| 69 | + if p and p not in known: |
| 70 | + known.add(p) |
| 71 | + ports.append((p, _all_discovery_types())) |
58 | 72 | hosts = [] |
59 | 73 | for h in extra_hosts or (): |
60 | 74 | h = str(h).strip() |
61 | 75 | if h and h not in hosts and h not in LOCAL_HOSTS: |
62 | 76 | hosts.append(h) |
63 | | - out = [(LOCAL_HOST, port, types) for port, types in DISCOVERY] |
64 | | - out.extend((h, port, types) for h in hosts for port, types in DISCOVERY) |
| 77 | + out = [(LOCAL_HOST, port, types) for port, types in ports] |
| 78 | + out.extend((h, port, types) for h in hosts for port, types in ports) |
65 | 79 | return out |
66 | 80 |
|
67 | 81 |
|
| 82 | +def _all_discovery_types(): |
| 83 | + """Every discoverable adapter, each once, generic `openai` last. Used for |
| 84 | + ports [discovery] ports adds that the built-in table does not know.""" |
| 85 | + out = [] |
| 86 | + for _port, types in DISCOVERY: |
| 87 | + for t in types: |
| 88 | + if t not in out: |
| 89 | + out.append(t) |
| 90 | + return tuple([t for t in out if t != "openai"] + ["openai"]) |
| 91 | + |
| 92 | + |
68 | 93 | def parse_prom(text): |
69 | 94 | """Tiny Prometheus text parser: sum the samples of each metric name, |
70 | 95 | ignore labels. NaN / non-numeric values are skipped.""" |
@@ -984,16 +1009,42 @@ def poll(self): |
984 | 1009 | "unsloth": "Unsloth", "openai": "OpenAI"} |
985 | 1010 |
|
986 | 1011 |
|
| 1012 | +def engine_url(eng, cls=None, override_port=None): |
| 1013 | + """Endpoint for an [[engine]] table: an explicit `url`, or the `host` / |
| 1014 | + `port` shorthand (host defaults to 127.0.0.1, port to the adapter's own |
| 1015 | + default). `override_port` re-points a URL at another port without dropping |
| 1016 | + its host or path (discovery).""" |
| 1017 | + url = eng.get("url") or "" |
| 1018 | + host = eng.get("host") or None |
| 1019 | + port = eng.get("port") |
| 1020 | + default_port = getattr(cls, "default_port", None) if cls else None |
| 1021 | + |
| 1022 | + def netloc(u, h, p): |
| 1023 | + parts = urllib.request.urlparse(u) |
| 1024 | + return "%s:%s" % (h or parts.hostname or "127.0.0.1", p) |
| 1025 | + |
| 1026 | + if override_port is not None: |
| 1027 | + port = override_port |
| 1028 | + if port is not None: |
| 1029 | + url = (urllib.request.urlparse(url)._replace( |
| 1030 | + netloc=netloc(url, host, port)).geturl() |
| 1031 | + if url else "http://%s:%s" % (host or "127.0.0.1", port)) |
| 1032 | + elif host: |
| 1033 | + if url: |
| 1034 | + p = urllib.request.urlparse(url) |
| 1035 | + url = p._replace( |
| 1036 | + netloc=netloc(url, host, p.port or default_port)).geturl() |
| 1037 | + elif default_port: |
| 1038 | + url = "http://%s:%s" % (host, default_port) |
| 1039 | + return url |
| 1040 | + |
| 1041 | + |
987 | 1042 | def make_adapter(eng, port=None): |
988 | 1043 | """Build an adapter from a config [[engine]] table (a plain dict of |
989 | 1044 | type/url/...); `port` overrides the table's url port (discovery).""" |
990 | 1045 | etype = eng.get("type") or "custom" |
991 | 1046 | cls = ADAPTERS.get(etype, CustomAdapter) |
992 | | - url = eng.get("url") or "" |
993 | | - if port is not None and url: |
994 | | - p = urllib.request.urlparse(url) |
995 | | - url = p._replace(netloc=(p.hostname or "127.0.0.1") + ":" + str(port) |
996 | | - ).geturl() |
| 1047 | + url = engine_url(eng, cls, override_port=port) |
997 | 1048 | a = cls(url=url or None, key=eng.get("name"), label=eng.get("name"), |
998 | 1049 | parent=eng.get("parent"), api_key_env=eng.get("api_key_env")) |
999 | 1050 | a.optional = bool(eng.get("optional")) # down is normal: no alert (speculum.toml) |
@@ -1022,10 +1073,11 @@ class Scheduler: |
1022 | 1073 |
|
1023 | 1074 | def __init__(self, adapters, discovery_enabled=True, claimed_ports=None, |
1024 | 1075 | on_result=None, on_event=None, on_discover=None, |
1025 | | - discovery_hosts=None): |
| 1076 | + discovery_hosts=None, discovery_ports=None): |
1026 | 1077 | self.discovery_enabled = discovery_enabled |
1027 | 1078 | self.claimed_ports = claimed_ports |
1028 | 1079 | self.discovery_hosts = list(discovery_hosts or ()) |
| 1080 | + self.discovery_ports = list(discovery_ports or ()) |
1029 | 1081 | self.on_result = on_result |
1030 | 1082 | self.on_event = on_event |
1031 | 1083 | self.on_discover = on_discover |
@@ -1072,7 +1124,8 @@ def _claimed(self): |
1072 | 1124 | def discovery(self): |
1073 | 1125 | claimed = self._claimed() |
1074 | 1126 | found = [] |
1075 | | - for host, port, types in discovery_targets(self.discovery_hosts): |
| 1127 | + for host, port, types in discovery_targets(self.discovery_hosts, |
| 1128 | + self.discovery_ports): |
1076 | 1129 | if (host, port) in claimed: |
1077 | 1130 | continue |
1078 | 1131 | base = "http://%s:%d" % (host, port) |
|
0 commit comments