Spaces:
Running
Running
Sync from GitHub (tests passed)
Browse files- app/commentary.py +1 -0
- app/openrouter_client.py +26 -6
- app/settings.py +9 -8
- scripts/probe_openrouter_contracts.py +2 -2
app/commentary.py
CHANGED
|
@@ -383,6 +383,7 @@ Write as a seasoned commodities strategist would for institutional clients—pre
|
|
| 383 |
"fallback_models": settings.openrouter_fallback_models_list,
|
| 384 |
"chain_deadline_seconds": getattr(settings, "openrouter_chain_deadline_seconds", 120.0),
|
| 385 |
"provider": {"require_parameters": True},
|
|
|
|
| 386 |
"referer": "https://copper-mind.vercel.app",
|
| 387 |
"title": "CopperMind Commentary",
|
| 388 |
}
|
|
|
|
| 383 |
"fallback_models": settings.openrouter_fallback_models_list,
|
| 384 |
"chain_deadline_seconds": getattr(settings, "openrouter_chain_deadline_seconds", 120.0),
|
| 385 |
"provider": {"require_parameters": True},
|
| 386 |
+
"extra_payload": {"reasoning": {"effort": "medium"}},
|
| 387 |
"referer": "https://copper-mind.vercel.app",
|
| 388 |
"title": "CopperMind Commentary",
|
| 389 |
}
|
app/openrouter_client.py
CHANGED
|
@@ -120,8 +120,14 @@ def _extract_message_text(data: dict[str, Any]) -> str:
|
|
| 120 |
|
| 121 |
def _candidate_payload(base_payload: dict[str, Any], candidate: str) -> dict[str, Any]:
|
| 122 |
payload = {**base_payload, "model": candidate}
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
return payload
|
| 126 |
|
| 127 |
|
|
@@ -178,12 +184,19 @@ async def create_chat_completion(
|
|
| 178 |
if time.monotonic() >= deadline:
|
| 179 |
raise OpenRouterError("OpenRouter model chain deadline exceeded", category="timeout", model=candidate, attempts=attempts)
|
| 180 |
await _throttle_request(rpm)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
payload = _candidate_payload(base_payload, candidate)
|
| 182 |
response: Optional[httpx.Response] = None
|
| 183 |
try:
|
| 184 |
-
response = await
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
| 187 |
except httpx.RequestError as exc:
|
| 188 |
last_error = OpenRouterError(str(exc), category="network", model=candidate)
|
| 189 |
else:
|
|
@@ -210,7 +223,14 @@ async def create_chat_completion(
|
|
| 210 |
|
| 211 |
status = response.status_code if response is not None else None
|
| 212 |
retryable = last_error is not None and last_error.category in retryable_categories
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
attempts.append(_attempt(candidate, attempt_index + 1, last_error.category if last_error else "network", status, will_retry))
|
| 215 |
if last_error and last_error.category == "auth":
|
| 216 |
last_error.attempts = attempts
|
|
|
|
| 120 |
|
| 121 |
def _candidate_payload(base_payload: dict[str, Any], candidate: str) -> dict[str, Any]:
|
| 122 |
payload = {**base_payload, "model": candidate}
|
| 123 |
+
existing_reasoning = payload.get("reasoning")
|
| 124 |
+
if candidate.lower().startswith("nvidia/nemotron"):
|
| 125 |
+
if isinstance(existing_reasoning, dict) and "effort" in existing_reasoning:
|
| 126 |
+
payload["reasoning"] = {"effort": existing_reasoning["effort"]}
|
| 127 |
+
else:
|
| 128 |
+
payload["reasoning"] = {"effort": "low"}
|
| 129 |
+
elif isinstance(existing_reasoning, dict) and set(existing_reasoning.keys()) == {"effort"}:
|
| 130 |
+
payload.pop("reasoning", None)
|
| 131 |
return payload
|
| 132 |
|
| 133 |
|
|
|
|
| 184 |
if time.monotonic() >= deadline:
|
| 185 |
raise OpenRouterError("OpenRouter model chain deadline exceeded", category="timeout", model=candidate, attempts=attempts)
|
| 186 |
await _throttle_request(rpm)
|
| 187 |
+
remaining_before_call = deadline - time.monotonic()
|
| 188 |
+
if remaining_before_call <= 0:
|
| 189 |
+
raise OpenRouterError("OpenRouter model chain deadline exceeded", category="timeout", model=candidate, attempts=attempts)
|
| 190 |
+
attempt_timeout = min(float(timeout_seconds), max(remaining_before_call, 0.1))
|
| 191 |
payload = _candidate_payload(base_payload, candidate)
|
| 192 |
response: Optional[httpx.Response] = None
|
| 193 |
try:
|
| 194 |
+
response = await asyncio.wait_for(
|
| 195 |
+
client.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload),
|
| 196 |
+
timeout=attempt_timeout,
|
| 197 |
+
)
|
| 198 |
+
except (httpx.TimeoutException, asyncio.TimeoutError, TimeoutError) as exc:
|
| 199 |
+
last_error = OpenRouterError(str(exc) or "OpenRouter request timed out", category="timeout", model=candidate)
|
| 200 |
except httpx.RequestError as exc:
|
| 201 |
last_error = OpenRouterError(str(exc), category="network", model=candidate)
|
| 202 |
else:
|
|
|
|
| 223 |
|
| 224 |
status = response.status_code if response is not None else None
|
| 225 |
retryable = last_error is not None and last_error.category in retryable_categories
|
| 226 |
+
# When fallback models exist, advance immediately on a timeout instead of
|
| 227 |
+
# burning a second 45s window on the same unresponsive upstream model.
|
| 228 |
+
skip_same_model_timeout_retry = (
|
| 229 |
+
last_error is not None
|
| 230 |
+
and last_error.category == "timeout"
|
| 231 |
+
and len(models) > 1
|
| 232 |
+
)
|
| 233 |
+
will_retry = retryable and not skip_same_model_timeout_retry and attempt_index < max(0, int(max_retries))
|
| 234 |
attempts.append(_attempt(candidate, attempt_index + 1, last_error.category if last_error else "network", status, will_retry))
|
| 235 |
if last_error and last_error.category == "auth":
|
| 236 |
last_error.attempts = attempts
|
app/settings.py
CHANGED
|
@@ -92,15 +92,15 @@ class Settings(BaseSettings):
|
|
| 92 |
# Deprecated - kept for backward compatibility. Keep this on a currently
|
| 93 |
# listed free model so installations without an explicit env override do
|
| 94 |
# not silently start with a retired OpenRouter slug.
|
| 95 |
-
openrouter_model: str = "nvidia/nemotron-3
|
| 96 |
# Keep fast and reliable roles independent so one provider/model removal
|
| 97 |
# cannot collapse the entire LLM path. NVIDIA and Liquid passed the exact
|
| 98 |
# credentialed scoring/commentary probe on 2026-09-27; the Gemma free
|
| 99 |
# models were temporarily rate-limited by their shared upstream pool.
|
| 100 |
-
openrouter_model_scoring: str = "nvidia/nemotron-3
|
| 101 |
-
openrouter_model_scoring_fast: Optional[str] = "nvidia/nemotron-3
|
| 102 |
openrouter_model_scoring_reliable: Optional[str] = "liquid/lfm-2.5-2.6b:free"
|
| 103 |
-
openrouter_model_commentary: str = "nvidia/nemotron-3
|
| 104 |
# Stay below the shared free-provider request ceiling. The worker still
|
| 105 |
# uses bounded retries and a per-chain deadline for transient 429s.
|
| 106 |
openrouter_rpm: int = 12
|
|
@@ -115,7 +115,8 @@ class Settings(BaseSettings):
|
|
| 115 |
# client as transport-level fallbacks when the primary model 429s/5xx's.
|
| 116 |
# Example: "google/gemini-flash-1.5:free,meta-llama/llama-3.1-8b-instruct:free"
|
| 117 |
openrouter_fallback_models: Optional[str] = (
|
| 118 |
-
"liquid/lfm-2.5-2.6b:free,nvidia/nemotron-3
|
|
|
|
| 119 |
"google/gemma-4-31b-it:free,google/gemma-4-26b-a4b-it:free"
|
| 120 |
)
|
| 121 |
tokenizers_parallelism: str = "false"
|
|
@@ -134,7 +135,7 @@ class Settings(BaseSettings):
|
|
| 134 |
|
| 135 |
# LLM Sentiment Analysis
|
| 136 |
# Deprecated - kept for backward compatibility
|
| 137 |
-
llm_sentiment_model: str = "nvidia/nemotron-3
|
| 138 |
|
| 139 |
# Pipeline trigger authentication
|
| 140 |
pipeline_trigger_secret: Optional[str] = None
|
|
@@ -255,7 +256,7 @@ class Settings(BaseSettings):
|
|
| 255 |
self.llm_sentiment_model,
|
| 256 |
self.openrouter_model,
|
| 257 |
)
|
| 258 |
-
or "nvidia/nemotron-3
|
| 259 |
)
|
| 260 |
|
| 261 |
@property
|
|
@@ -273,7 +274,7 @@ class Settings(BaseSettings):
|
|
| 273 |
self.llm_sentiment_model,
|
| 274 |
self.openrouter_model_scoring,
|
| 275 |
)
|
| 276 |
-
or "nvidia/nemotron-3
|
| 277 |
)
|
| 278 |
|
| 279 |
@property
|
|
|
|
| 92 |
# Deprecated - kept for backward compatibility. Keep this on a currently
|
| 93 |
# listed free model so installations without an explicit env override do
|
| 94 |
# not silently start with a retired OpenRouter slug.
|
| 95 |
+
openrouter_model: str = "nvidia/nemotron-3-super-120b-a12b:free"
|
| 96 |
# Keep fast and reliable roles independent so one provider/model removal
|
| 97 |
# cannot collapse the entire LLM path. NVIDIA and Liquid passed the exact
|
| 98 |
# credentialed scoring/commentary probe on 2026-09-27; the Gemma free
|
| 99 |
# models were temporarily rate-limited by their shared upstream pool.
|
| 100 |
+
openrouter_model_scoring: str = "nvidia/nemotron-3-super-120b-a12b:free"
|
| 101 |
+
openrouter_model_scoring_fast: Optional[str] = "nvidia/nemotron-3-super-120b-a12b:free"
|
| 102 |
openrouter_model_scoring_reliable: Optional[str] = "liquid/lfm-2.5-2.6b:free"
|
| 103 |
+
openrouter_model_commentary: str = "nvidia/nemotron-3-super-120b-a12b:free"
|
| 104 |
# Stay below the shared free-provider request ceiling. The worker still
|
| 105 |
# uses bounded retries and a per-chain deadline for transient 429s.
|
| 106 |
openrouter_rpm: int = 12
|
|
|
|
| 115 |
# client as transport-level fallbacks when the primary model 429s/5xx's.
|
| 116 |
# Example: "google/gemini-flash-1.5:free,meta-llama/llama-3.1-8b-instruct:free"
|
| 117 |
openrouter_fallback_models: Optional[str] = (
|
| 118 |
+
"liquid/lfm-2.5-2.6b:free,nvidia/nemotron-3-super-120b-a12b:free,"
|
| 119 |
+
"nvidia/nemotron-3.5-super-vl:free,"
|
| 120 |
"google/gemma-4-31b-it:free,google/gemma-4-26b-a4b-it:free"
|
| 121 |
)
|
| 122 |
tokenizers_parallelism: str = "false"
|
|
|
|
| 135 |
|
| 136 |
# LLM Sentiment Analysis
|
| 137 |
# Deprecated - kept for backward compatibility
|
| 138 |
+
llm_sentiment_model: str = "nvidia/nemotron-3-super-120b-a12b:free"
|
| 139 |
|
| 140 |
# Pipeline trigger authentication
|
| 141 |
pipeline_trigger_secret: Optional[str] = None
|
|
|
|
| 256 |
self.llm_sentiment_model,
|
| 257 |
self.openrouter_model,
|
| 258 |
)
|
| 259 |
+
or "nvidia/nemotron-3-super-120b-a12b:free"
|
| 260 |
)
|
| 261 |
|
| 262 |
@property
|
|
|
|
| 274 |
self.llm_sentiment_model,
|
| 275 |
self.openrouter_model_scoring,
|
| 276 |
)
|
| 277 |
+
or "nvidia/nemotron-3-super-120b-a12b:free"
|
| 278 |
)
|
| 279 |
|
| 280 |
@property
|
scripts/probe_openrouter_contracts.py
CHANGED
|
@@ -106,9 +106,9 @@ async def run_probe(*, fast: str, reliable: str, commentary_model: str, batch_si
|
|
| 106 |
|
| 107 |
def main() -> int:
|
| 108 |
parser = argparse.ArgumentParser(description="Probe exact OpenRouter production contracts")
|
| 109 |
-
parser.add_argument("--fast", default="nvidia/nemotron-3
|
| 110 |
parser.add_argument("--reliable", default="liquid/lfm-2.5-2.6b:free")
|
| 111 |
-
parser.add_argument("--commentary", default="nvidia/nemotron-3
|
| 112 |
parser.add_argument("--batch-size", type=int, default=12, help="Synthetic scoring items, up to the worker's 12-item chunk")
|
| 113 |
args = parser.parse_args()
|
| 114 |
result = asyncio.run(run_probe(fast=args.fast, reliable=args.reliable, commentary_model=args.commentary, batch_size=args.batch_size))
|
|
|
|
| 106 |
|
| 107 |
def main() -> int:
|
| 108 |
parser = argparse.ArgumentParser(description="Probe exact OpenRouter production contracts")
|
| 109 |
+
parser.add_argument("--fast", default="nvidia/nemotron-3-super-120b-a12b:free")
|
| 110 |
parser.add_argument("--reliable", default="liquid/lfm-2.5-2.6b:free")
|
| 111 |
+
parser.add_argument("--commentary", default="nvidia/nemotron-3-super-120b-a12b:free")
|
| 112 |
parser.add_argument("--batch-size", type=int, default=12, help="Synthetic scoring items, up to the worker's 12-item chunk")
|
| 113 |
args = parser.parse_args()
|
| 114 |
result = asyncio.run(run_probe(fast=args.fast, reliable=args.reliable, commentary_model=args.commentary, batch_size=args.batch_size))
|