Add optional proxy_base_url so Aspis works without the Vector proxy - #160
Add optional proxy_base_url so Aspis works without the Vector proxy#160lotif wants to merge 1 commit into
Conversation
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>
📝 WalkthroughWalkthroughThe 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 Estimated code review effort: 4 (Complex) | ~60 minutes Mergeability Score: 🟠 High · up to 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)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (7)
src/aspis/api/main.py (3)
121-128: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePass
proxy_base_urlby keyword.
asyncio.to_threadforwards keyword arguments to the target. Naming the last argument removes the dependency on the parameter position inevaluate_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 valueConsider moving
_normalize_optional_proxyintoinferencer.pyand reusing it.The same "strip, then treat blank as absent" rule now exists in three places:
_normalize_optional_proxyhere.resolve_proxy_base_urlinsrc/aspis/inferencer.pylines 136-141.resolve_submitted_proxyinsrc/aspis/ui/main.pyline 47.A single shared helper next to
validate_proxy_base_urlkeeps 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 NoneThen 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 valueThe 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
ModelInfomember insrc/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
ModelInfoat 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_rejectedis 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_rejectedand keepingtest_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 winAdd a case for the uncovered "Please select a model" branch.
_apply_landing_form_submissionline 96 insrc/aspis/ui/main.pyrejects aNoneor 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. Thesubmit_landing_formharness already acceptsmodel_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 winAdd 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_urlhave no test:
model=Noneandproxy_base_url=None, whichcreate_openai_client(api_key)reaches and which must raiseValueError.- A blank
ASPIS_OPENAI_BASE_URLvalue, which line 140 ofsrc/aspis/inferencer.pymust 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 winTests that assert provider-default base URLs depend on the ambient
ASPIS_OPENAI_BASE_URL.resolve_proxy_base_urlreturnsASPIS_OPENAI_BASE_URLahead of any provider default. Every assertion ofdefault_proxy_base_urlin these two files therefore fails when a developer or CI environment sets that variable.tests/aspis/test_inferencer.pyguards the equivalent assertions withmonkeypatch.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 callsmonkeypatch.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, coveringmake_openai_side_effectand 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
📒 Files selected for processing (9)
plans/2026-08-12-optional-proxy-base-url.mdsrc/aspis/api/main.pysrc/aspis/inferencer.pysrc/aspis/systematization.pysrc/aspis/ui/main.pytests/aspis/api/test_main.pytests/aspis/test_inferencer.pytests/aspis/test_systematization.pytests/aspis/ui/test_main.py
| 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", | ||
| ) |
There was a problem hiding this comment.
🎯 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 theknown_model is None and normalized_proxy is Noneguard with aresolve_proxy_base_urlcall wrapped intry/except ValueError, and map the error to 422.src/aspis/ui/main.py#L27-L58: inresolve_submitted_proxy, before raising "Please enter a proxy address for custom model IDs.", letresolve_proxy_base_url(proxy_base_url=None, model=resolved_model)decide, and raise the user-facing message only when it raisesValueError.
📍 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.
| 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}") |
There was a problem hiding this comment.
🔒 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.
| 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.
| 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 |
There was a problem hiding this comment.
🩺 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.
PR Type
Feature
Short Description
Add an optional
proxy_base_urlso 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 includesmodel_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
Bug Fixes