Coverage for orchestrant/benchmark/gateway.py: 95%

133 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-18 00:48 +0000

1"""Reports taken THROUGH the llm-stack gateway: which lane served, on what.""" 

2 

3import collections 

4import hashlib 

5import json 

6import threading 

7import urllib.parse 

8import urllib.request 

9 

10 

11# What /gateway/info may put in a report: the image reference and shas. 

12INFO_KEYS = ( 

13 "image", 

14 "config_sha256", 

15 "restart_sha256", 

16 "registry_sha256", 

17 "boot_config_sha256", 

18 "lua_sha256", 

19 "prompts_sha256", 

20) 

21 

22# Process-wide: a report is written at run end from what its requests were told. 

23_SERVED = collections.Counter() 

24# Counter += is a read then a write: concurrent replies would lose counts, more so free-threaded. 

25_SERVED_LOCK = threading.Lock() 

26 

27 

28def _origin(url): 

29 """host:port of `url`, localhost spelled 127.0.0.1; None without both.""" 

30 url = url or "" 

31 try: 

32 parts = urllib.parse.urlsplit(url if "://" in url else f"http://{url}") 

33 port = parts.port 

34 except ValueError: 

35 return None 

36 host = "127.0.0.1" if parts.hostname == "localhost" else parts.hostname 

37 return f"{host}:{port}" if host and port else None 

38 

39 

40def registry_sha256(doc): 

41 """The registry's sha256 as the gateway's renderer computes it: canonical JSON.""" 

42 text = json.dumps(doc, sort_keys=True, separators=(",", ":"), ensure_ascii=True) 

43 return hashlib.sha256(text.encode("ascii")).hexdigest() 

44 

45 

46def _registry(path=None): 

47 """(path, document) of the registry the lab resolves backends from.""" 

48 from orchestrant.benchmark import openai_api # lazy: it imports provenance 

49 

50 path = path or openai_api.BACKENDS_FILE 

51 try: 

52 with open(path, encoding="utf-8") as f: 

53 doc = json.load(f) 

54 except (OSError, ValueError): 

55 return path, {} 

56 return path, doc if isinstance(doc, dict) else {} 

57 

58 

59def _lane_names(serving, alias): 

60 """[(role, lane)] an alias reaches: a route's lane and overflow, or raw-<lane>.""" 

61 lanes = serving.get("lanes") or {} 

62 route = (serving.get("routes") or {}).get(alias) 

63 if isinstance(route, dict): 

64 pairs = ( 

65 ("primary", route.get("lane")), 

66 ("overflow", route.get("overflow_lane")), 

67 ) 

68 return [(role, lane) for role, lane in pairs if lane in lanes] 

69 raw = alias[4:] if isinstance(alias, str) and alias.startswith("raw-") else None 

70 offered = serving["gateway"].get("raw_routes") is True 

71 return [("primary", raw)] if offered and raw in lanes else [] 

72 

73 

74def _lane_rows(serving, backends, alias, gateway): 

75 """One row per lane an alias reaches, never one pointing back at the gateway.""" 

76 rows = [] 

77 for role, name in _lane_names(serving, alias): 

78 backend = serving["lanes"][name].get("backend") 

79 entry = backends.get(backend) or {} 

80 url = entry.get("base_url") 

81 url = url.rstrip("/") if isinstance(url, str) else None 

82 # The renderer refuses such a lane; resolving one would recurse. 

83 if url and _origin(url) != gateway: 

84 rows.append( 

85 { 

86 "role": role, 

87 "lane": name, 

88 "backend": backend, 

89 "base_url": url, 

90 "model": entry.get("model"), 

91 } 

92 ) 

93 return rows 

94 

95 

96def route(base_url, model, path=None): 

97 """The lanes a gateway alias reaches, or None when base_url is not the gateway.""" 

98 path, doc = _registry(path) 

99 serving = doc.get("serving") 

100 gw = serving.get("gateway") if isinstance(serving, dict) else None 

101 listen = gw.get("listen") if isinstance(gw, dict) else None 

102 gateway = _origin(listen) if isinstance(listen, str) else None 

103 if gateway is None or _origin(base_url) != gateway: 

104 return None 

105 try: 

106 lanes = _lane_rows(serving, doc.get("backends") or {}, model, gateway) 

107 error = None if lanes else f"the serving block routes no alias {model!r}" 

108 except (AttributeError, TypeError, KeyError) as e: # a hand-edited block 

109 lanes, error = [], f"unreadable serving block: {type(e).__name__}: {e}"[:200] 

110 return { 

111 "listen": listen, 

112 "alias": model, 

113 "lanes": lanes, 

114 "registry": path, 

115 "registry_sha256": registry_sha256(doc), 

116 "error": error, 

117 } 

118 

119 

120def lane_behind(base_url, model, path=None): 

121 """(base_url, model) of the lane the gateway sends `model` to first, or None.""" 

122 lanes = (route(base_url, model, path) or {}).get("lanes") or [] 

123 lane = next((r for r in lanes if r["role"] == "primary"), None) 

124 return (lane["base_url"], lane["model"]) if lane else None 

125 

126 

127def _header(headers, name): 

128 try: 

129 value = headers.get(name) if headers is not None else None 

130 except Exception: # a header object of another shape is no evidence 

131 return None 

132 return value if isinstance(value, str) else None 

133 

134 

135def note_reply(url, body, headers): 

136 """Count one reply by the lane the gateway names in its X-Gw-Lane header.""" 

137 lane = _header(headers, "X-Gw-Lane") 

138 if lane is None: 

139 return 

140 alias = body.get("model") if isinstance(body, dict) else None 

141 alias = alias if isinstance(alias, str) else None 

142 key = (_origin(url), alias, lane, _header(headers, "X-Gw-Rerouted")) 

143 with _SERVED_LOCK: 

144 _SERVED[key] += 1 

145 

146 

147def served(base_url, model=None): 

148 """Every reply this process got from the gateway at base_url, by alias, lane, reroute.""" 

149 origin = _origin(base_url) 

150 with _SERVED_LOCK: 

151 tally = sorted(_SERVED.items(), key=str) 

152 return [ 

153 {"alias": alias, "lane": lane, "rerouted": rerouted, "replies": n} 

154 for (where, alias, lane, rerouted), n in tally 

155 if where == origin and (model is None or alias == model) 

156 ] 

157 

158 

159def info(base_url, timeout=3): 

160 """The running gateway's /gateway/info, cut to INFO_KEYS, or {"error": ...}.""" 

161 url = f"{base_url.rstrip('/')}/gateway/info" 

162 try: 

163 with urllib.request.urlopen(url, timeout=timeout) as r: # nosec B310 

164 doc = json.load(r) 

165 except Exception as e: # a gateway that is down is a gap, not a crash 

166 return {"error": f"{type(e).__name__}: {e}"[:160]} 

167 if not isinstance(doc, dict): 

168 return {"error": "/gateway/info is not a JSON object"} 

169 return {k: doc[k] for k in INFO_KEYS if k in doc} 

170 

171 

172def gateway_block(base_url, model, runtime_of, path=None): 

173 """provenance["gateway"]: None for a direct lane; for the gateway, what served.""" 

174 found = route(base_url, model, path) 

175 if found is None: 

176 return None 

177 for lane in found["lanes"]: 

178 if lane["role"] != "primary": 

179 lane["runtime"] = runtime_of(lane["base_url"], lane["model"]) 

180 live = info(base_url) 

181 rendered_from = live.get("registry_sha256") 

182 found["info"] = live 

183 found["registry_matches"] = ( 

184 None if rendered_from is None else rendered_from == found["registry_sha256"] 

185 ) 

186 found["served"] = served(base_url, model) 

187 return found 

188 

189 

190_GATEWAY_SHAS = ( 

191 ("restart_sha256", "image, boot config or Lua"), 

192 ("config_sha256", "routes"), 

193) 

194 

195 

196def _lanes_served(block): 

197 rows = [r for r in block.get("served") or [] if isinstance(r, dict)] 

198 return sorted({f"{r.get('lane')} ({r.get('rerouted')})" for r in rows}) 

199 

200 

201def gateway_notes(old, new): 

202 """For compare(): one run through the gateway and one not, or two gateways apart.""" 

203 before, after = old.get("gateway"), new.get("gateway") 

204 if not (before or after): 

205 return [] 

206 if not (before and after): 

207 side, block = ("old", before) if before else ("new", after) 

208 return [ 

209 f"only the {side} run went through the gateway (alias " 

210 f"{block.get('alias')!r}): it re-encodes every body, and an alias " 

211 f"that is not raw-* also shapes it (tools prompt, T=0 on GGUF lanes, " 

212 f"power_mode dropped); its /v1/models lists aliases, not models" 

213 ] 

214 notes = [] 

215 aliases = before.get("alias"), after.get("alias") 

216 if aliases[0] != aliases[1]: 

217 notes.append(f"gateway alias differs: {aliases[0]!r} vs {aliases[1]!r}") 

218 for key, what in _GATEWAY_SHAS: 

219 was, now = ((b.get("info") or {}).get(key) for b in (before, after)) 

220 if was and now and was != now: 

221 notes.append(f"the gateway's {what} changed ({key} {was[:12]}→{now[:12]})") 

222 lanes = [_lanes_served(b) for b in (before, after)] 

223 if lanes[0] != lanes[1]: 

224 notes.append(f"other lanes served the two runs: {lanes[0]} vs {lanes[1]}") 

225 return notes