-
Notifications
You must be signed in to change notification settings - Fork 0
fix(strix): retry transient OpenRouter upstream 502s #1359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c63edc2
31594fc
f779ba3
3913b4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2869,7 +2869,7 @@ PY | |||||||||||||||||||||||||||||||||||||||
| echo "Strix report artifacts emitted warning/fatal/denied/timeout output; failing closed." | tee -a "$STRIX_LOG" >&2 | ||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if [ "$report_failure_signal" -eq 1 ] || has_detected_infrastructure_error; then | ||||||||||||||||||||||||||||||||||||||||
| if [ "$report_failure_signal" -eq 1 ] || has_detected_infrastructure_error "$model"; then | ||||||||||||||||||||||||||||||||||||||||
| INFRA_ERROR_DETECTED=1 | ||||||||||||||||||||||||||||||||||||||||
| if [ "$rc" -eq 0 ] && provider_signal_fail_closed_enabled; then | ||||||||||||||||||||||||||||||||||||||||
| echo "Strix run emitted provider infrastructure or failure-signal output; failing closed." >&2 | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2915,13 +2915,72 @@ is_llm_api_connection_error() { | |||||||||||||||||||||||||||||||||||||||
| return 1 | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| is_openrouter_upstream_502_error() { | ||||||||||||||||||||||||||||||||||||||||
| local model="${1-}" | ||||||||||||||||||||||||||||||||||||||||
| case "$model" in | ||||||||||||||||||||||||||||||||||||||||
| openrouter/*) ;; | ||||||||||||||||||||||||||||||||||||||||
| *) return 1 ;; | ||||||||||||||||||||||||||||||||||||||||
| esac | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| # Parse only the JSON record attached to LiteLLM's OpenRouter APIError. | ||||||||||||||||||||||||||||||||||||||||
| # Whole-log regexes can borrow an unrelated target 502, while JSON parsing | ||||||||||||||||||||||||||||||||||||||||
| # also keeps metadata key order and nested values irrelevant. | ||||||||||||||||||||||||||||||||||||||||
| python3 - "$STRIX_LOG" <<'PY' | ||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||||||||
| import re | ||||||||||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| api_error = re.compile(r"litellm(?:\.exceptions)?\.APIError:.*OpenrouterException", re.I) | ||||||||||||||||||||||||||||||||||||||||
| lines = Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").splitlines() | ||||||||||||||||||||||||||||||||||||||||
| for index, line in enumerate(lines): | ||||||||||||||||||||||||||||||||||||||||
| match = api_error.search(line) | ||||||||||||||||||||||||||||||||||||||||
| if match is None: | ||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||
| inline_start = line.find("{", match.end()) | ||||||||||||||||||||||||||||||||||||||||
| fragments = [line[inline_start:]] if inline_start >= 0 else lines[index + 1 : index + 33] | ||||||||||||||||||||||||||||||||||||||||
| if not fragments or not fragments[0].lstrip().startswith("{"): | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2940
to
+2942
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win inline JSON 레코드의 후속 줄을 함께 누적하십시오. Line 2941은 APIError 줄에 수정 예시- fragments = [line[inline_start:]] if inline_start >= 0 else lines[index + 1 : index + 33]
+ fragments = (
+ [line[inline_start:]] + lines[index + 1 : index + 33]
+ if inline_start >= 0
+ else lines[index + 1 : index + 33]
+ )이 형식의 다중 줄 JSON 회귀 테스트도 추가하십시오. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||
| record = "" | ||||||||||||||||||||||||||||||||||||||||
| for fragment in fragments: | ||||||||||||||||||||||||||||||||||||||||
| record += fragment.strip() | ||||||||||||||||||||||||||||||||||||||||
| if len(record) > 65536: | ||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||
| value = json.loads(record) | ||||||||||||||||||||||||||||||||||||||||
| except json.JSONDecodeError: | ||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||
| error = value.get("error", {}) if isinstance(value, dict) else {} | ||||||||||||||||||||||||||||||||||||||||
| metadata = error.get("metadata", {}) if isinstance(error, dict) else {} | ||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||
| type(error.get("code")) is int | ||||||||||||||||||||||||||||||||||||||||
| and error["code"] == 502 | ||||||||||||||||||||||||||||||||||||||||
| and isinstance(metadata, dict) | ||||||||||||||||||||||||||||||||||||||||
| and isinstance(metadata.get("provider_name"), str) | ||||||||||||||||||||||||||||||||||||||||
| and metadata["provider_name"].strip() | ||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2953
to
+2961
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Malformed error JSON crashes the OpenRouter 502 classifier When the OpenRouter error JSON has a non-object
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||||
| raise SystemExit(0) | ||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||
| raise SystemExit(1) | ||||||||||||||||||||||||||||||||||||||||
| PY | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| is_llm_service_unavailable_error() { | ||||||||||||||||||||||||||||||||||||||||
| local model="${1-}" | ||||||||||||||||||||||||||||||||||||||||
| if grep -Eiq 'litellm(\.exceptions)?\.ServiceUnavailableError' "$STRIX_LOG" && | ||||||||||||||||||||||||||||||||||||||||
| grep -Eiq '(GeminiException|Nvidia_nimException|nvidia[_ -]?nim|VertexAI|Vertex_ai|vertex\.ai|openai|anthropic|LLM CONNECTION FAILED|Could not establish connection to the language model)' "$STRIX_LOG" && | ||||||||||||||||||||||||||||||||||||||||
| grep -Eiq '("status"[[:space:]]*:[[:space:]]*"UNAVAILABLE"|(^|[^0-9])503([^0-9]|$)|high demand|temporarily overloaded|Service Unavailable)' "$STRIX_LOG"; then | ||||||||||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| # OpenRouter's dynamic free route can surface an upstream provider 502 as | ||||||||||||||||||||||||||||||||||||||||
| # APIError rather than ServiceUnavailableError. Require both LiteLLM's | ||||||||||||||||||||||||||||||||||||||||
| # OpenRouter exception and OpenRouter's provider metadata so target-app 502 | ||||||||||||||||||||||||||||||||||||||||
| # output cannot independently trigger a provider retry. | ||||||||||||||||||||||||||||||||||||||||
| if is_openrouter_upstream_502_error "$model"; then | ||||||||||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return 1 | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2965,10 +3024,14 @@ is_transient_same_model_retry_error() { | |||||||||||||||||||||||||||||||||||||||
| if is_timeout_error; then | ||||||||||||||||||||||||||||||||||||||||
| return 1 | ||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||
| if grep -Eiq 'Vulnerabilities[[:space:]]+[1-9][0-9]*' "$STRIX_LOG" || | ||||||||||||||||||||||||||||||||||||||||
| has_blocking_vulnerability_reports; then | ||||||||||||||||||||||||||||||||||||||||
| return 1 | ||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||
|
seonghobae marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||
| if is_llm_api_connection_error; then | ||||||||||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||
| if is_llm_service_unavailable_error; then | ||||||||||||||||||||||||||||||||||||||||
| if is_llm_service_unavailable_error "$model"; then | ||||||||||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||
| if is_rate_limit_error; then | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3037,7 +3100,7 @@ run_strix_with_transient_retry() { | |||||||||||||||||||||||||||||||||||||||
| retry_reason="rate limit" | ||||||||||||||||||||||||||||||||||||||||
| elif is_llm_api_connection_error; then | ||||||||||||||||||||||||||||||||||||||||
| retry_reason="LLM API connection" | ||||||||||||||||||||||||||||||||||||||||
| elif is_llm_service_unavailable_error; then | ||||||||||||||||||||||||||||||||||||||||
| elif is_llm_service_unavailable_error "$model"; then | ||||||||||||||||||||||||||||||||||||||||
| retry_reason="LLM service unavailable" | ||||||||||||||||||||||||||||||||||||||||
| elif is_midstream_fallback_error; then | ||||||||||||||||||||||||||||||||||||||||
| retry_reason="midstream fallback" | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3269,6 +3332,7 @@ is_llm_token_limit_error() { | |||||||||||||||||||||||||||||||||||||||
| # was interrupted or incomplete. Used as a guard to prevent the | ||||||||||||||||||||||||||||||||||||||||
| # below-threshold override from silently passing an aborted scan. | ||||||||||||||||||||||||||||||||||||||||
| has_detected_infrastructure_error() { | ||||||||||||||||||||||||||||||||||||||||
| local model="${1-}" | ||||||||||||||||||||||||||||||||||||||||
| if grep -Eiq '(^|[^[:alpha:]])(Fatal|Denied|Warn|Warning)([^[:alpha:]]|$)' "$STRIX_LOG"; then | ||||||||||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3293,7 +3357,7 @@ has_detected_infrastructure_error() { | |||||||||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if is_llm_service_unavailable_error; then | ||||||||||||||||||||||||||||||||||||||||
| if is_llm_service_unavailable_error "$model"; then | ||||||||||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -4190,7 +4254,7 @@ is_model_retryable_error() { | |||||||||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if is_llm_service_unavailable_error; then | ||||||||||||||||||||||||||||||||||||||||
| if is_llm_service_unavailable_error "$model"; then | ||||||||||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3774,6 +3774,62 @@ REPORT | |
| ;; | ||
| esac | ||
| ;; | ||
| openrouter-502-fallback-retry-same-model-success) | ||
| case "${STRIX_LLM:-}" in | ||
| vertex_ai/missing-primary) | ||
| echo "Error: litellm.NotFoundError: Vertex_aiException - x" | ||
| echo '"status": "NOT_FOUND"' | ||
| exit 1 | ||
| ;; | ||
| openrouter/free) | ||
| attempt="0" | ||
| if [ -f "${FAKE_STRIX_STATE_FILE:?}" ]; then | ||
| attempt="$(cat "${FAKE_STRIX_STATE_FILE:?}")" | ||
| fi | ||
| attempt="$((attempt + 1))" | ||
| echo "$attempt" > "${FAKE_STRIX_STATE_FILE:?}" | ||
| if [ "$attempt" -eq 1 ]; then | ||
| echo "LLM CONNECTION FAILED" | ||
| echo "Could not establish connection to the language model." | ||
| echo "Error: litellm.APIError: APIError: OpenrouterException -" | ||
| echo '{"error":{"message":"Invalid URL:' | ||
| echo '","code":502,"metadata":{"provider":{"region":"us"},"provider_name":"Stealth"}}}' | ||
| exit 1 | ||
| fi | ||
| echo "scan ok after OpenRouter 502 same-model retry" | ||
| exit 0 | ||
| ;; | ||
| vertex_ai/fallback-two) | ||
| echo "Error: second fallback should not be needed after transient OpenRouter 502" >&2 | ||
| exit 38 | ||
| ;; | ||
| *) | ||
| echo "Error: OpenRouter 502 fallback path unexpected (${STRIX_LLM:-})" >&2 | ||
| exit 38 | ||
| ;; | ||
| esac | ||
| ;; | ||
| non-openrouter-openrouter-502-nonrecoverable) | ||
| echo "Error: litellm.APIError: APIError: OpenrouterException -" | ||
| echo '{"error":{"message":"upstream","code":502,"metadata":{"provider_name":"Stealth"}}}' | ||
| exit 1 | ||
| ;; | ||
| openrouter-apierror-400-target-502-nonrecoverable) | ||
| echo "Error: litellm.APIError: APIError: OpenrouterException -" | ||
| echo '{"error":{"message":"bad request","code":400,"metadata":{"provider_name":"Stealth"}}}' | ||
| echo 'TARGET RESPONSE: {"error":{"code":502,"metadata":{"provider_name":"target-service"}}}' | ||
| exit 1 | ||
| ;; | ||
| openrouter-502-with-critical-report-fails-closed) | ||
| mkdir -p "$STRIX_REPORTS_DIR/openrouter-critical/vulnerabilities" | ||
| cat >"$STRIX_REPORTS_DIR/openrouter-critical/vulnerabilities/vuln-0001.md" <<'REPORT' | ||
| **Severity:** CRITICAL | ||
| **Title:** OpenRouter retry must preserve this finding | ||
| REPORT | ||
| echo "Error: litellm.APIError: APIError: OpenrouterException -" | ||
| echo '{"error":{"message":"upstream","code":502,"metadata":{"provider_name":"Stealth"}}}' | ||
| exit 1 | ||
| ;; | ||
| github-models-primary-unavailable-fallback-success|github-models-primary-denied-fallback-success) | ||
| case "${STRIX_LLM:-}" in | ||
| openai/gpt-5) | ||
|
|
@@ -4012,6 +4068,7 @@ EOS | |
| ;; | ||
| service-unavailable-no-llm-marker-nonrecoverable) | ||
| echo 'ServiceUnavailableError: {"error":{"code":503,"status":"UNAVAILABLE"}}' | ||
| echo '{"error":{"code":502,"metadata":{"provider_name":"Stealth"}}}' | ||
| echo 'target application high demand response' | ||
| exit 1 | ||
| ;; | ||
|
|
@@ -6167,6 +6224,76 @@ run_filtered_gate_case_if_requested() { | |
| "" \ | ||
| "github_models/deepseek/deepseek-v3-0324 github_models/deepseek/deepseek-r1-0528" | ||
| ;; | ||
| openrouter-502-fallback-retry-same-model-success) | ||
| run_gate_case "openrouter-502-fallback-retry-same-model-success" \ | ||
| "vertex_ai/missing-primary" \ | ||
| "openrouter/free vertex_ai/fallback-two" \ | ||
| "0" \ | ||
| "scan ok after OpenRouter 502 same-model retry" \ | ||
| "3" \ | ||
| "vertex_ai/missing-primary|openrouter/free|openrouter/free" \ | ||
| "<unset>|https://example.invalid|https://example.invalid" \ | ||
| "vertex_ai" \ | ||
| "__DEFAULT__" \ | ||
| "" \ | ||
| "1" | ||
| ;; | ||
| non-openrouter-openrouter-502-nonrecoverable) | ||
| run_gate_case "$STRIX_TEST_CASE_FILTER" \ | ||
| "custom/non-openrouter" \ | ||
| "vertex_ai/fallback-one" \ | ||
| "1" \ | ||
| "Strix quick scan failed with a non-recoverable error." \ | ||
| "1" \ | ||
| "custom/non-openrouter" \ | ||
| "https://example.invalid" \ | ||
| "custom" \ | ||
| "__DEFAULT__" \ | ||
| "" \ | ||
| "1" | ||
| ;; | ||
| openrouter-apierror-400-target-502-nonrecoverable) | ||
| run_gate_case "$STRIX_TEST_CASE_FILTER" \ | ||
| "openrouter/free" \ | ||
| "vertex_ai/fallback-one" \ | ||
| "1" \ | ||
| "Strix quick scan failed with a non-recoverable error." \ | ||
| "1" \ | ||
| "openrouter/free" \ | ||
| "https://example.invalid" \ | ||
| "openrouter" \ | ||
| "__DEFAULT__" \ | ||
| "" \ | ||
| "1" | ||
| ;; | ||
| openrouter-502-with-critical-report-fails-closed) | ||
| run_gate_case "$STRIX_TEST_CASE_FILTER" \ | ||
| "openrouter/free" \ | ||
| "vertex_ai/fallback-one" \ | ||
| "1" \ | ||
| "Strix scan failed after provider infrastructure or failure-signal output; failing closed." \ | ||
| "1" \ | ||
| "openrouter/free" \ | ||
| "https://example.invalid" \ | ||
| "openrouter" \ | ||
| "__DEFAULT__" \ | ||
| "" \ | ||
| "1" | ||
| ;; | ||
| service-unavailable-no-llm-marker-nonrecoverable) | ||
| run_gate_case "service-unavailable-no-llm-marker-nonrecoverable" \ | ||
| "custom/service-unavailable-primary" \ | ||
| "vertex_ai/fallback-one vertex_ai/fallback-two" \ | ||
| "1" \ | ||
| "Strix quick scan failed with a non-recoverable error." \ | ||
| "1" \ | ||
| "custom/service-unavailable-primary" \ | ||
| "https://example.invalid" \ | ||
| "custom" \ | ||
| "__DEFAULT__" \ | ||
| "" \ | ||
| "1" | ||
| ;; | ||
| custom-openai-compatible-preserves-effort) | ||
| run_gate_case "custom-openai-compatible-preserves-effort" \ | ||
| "openai-direct/gpt-5.4" \ | ||
|
|
@@ -9942,6 +10069,58 @@ run_gate_case_allow_provider_signal "github-models-internal-server-connection-re | |
| "" \ | ||
| "1" | ||
|
|
||
| run_gate_case "openrouter-502-fallback-retry-same-model-success" \ | ||
| "vertex_ai/missing-primary" \ | ||
| "openrouter/free vertex_ai/fallback-two" \ | ||
| "0" \ | ||
| "scan ok after OpenRouter 502 same-model retry" \ | ||
| "3" \ | ||
| "vertex_ai/missing-primary|openrouter/free|openrouter/free" \ | ||
| "<unset>|https://example.invalid|https://example.invalid" \ | ||
| "vertex_ai" \ | ||
| "__DEFAULT__" \ | ||
| "" \ | ||
| "1" | ||
|
|
||
| run_gate_case "non-openrouter-openrouter-502-nonrecoverable" \ | ||
| "custom/non-openrouter" \ | ||
| "vertex_ai/fallback-one" \ | ||
| "1" \ | ||
| "Strix quick scan failed with a non-recoverable error." \ | ||
| "1" \ | ||
| "custom/non-openrouter" \ | ||
| "https://example.invalid" \ | ||
| "custom" \ | ||
| "__DEFAULT__" \ | ||
| "" \ | ||
| "1" | ||
|
|
||
| run_gate_case "openrouter-apierror-400-target-502-nonrecoverable" \ | ||
| "openrouter/free" \ | ||
| "vertex_ai/fallback-one" \ | ||
| "1" \ | ||
| "Strix quick scan failed with a non-recoverable error." \ | ||
| "1" \ | ||
| "openrouter/free" \ | ||
| "https://example.invalid" \ | ||
| "openrouter" \ | ||
| "__DEFAULT__" \ | ||
| "" \ | ||
| "1" | ||
|
|
||
| run_gate_case "openrouter-502-with-critical-report-fails-closed" \ | ||
| "openrouter/free" \ | ||
| "vertex_ai/fallback-one" \ | ||
| "1" \ | ||
| "Strix scan failed after provider infrastructure or failure-signal output; failing closed." \ | ||
| "1" \ | ||
| "openrouter/free" \ | ||
| "https://example.invalid" \ | ||
| "openrouter" \ | ||
| "__DEFAULT__" \ | ||
| "" \ | ||
| "1" | ||
|
Comment on lines
+10111
to
+10122
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 New fail-closed test asserts a message the gate never emits For an OpenRouter 502 that also carries a blocking CRITICAL finding, the gate treats it as retryable ( Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| run_gate_case "github-models-primary-unavailable-fallback-success" \ | ||
| "openai/gpt-5" \ | ||
| "" \ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.