mekosotto commited on
Commit
4d00c18
·
1 Parent(s): 1761dcd

test(llm): pin 401 short-circuit + 400 try-next-model behavior (red)

Browse files
Files changed (1) hide show
  1. tests/llm/test_explainer.py +122 -0
tests/llm/test_explainer.py CHANGED
@@ -128,3 +128,125 @@ class TestModalityDispatch:
128
  # Should not raise; should produce a non-empty rationale
129
  assert result["source"] == "template"
130
  assert result["rationale"], "rationale must be non-empty"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  # Should not raise; should produce a non-empty rationale
129
  assert result["source"] == "template"
130
  assert result["rationale"], "rationale must be non-empty"
131
+
132
+
133
+ class TestAuthFailureShortCircuits:
134
+ """A 401 from OpenRouter means the key is unauthorized — every model
135
+ in the chain will fail the same way, so we must short-circuit instead
136
+ of burning the full chain on every request."""
137
+
138
+ def test_401_short_circuits_to_template_after_one_attempt(self, monkeypatch):
139
+ from src.llm import explainer as ex
140
+ from openai import APIStatusError
141
+ import httpx
142
+
143
+ monkeypatch.delenv("NEUROBRIDGE_DISABLE_LLM", raising=False)
144
+ monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-v1-deliberately-bad")
145
+
146
+ attempts: list[str] = []
147
+
148
+ def _raise_401(**kwargs):
149
+ attempts.append(kwargs["model"])
150
+ req = httpx.Request("POST", "https://openrouter.ai/api/v1/chat/completions")
151
+ resp = httpx.Response(status_code=401, request=req)
152
+ raise APIStatusError(message="No auth credentials found", response=resp, body={})
153
+
154
+ class _StubCompletions:
155
+ create = staticmethod(_raise_401)
156
+
157
+ class _StubChat:
158
+ completions = _StubCompletions()
159
+
160
+ class _StubClient:
161
+ chat = _StubChat()
162
+ def __init__(self, **kwargs):
163
+ pass
164
+
165
+ # Must patch on the `openai` module — the explainer does
166
+ # `from openai import OpenAI` *inside* the function (see
167
+ # src/llm/explainer.py:269-275), so any module-level attribute
168
+ # on `src.llm.explainer` would be a no-op.
169
+ monkeypatch.setattr("openai.OpenAI", _StubClient)
170
+
171
+ out = ex._llm_explain(_payload(), modality="bbb")
172
+
173
+ assert out is None, "401 must surface as a None return (caller falls back to template)"
174
+ assert len(attempts) == 1, f"401 must short-circuit; tried {len(attempts)} models: {attempts}"
175
+
176
+ def test_explain_returns_template_source_on_401(self, monkeypatch):
177
+ from src.llm import explainer as ex
178
+ from openai import APIStatusError
179
+ import httpx
180
+
181
+ monkeypatch.delenv("NEUROBRIDGE_DISABLE_LLM", raising=False)
182
+ monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-v1-deliberately-bad")
183
+
184
+ def _raise_401(**kwargs):
185
+ req = httpx.Request("POST", "https://openrouter.ai/api/v1/chat/completions")
186
+ raise APIStatusError(
187
+ message="auth",
188
+ response=httpx.Response(401, request=req),
189
+ body={},
190
+ )
191
+
192
+ class _Comp:
193
+ create = staticmethod(_raise_401)
194
+
195
+ class _Chat:
196
+ completions = _Comp()
197
+
198
+ class _Client:
199
+ chat = _Chat()
200
+ def __init__(self, **kwargs):
201
+ pass
202
+
203
+ monkeypatch.setattr("openai.OpenAI", _Client)
204
+
205
+ result = ex.explain(_payload(), modality="bbb")
206
+
207
+ assert result["source"] == "template"
208
+ assert result["model"] is None
209
+ assert result["rationale"], "rationale must never be empty"
210
+
211
+ def test_400_advances_to_next_model_instead_of_short_circuiting(self, monkeypatch):
212
+ """A 400 from one model is a prompt-shape mismatch with THAT model
213
+ (some models reject system roles, etc.) — try the next, don't give up."""
214
+ from src.llm import explainer as ex
215
+ from openai import APIStatusError
216
+ import httpx
217
+
218
+ monkeypatch.delenv("NEUROBRIDGE_DISABLE_LLM", raising=False)
219
+ monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-v1-anything")
220
+
221
+ attempts: list[str] = []
222
+ # Force a known multi-model chain so we can count attempts deterministically
223
+ monkeypatch.setenv("OPENROUTER_FREE_MODELS", "model-a:free,model-b:free,model-c:free")
224
+
225
+ def _raise_400(**kwargs):
226
+ attempts.append(kwargs["model"])
227
+ req = httpx.Request("POST", "https://openrouter.ai/api/v1/chat/completions")
228
+ raise APIStatusError(
229
+ message="bad request",
230
+ response=httpx.Response(400, request=req),
231
+ body={},
232
+ )
233
+
234
+ class _Comp:
235
+ create = staticmethod(_raise_400)
236
+
237
+ class _Chat:
238
+ completions = _Comp()
239
+
240
+ class _Client:
241
+ chat = _Chat()
242
+ def __init__(self, **kwargs):
243
+ pass
244
+
245
+ monkeypatch.setattr("openai.OpenAI", _Client)
246
+
247
+ out = ex._llm_explain(_payload(), modality="bbb")
248
+
249
+ assert out is None, "all models 400'd → must return None for template fallback"
250
+ assert attempts == ["model-a:free", "model-b:free", "model-c:free"], (
251
+ f"400 must advance to next model; got attempts={attempts}"
252
+ )