Skip to content

Add optional proxy_base_url so Aspis works without the Vector proxy - #160

Open
lotif wants to merge 1 commit into
mainfrom
optional-proxy-base-url
Open

Add optional proxy_base_url so Aspis works without the Vector proxy#160
lotif wants to merge 1 commit into
mainfrom
optional-proxy-base-url

Conversation

@lotif

@lotif lotif commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

PR Type

Feature

Short Description

Add an optional proxy_base_url so Aspis can use each provider's OpenAI-compatible default endpoint instead of requiring the Vector proxy. Custom model IDs still require an explicit proxy; known models fall back through request override → ASPIS_OPENAI_BASE_URL → provider default. Saved YAML now includes model_id.

Tests Added

Unit/API/UI coverage for resolution order, custom-vs-known model validation, form submission, and YAML restore (including legacy files without model_id).

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added optional proxy URL configuration for model evaluations.
    • Added support for custom model IDs, with required proxy details when needed.
    • Added provider-specific default proxy endpoints and override support.
    • Updated the evaluation form to include expandable proxy settings and URL validation.
    • Model IDs are now preserved in exported results and restored when files are uploaded.
  • Bug Fixes

    • Improved validation errors and preserved specific API error responses.
    • Added compatibility for older result files without model IDs.

Aspis previously routed every provider through a single hardcoded Vector
proxy, which locked out anyone without access to it. Each model now carries
its provider's OpenAI-compatible default base URL, and both the UI and the
API accept an optional proxy_base_url override, making Vector one option
among many rather than a requirement.

The base URL resolves as: explicit proxy_base_url, then ASPIS_OPENAI_BASE_URL,
then the model's provider default. Custom model IDs have no known provider,
so they require an explicit proxy in both the UI and the API.

The UI keeps every landing-page input inside a single form, since Streamlit
only flushes text typed immediately before a click for widgets belonging to
the submitted form. The proxy field sits in a collapsed "Proxy details"
expander and starts empty. Saved YAML now records the model ID used; files
written before this change still load.

Co-authored-by: Cursor <cursoragent@cursor.com>
@lotif
lotif marked this pull request as ready for review August 13, 2026 00:13
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds provider metadata and provider-specific proxy defaults. Inference resolves known and custom model IDs with request, environment, or provider proxy precedence. The API accepts string model IDs and optional proxy URLs. The UI validates proxy and model inputs, forwards proxy settings, and persists model_id in YAML results. Uploads restore known or custom models. Tests cover resolution, validation, propagation, form behavior, and persistence.

Estimated code review effort: 4 (Complex) | ~60 minutes

Mergeability Score: 🟠 High · up to 866d9

This PR changes endpoint selection and saved-session restoration. At the current head, some custom-model requests can be rejected inconsistently, restored custom models can fail during generation or reuse an old proxy, and publicly exposed deployments may allow caller-controlled URLs to reach internal services. Merge should wait for these correctness and security risks to be fixed or explicitly accepted by the owning team.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description includes all required sections and accurately summarizes proxy resolution, custom models, persistence, and test coverage.
Title check ✅ Passed The title clearly and concisely describes the main change: optional proxy_base_url support without the Vector proxy.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch optional-proxy-base-url

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
src/aspis/api/main.py (3)

121-128: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pass proxy_base_url by keyword.

asyncio.to_thread forwards keyword arguments to the target. Naming the last argument removes the dependency on the parameter position in evaluate_text.

♻️ Proposed change
         results = await asyncio.to_thread(
             evaluate_text,
             text_to_evaluate,
             prompt_templates,
             model_for_eval,
             api_key,
-            normalized_proxy,
+            proxy_base_url=normalized_proxy,
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aspis/api/main.py` around lines 121 - 128, Update the asyncio.to_thread
call around evaluate_text to pass normalized_proxy as the proxy_base_url keyword
argument, while preserving the existing positional arguments and evaluation
flow.

34-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider moving _normalize_optional_proxy into inferencer.py and reusing it.

The same "strip, then treat blank as absent" rule now exists in three places:

  • _normalize_optional_proxy here.
  • resolve_proxy_base_url in src/aspis/inferencer.py lines 136-141.
  • resolve_submitted_proxy in src/aspis/ui/main.py line 47.

A single shared helper next to validate_proxy_base_url keeps the blank-handling rule in one place, so the API, the UI, and the resolver cannot drift.

♻️ Proposed shared helper

Add to src/aspis/inferencer.py:

def normalize_optional_proxy_base_url(proxy_base_url: str | None) -> str | None:
    """Return a stripped proxy base URL, or None when absent or blank."""
    if proxy_base_url is None:
        return None
    return proxy_base_url.strip() or None

Then in src/aspis/api/main.py:

-from aspis.inferencer import ModelInfo, evaluate_text, get_inference_prompt, validate_proxy_base_url
+from aspis.inferencer import (
+    ModelInfo,
+    evaluate_text,
+    get_inference_prompt,
+    normalize_optional_proxy_base_url,
+    validate_proxy_base_url,
+)
-
-
-def _normalize_optional_proxy(proxy_base_url: str | None) -> str | None:
-    """Return a stripped proxy URL, or None when empty/absent."""
-    if proxy_base_url is None:
-        return None
-    stripped = proxy_base_url.strip()
-    return stripped or None
-        normalized_proxy = _normalize_optional_proxy(proxy_base_url)
+        normalized_proxy = normalize_optional_proxy_base_url(proxy_base_url)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aspis/api/main.py` around lines 34 - 39, Move the blank-proxy
normalization into a shared normalize_optional_proxy_base_url helper in
inferencer.py near validate_proxy_base_url, then reuse it from
resolve_proxy_base_url, resolve_submitted_proxy, and the API code. Remove the
local _normalize_optional_proxy implementation and update imports so all three
callers apply the same strip-and-None behavior.

57-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The hardcoded model list will drift from ModelInfo.

This docstring becomes the published OpenAPI description. It lists all seven model IDs by hand. When someone adds or removes a ModelInfo member in src/aspis/inferencer.py, this list goes stale and the API documentation misreports the accepted values.

Consider pointing readers at the enum instead of enumerating members, or building the description from ModelInfo at import time.

♻️ Proposed change
-        model: The model ID to use for this evaluation. Optional,
-            defaults to `gpt-4o`. Known values include `gpt-4o`,
-            `gpt-5.5`, `gpt-5.4-mini`, `gemini-3.1-pro-preview`,
-            `gemini-3-flash-preview`, `claude-opus-4-7`, and
-            `claude-sonnet-4-6`. Custom model IDs are allowed when
-            `proxy_base_url` is provided.
+        model: The model ID to use for this evaluation. Optional,
+            defaults to `gpt-4o`. Known values are the `model_id`
+            values of `aspis.inferencer.ModelInfo`. A known model uses
+            its provider default endpoint. Custom model IDs are allowed
+            when `proxy_base_url` is provided.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aspis/api/main.py` around lines 57 - 62, Update the model parameter
documentation in the API definition to avoid hardcoding individual model IDs;
reference ModelInfo as the source of supported values or generate the
description from it at import time. Preserve the documented default and the note
that custom model IDs require proxy_base_url.
tests/aspis/ui/test_main.py (2)

634-690: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

test_main_invalid_proxy_url_rejected is fully contained in the next test.

Lines 636-650 and lines 667-690 submit the same inputs, assert the same error text, and both assert mock_openai.assert_not_called(). The second test adds the session-state and rerun assertions. The first test adds no distinct coverage.

Consider deleting test_main_invalid_proxy_url_rejected and keeping test_main_failed_proxy_validation_does_not_persist_inputs, which is the stronger assertion. The rerun check at line 687 is the valuable part; keep it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/aspis/ui/test_main.py` around lines 634 - 690, Remove the redundant
test_main_invalid_proxy_url_rejected test. Retain
test_main_failed_proxy_validation_does_not_persist_inputs with its validation
error, session-state, landing-page, mock_openai, and subsequent rerun
assertions, including the rerun check.

789-813: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for the uncovered "Please select a model" branch.

_apply_landing_form_submission line 96 in src/aspis/ui/main.py rejects a None or blank model and emits "Please select a model before proceeding." No test exercises that branch, so it is the one error message in the function with no coverage. The submit_landing_form harness already accepts model_info=None.

💚 Proposed additional test
+@pytest.mark.parametrize("model_info", [None, "", "   "])
+def test_apply_landing_form_submission_missing_model_persists_nothing(model_info: str | None) -> None:
+    accepted, session_state, errors = submit_landing_form(model_info, "")
+
+    assert accepted is False
+    assert errors == ["Please select a model before proceeding."]
+    assert_nothing_persisted(session_state)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/aspis/ui/test_main.py` around lines 789 - 813, Add a parameterized case
to test_apply_landing_form_submission_missing_inputs_persist_nothing that passes
model_info=None while providing otherwise valid product, risk, and API key
inputs, and expects “Please select a model before proceeding.” with
accepted=False and nothing persisted. Keep the existing missing-input cases
unchanged.
tests/aspis/test_inferencer.py (1)

113-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add two small cases to complete the precedence matrix.

The existing tests cover request, environment, and provider-default precedence well. Two reachable branches of resolve_proxy_base_url have no test:

  • model=None and proxy_base_url=None, which create_openai_client(api_key) reaches and which must raise ValueError.
  • A blank ASPIS_OPENAI_BASE_URL value, which line 140 of src/aspis/inferencer.py must skip so the provider default applies.
💚 Proposed additional tests
 def test_resolve_proxy_base_url_ignores_blank_request(monkeypatch: pytest.MonkeyPatch) -> None:
     monkeypatch.delenv("ASPIS_OPENAI_BASE_URL", raising=False)
     assert (
         resolve_proxy_base_url(proxy_base_url="   ", model=ModelInfo.OPENAI_GPT_4O)
         == ModelInfo.OPENAI_GPT_4O.default_proxy_base_url
     )
+
+
+def test_resolve_proxy_base_url_ignores_blank_env(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setenv("ASPIS_OPENAI_BASE_URL", "   ")
+    assert resolve_proxy_base_url(model=ModelInfo.OPENAI_GPT_4O) == ModelInfo.OPENAI_GPT_4O.default_proxy_base_url
+
+
+def test_resolve_proxy_base_url_requires_model_or_override(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.delenv("ASPIS_OPENAI_BASE_URL", raising=False)
+    with pytest.raises(ValueError, match="proxy_base_url is required"):
+        resolve_proxy_base_url()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/aspis/test_inferencer.py` around lines 113 - 124, Add tests for the
uncovered resolve_proxy_base_url branches: verify
resolve_proxy_base_url(model=None, proxy_base_url=None) raises ValueError, and
verify a whitespace-only ASPIS_OPENAI_BASE_URL is ignored so the provider
default is returned. Follow the existing monkeypatch environment cleanup and
assertion style in the adjacent tests.
tests/aspis/api/test_main.py (1)

202-203: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tests that assert provider-default base URLs depend on the ambient ASPIS_OPENAI_BASE_URL. resolve_proxy_base_url returns ASPIS_OPENAI_BASE_URL ahead of any provider default. Every assertion of default_proxy_base_url in these two files therefore fails when a developer or CI environment sets that variable. tests/aspis/test_inferencer.py guards the equivalent assertions with monkeypatch.delenv; these files do not. Add one autouse fixture per file.

  • tests/aspis/api/test_main.py#L202-L203: add a module-level autouse fixture that calls monkeypatch.delenv("ASPIS_OPENAI_BASE_URL", raising=False), covering the provider-default assertions at lines 81-85, 185-189, 235-239, and 432-436.
  • tests/aspis/ui/test_main.py#L26-L29: add the same autouse fixture, covering make_openai_side_effect and the provider-default assertions at lines 84, 108-112, 191-195, 219-223, 240, 265-269, 382, 420-424, 471-475, and 837-841.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/aspis/api/test_main.py` around lines 202 - 203, Add a module-level
autouse fixture in tests/aspis/api/test_main.py at lines 202-203 that deletes
ASPIS_OPENAI_BASE_URL via monkeypatch.delenv(..., raising=False), covering the
listed provider-default assertions. Add the same fixture in
tests/aspis/ui/test_main.py at lines 26-29 so make_openai_side_effect and all
listed provider-default assertions are isolated from the ambient environment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aspis/api/main.py`:
- Around line 89-95: Replace the duplicated custom-model proxy checks with the
shared resolve_proxy_base_url behavior: in src/aspis/api/main.py lines 89-95,
call resolve_proxy_base_url and map ValueError to HTTP 422; in
src/aspis/ui/main.py lines 27-58, update resolve_submitted_proxy to call
resolve_proxy_base_url(proxy_base_url=None, model=resolved_model) before showing
the user-facing error, and only raise that message when ValueError occurs.

In `@src/aspis/inferencer.py`:
- Around line 99-112: Extend validate_proxy_base_url with an opt-in
environment-controlled host allowlist: retain unrestricted http/https validation
by default, but when the guard is enabled, reject loopback, link-local, private,
and otherwise disallowed hosts before the URL reaches the OpenAI client. Reuse
the existing environment/configuration conventions and preserve the current
behavior for self-hosted runs with the guard unset.

In `@src/aspis/ui/main.py`:
- Around line 389-394: Update the saved-results restore path around
ModelInfo.from_model_id to clear st.session_state.proxy_base_url on every
restore, and leave api_key unset when restoring a custom model so the landing
page can request a proxy address. Preserve the existing known-model and
default-model behavior, and add a test in the restore coverage that uploads a
custom-model file and triggers generation.

---

Nitpick comments:
In `@src/aspis/api/main.py`:
- Around line 121-128: Update the asyncio.to_thread call around evaluate_text to
pass normalized_proxy as the proxy_base_url keyword argument, while preserving
the existing positional arguments and evaluation flow.
- Around line 34-39: Move the blank-proxy normalization into a shared
normalize_optional_proxy_base_url helper in inferencer.py near
validate_proxy_base_url, then reuse it from resolve_proxy_base_url,
resolve_submitted_proxy, and the API code. Remove the local
_normalize_optional_proxy implementation and update imports so all three callers
apply the same strip-and-None behavior.
- Around line 57-62: Update the model parameter documentation in the API
definition to avoid hardcoding individual model IDs; reference ModelInfo as the
source of supported values or generate the description from it at import time.
Preserve the documented default and the note that custom model IDs require
proxy_base_url.

In `@tests/aspis/api/test_main.py`:
- Around line 202-203: Add a module-level autouse fixture in
tests/aspis/api/test_main.py at lines 202-203 that deletes ASPIS_OPENAI_BASE_URL
via monkeypatch.delenv(..., raising=False), covering the listed provider-default
assertions. Add the same fixture in tests/aspis/ui/test_main.py at lines 26-29
so make_openai_side_effect and all listed provider-default assertions are
isolated from the ambient environment.

In `@tests/aspis/test_inferencer.py`:
- Around line 113-124: Add tests for the uncovered resolve_proxy_base_url
branches: verify resolve_proxy_base_url(model=None, proxy_base_url=None) raises
ValueError, and verify a whitespace-only ASPIS_OPENAI_BASE_URL is ignored so the
provider default is returned. Follow the existing monkeypatch environment
cleanup and assertion style in the adjacent tests.

In `@tests/aspis/ui/test_main.py`:
- Around line 634-690: Remove the redundant test_main_invalid_proxy_url_rejected
test. Retain test_main_failed_proxy_validation_does_not_persist_inputs with its
validation error, session-state, landing-page, mock_openai, and subsequent rerun
assertions, including the rerun check.
- Around line 789-813: Add a parameterized case to
test_apply_landing_form_submission_missing_inputs_persist_nothing that passes
model_info=None while providing otherwise valid product, risk, and API key
inputs, and expects “Please select a model before proceeding.” with
accepted=False and nothing persisted. Keep the existing missing-input cases
unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8157cdf6-29e5-4628-816a-bf821f5ea65f

📥 Commits

Reviewing files that changed from the base of the PR and between 81557d0 and 866d95a.

📒 Files selected for processing (9)
  • plans/2026-08-12-optional-proxy-base-url.md
  • src/aspis/api/main.py
  • src/aspis/inferencer.py
  • src/aspis/systematization.py
  • src/aspis/ui/main.py
  • tests/aspis/api/test_main.py
  • tests/aspis/test_inferencer.py
  • tests/aspis/test_systematization.py
  • tests/aspis/ui/test_main.py

Comment thread src/aspis/api/main.py
Comment on lines +89 to +95
model = model.strip()
known_model = ModelInfo.from_model_id(model)
if known_model is None and normalized_proxy is None:
raise HTTPException(
status_code=422,
detail="proxy_base_url is required when model is not a known ModelInfo value",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The required-proxy rule is duplicated in two entry points and ignores ASPIS_OPENAI_BASE_URL. resolve_proxy_base_url in src/aspis/inferencer.py lines 136-149 accepts a custom model ID when ASPIS_OPENAI_BASE_URL is set. Both entry points reimplement the rule as "a custom model ID always requires an explicit proxy", so both reject input the core layer supports. The shared root cause is the duplicated rule; delegating to the resolver removes the drift.

  • src/aspis/api/main.py#L89-L95: replace the known_model is None and normalized_proxy is None guard with a resolve_proxy_base_url call wrapped in try/except ValueError, and map the error to 422.
  • src/aspis/ui/main.py#L27-L58: in resolve_submitted_proxy, before raising "Please enter a proxy address for custom model IDs.", let resolve_proxy_base_url(proxy_base_url=None, model=resolved_model) decide, and raise the user-facing message only when it raises ValueError.
📍 Affects 2 files
  • src/aspis/api/main.py#L89-L95 (this comment)
  • src/aspis/ui/main.py#L27-L58
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aspis/api/main.py` around lines 89 - 95, Replace the duplicated
custom-model proxy checks with the shared resolve_proxy_base_url behavior: in
src/aspis/api/main.py lines 89-95, call resolve_proxy_base_url and map
ValueError to HTTP 422; in src/aspis/ui/main.py lines 27-58, update
resolve_submitted_proxy to call resolve_proxy_base_url(proxy_base_url=None,
model=resolved_model) before showing the user-facing error, and only raise that
message when ValueError occurs.

Comment thread src/aspis/inferencer.py
Comment on lines +99 to +112
def validate_proxy_base_url(proxy_base_url: str) -> None:
"""Validate a non-empty proxy base URL.

Empty strings are not validated; callers should skip this when the value is empty.

Args:
proxy_base_url: The proxy base URL to validate.

Raises:
ValueError: If the URL is not a valid http(s) URL with a host.
"""
parsed = urlparse(proxy_base_url.strip())
if parsed.scheme not in ("http", "https") or not parsed.netloc:
raise ValueError(f"Invalid proxy_base_url: {proxy_base_url}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚖️ Poor tradeoff

Consider an opt-in SSRF guard for deployments that expose the API.

validate_proxy_base_url accepts any http/https host. src/aspis/api/main.py passes the caller-supplied value straight into the OpenAI client base URL. On a publicly reachable deployment, a caller can direct outbound requests to internal addresses, for example http://169.254.169.254/ or http://localhost:9200/, and read the response body through the evaluation result.

plans/2026-08-12-optional-proxy-base-url.md lines 29 and 87 record the host allowlist as out of scope, so this is a deliberate trade-off and not a defect in this PR. One low-cost mitigation keeps that decision intact: gate the host check behind an environment variable so self-hosted single-user runs stay unrestricted and shared deployments can restrict hosts.

🛡️ Optional opt-in host allowlist
 def validate_proxy_base_url(proxy_base_url: str) -> None:
     """Validate a non-empty proxy base URL.
 
     Empty strings are not validated; callers should skip this when the value is empty.
 
     Args:
         proxy_base_url: The proxy base URL to validate.
 
     Raises:
         ValueError: If the URL is not a valid http(s) URL with a host.
     """
     parsed = urlparse(proxy_base_url.strip())
     if parsed.scheme not in ("http", "https") or not parsed.netloc:
         raise ValueError(f"Invalid proxy_base_url: {proxy_base_url}")
+
+    # Optional deployment guard. Unset means "allow any host" (self-hosted default).
+    allowlist = os.environ.get("ASPIS_PROXY_HOST_ALLOWLIST", "").strip()
+    if allowlist:
+        allowed = {host.strip().lower() for host in allowlist.split(",") if host.strip()}
+        if (parsed.hostname or "").lower() not in allowed:
+            raise ValueError(f"proxy_base_url host is not allowed: {parsed.hostname}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def validate_proxy_base_url(proxy_base_url: str) -> None:
"""Validate a non-empty proxy base URL.
Empty strings are not validated; callers should skip this when the value is empty.
Args:
proxy_base_url: The proxy base URL to validate.
Raises:
ValueError: If the URL is not a valid http(s) URL with a host.
"""
parsed = urlparse(proxy_base_url.strip())
if parsed.scheme not in ("http", "https") or not parsed.netloc:
raise ValueError(f"Invalid proxy_base_url: {proxy_base_url}")
def validate_proxy_base_url(proxy_base_url: str) -> None:
"""Validate a non-empty proxy base URL.
Empty strings are not validated; callers should skip this when the value is empty.
Args:
proxy_base_url: The proxy base URL to validate.
Raises:
ValueError: If the URL is not a valid http(s) URL with a host.
"""
parsed = urlparse(proxy_base_url.strip())
if parsed.scheme not in ("http", "https") or not parsed.netloc:
raise ValueError(f"Invalid proxy_base_url: {proxy_base_url}")
# Optional deployment guard. Unset means "allow any host" (self-hosted default).
allowlist = os.environ.get("ASPIS_PROXY_HOST_ALLOWLIST", "").strip()
if allowlist:
allowed = {host.strip().lower() for host in allowlist.split(",") if host.strip()}
if (parsed.hostname or "").lower() not in allowed:
raise ValueError(f"proxy_base_url host is not allowed: {parsed.hostname}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aspis/inferencer.py` around lines 99 - 112, Extend
validate_proxy_base_url with an opt-in environment-controlled host allowlist:
retain unrestricted http/https validation by default, but when the guard is
enabled, reject loopback, link-local, private, and otherwise disallowed hosts
before the URL reaches the OpenAI client. Reuse the existing
environment/configuration conventions and preserve the current behavior for
self-hosted runs with the guard unset.

Comment thread src/aspis/ui/main.py
Comment on lines +389 to +394
saved_model_id = saved_results.get("model_id")
if saved_model_id:
known = ModelInfo.from_model_id(str(saved_model_id))
st.session_state.model_info = known if known is not None else str(saved_model_id)
else:
st.session_state.model_info = ModelInfo.OPENAI_GPT_4O

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restoring a custom model_id leaves proxy_base_url unset or stale.

This restore path sets st.session_state.model_info but never assigns st.session_state.proxy_base_url. The saved YAML omits the proxy by design, per plans/2026-08-12-optional-proxy-base-url.md decision 9. Two failures follow when the restored model_id is a custom ID.

First, an unhandled exception. In a fresh session proxy_base_url is absent, so main() line 125 yields None. The landing-page guard at line 133 passes, because model_info, api_key, product_description, and risk_description are all set by this function. Generation then calls resolve_proxy_base_url with a custom model, no override, and no ASPIS_OPENAI_BASE_URL, which raises ValueError. Streamlit renders a raw traceback instead of an actionable message.

Second, a stale value. If an earlier submission stored a proxy for one model, uploading a file with a different custom model_id silently reuses the previous proxy.

Fix the missing assignment rather than guarding downstream. Clear the proxy on every restore, and for a custom model leave api_key unset so the landing page stays in control and the user can enter a proxy address:

🐛 Proposed fix
     st.session_state.product_description = saved_results["product_description"]
     st.session_state.risk_description = saved_results["risk_description"]
-    # Note: API key is set to a placeholder because it can't be None,
-    # we're restoring saved results and don't need to make new API calls at this stage
-    st.session_state.api_key = "placeholder-key"
     saved_model_id = saved_results.get("model_id")
     if saved_model_id:
         known = ModelInfo.from_model_id(str(saved_model_id))
         st.session_state.model_info = known if known is not None else str(saved_model_id)
     else:
         st.session_state.model_info = ModelInfo.OPENAI_GPT_4O
+
+    # The proxy address is never persisted, so drop any value from an earlier submission.
+    st.session_state.proxy_base_url = None
+
+    # Note: API key is set to a placeholder because it can't be None,
+    # we're restoring saved results and don't need to make new API calls at this stage.
+    # A restored custom model ID has no provider default and no proxy, so keep the user
+    # on the landing page to collect a proxy address before any call is attempted.
+    if isinstance(st.session_state.model_info, ModelInfo):
+        st.session_state.api_key = "placeholder-key"
+
     st.session_state.follow_up_questions = saved_results["follow_up_questions"]

tests/aspis/ui/test_main.py lines 965-1001 restore a custom model_id and assert the results render. That test passes today only because no generation call is needed once systematized_concepts is present. Add a case that uploads a custom-model file and then triggers generation, to pin the corrected behaviour.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aspis/ui/main.py` around lines 389 - 394, Update the saved-results
restore path around ModelInfo.from_model_id to clear
st.session_state.proxy_base_url on every restore, and leave api_key unset when
restoring a custom model so the landing page can request a proxy address.
Preserve the existing known-model and default-model behavior, and add a test in
the restore coverage that uploads a custom-model file and triggers generation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant