Skip to content

feat(gradio): add external LM config UI vertical slice#949

Open
1larity wants to merge 10 commits intoace-step:mainfrom
1larity:pr808/external-lm-ui-slice
Open

feat(gradio): add external LM config UI vertical slice#949
1larity wants to merge 10 commits intoace-step:mainfrom
1larity:pr808/external-lm-ui-slice

Conversation

@1larity
Copy link
Copy Markdown
Contributor

@1larity 1larity commented Mar 27, 2026

Summary

This PR adds the external LM vertical slice required to make the new Gradio UI flow operational end to end.

TLDR:-
Use popular local and cloud based LLM APIs to enhance Captions

Caption input
Tropical funk, female lyrics
Is enhanced to
An upbeat and breezy instrumental piece with a distinct Latin and tropical feel. The track is built upon a foundation of lively hand percussion, including bongos and shakers, creating a steady, danceable groove. A nylon-string acoustic guitar provides the core harmonic and melodic structure with rhythmic chords and arpeggios. A warm, round bass guitar underpins the arrangement. The song evolves with the introduction of a clean-toned electric guitar playing melodic fills, followed by a bright, airy flute melody that soars over the top. The arrangement is dynamic, featuring sections where the instruments layer to create a full, celebratory soundscape before gracefully fading out on the main guitar and percussion theme.

Quickstart
Open Settings and configure External LM Configuration with your provider, model, base URL, and API key (you can leave this blank for Ollama). You can use Fetch Models, Save Settings, Test Endpoint, and Runtime Doctor to confirm the setup.

For this slice you will still need to pick a 5Hz LM for planning and lyric handling.
Initialize the service as usual

Once configured, the external model is used for the caption-enhancement actions in the UI. It does not replace ACE-Step’s built-in 5Hz LM for core music generation, and it does not change the local model you pick for the main pipeline. (this will come soon)

The science:-

It introduces external LM configuration/setup UI, provider-scoped runtime hydration, and an external text-model backed Enhance Caption / format path while preserving the existing local 5Hz LM path for audio-code generation.

This PR is larger than ideal, but that is intentional for this feature slice: the UI changes only become meaningful once the setup flow, handler wiring, runtime state, and caption-enhancement path all land together. To keep the slice as small as possible, additional best-practice cleanup, deeper hardening, and follow-on polish have been deferred to later PRs.

What Changed

  • Added a dedicated External LM Configuration accordion above LM generation controls.
  • Added external setup actions for:
    • provider/model/base URL/API key editing
    • model discovery
    • runtime settings save
    • endpoint test
    • runtime doctor
  • Added provider-scoped runtime hydration for external LM setup fields.
  • Routed Enhance Caption / format mode through an external text model when backend=external.
  • Preserved local 5Hz LM behavior for non-external backends.
  • Tightened the external format prompt so caption enhancement behaves more like faithful expansion than random reinvention.
  • Added focused decomposition helpers so service init / wiring / setup paths stay reviewable.
  • Added focused tests for:
    • external format routing
    • external credential resolution
    • external setup actions
    • generation service wiring
    • external LM config UI contracts
  • Updated locale files for the new UI strings in en/ja/zh/he.

Behavioral Parity / Intentional Boundaries

  • Non-external LM backends still use the native 5Hz LM path.
  • External LM support in this slice is for text-only caption enhancement / format mode.
  • Local audio-code generation still requires the native 5Hz LM.
  • Think mode and with-LM runtime limits intentionally remain tied to the local audio-code LM, not the external text LM.
  • Inline API keys are kept in-process only so users can Initialize and test immediately without first saving credentials; persisted runtime settings remain non-secret.

Validation

Executed:

  • uv run python -m unittest acestep.ui.gradio.events.generation.external_lm_setup_test acestep.ui.gradio.events.generation_handlers_test acestep.ui.gradio.events.wiring.generation_service_wiring_test acestep.text_tasks.external_lm_format_client_test acestep.llm_inference_external_format_routing_test
    • Result: Ran 41 tests ... OK
  • python3 -m py_compile on touched Python modules
    • Result: passed
  • Manual Gradio QA pass in /tmp/ace-step-qa-main
    • Result: external LM panel rendered, setup actions wired, and Enhance Caption path exercised against the new UI slice

Scope / Out of Scope

In scope:

  • external LM configuration/setup UI
  • external setup action wiring and runtime hydration
  • external Enhance Caption / format integration
  • focused regression fixes surfaced during QA for the touched Gradio settings surface
  • focused decomposition required to keep the slice reviewable

Out of scope:

  • full external replacement for native 5Hz LM audio-code generation
  • broader UI cleanup outside the touched settings areas
  • deeper credential hardening / storage abstractions beyond what this slice needs
  • broader refactors of pre-existing large core LM modules

Reviewer Focus

  1. External LM setup flow from UI -> handler wiring -> runtime state.
  2. Enhance Caption routing when backend=external.
  3. Behavioral parity for non-external / local 5Hz LM paths.
  4. Credential resolution and non-secret runtime persistence behavior.
  5. Whether the UI regression fixes stayed scoped to the touched settings surface.
  6. Whether any additional hardening should be split into follow-up PRs rather than blocking this vertical slice.

CodeRabbit Scope Guard

Please treat findings as in-scope only when introduced by this external LM vertical slice.

  • Pre-existing issues in large core files outside the touched external LM path should be treated as out-of-scope/non-blocking unless they indicate behavior drift introduced by this PR.
  • Follow-up best-practice cleanup and hardening has intentionally been deferred where it was not required to make the feature slice operational.

Manual UI Validation

  • External LM config panel render/placement validated.
  • Setup actions validated after reviewer-followup fixes.
  • Caption enhancement flow validated from the new UI surface.

Important note on API key security.
Security implementation:-
saved API keys can go through the secure storage path rather than plain runtime settings
macOS/Windows use native keyring when available
Linux uses encrypted file storage with passphrase-store integration
runtime calls resolve credentials from secure/env sources instead of requiring plaintext persistence

What is not fully hardened yet:
An inline API key can still live in process memory in llm_handler.external_config after Initialize, by design, so the user can test immediately without first saving credentials
That is not disk persistence, but it is still plaintext in memory for the session
The slice focuses on getting the vertical feature working; stronger in-memory handling and stricter secret flow Cleanup were deferred

This is secure enough for a vertical-slice feature PR
But not yet the final security posture I’d want long-term, this will be addressed in the next PR

Summary by CodeRabbit

  • New Features

    • External/remote text-model support for caption enhancement: full UI panel (provider/model/base-URL/API key), fetch/save/test/“runtime doctor” tools, model discovery, and secure API-key storage options.
  • Behavior Changes

    • Caption formatting can use a configured external provider even when a local LM is not initialized; UI and status messages now distinguish external vs local initialization and visibility.
  • Content/Prompts

    • Format requests emphasize faithful enhancement (preserve user intent); format-request temperature made configurable.
  • Tests

    • Added extensive tests for routing, HTTP client validation, credential resolution, format-plan client, UI wiring, and setup workflows.
  • Localization

    • Updated translations and UI copy for External backend and status/messages.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds external-LLM caption-enhancement: credential resolution (env or encrypted store), HTTP client and external format-plan requester with retry/merge logic, LLMHandler routing to external plans, Gradio UI controls/persistence/wiring for external providers, and unit tests covering routing, HTTP validation, credential resolution, and UI wiring.

Changes

Cohort / File(s) Summary
LLM routing & guards
acestep/inference.py, acestep/llm_inference.py, acestep/llm_inference_external_format_routing_test.py
Removed early local-init guard in format entry; added external_config, provider detection, availability helpers, _format_sample_from_external_llm, and routing to external path; tests verify external routing without local init.
External format client & HTTP
acestep/text_tasks/external_lm_format_client.py, acestep/text_tasks/external_lm_format_client_test.py, acestep/text_tasks/external_lm_http_client.py, acestep/text_tasks/external_lm_http_client_test.py
New external format-plan client and HTTP requester: provider/profile resolution, API-key selection, POST JSON requests, parse/merge retry logic, metadata overrides, URL validation and transport→ExternalAIClientError mapping; tests cover parsing, retries, key resolution, and URL validation.
Credential helpers
acestep/text_tasks/external_lm_credentials.py
New credential resolution helpers: explicit API key → env var → encrypted secret store; secret-store path resolution and presence checks (handles file vs keyring cases).
Prompt/request helpers
acestep/text_tasks/external_ai_request_helpers.py, acestep/text_tasks/external_lm_captioning.py, acestep/text_tasks/external_lm_captioning_test.py
Tightened format-mode prompt to require faithful enhancement and forbid inventing new concepts; exposed temperature parameter in request builder; added test asserting intent text.
UI controls & builders
acestep/ui/gradio/interfaces/generation_external_lm_controls.py, acestep/ui/gradio/interfaces/generation_advanced_primary_controls.py, acestep/ui/gradio/interfaces/generation_advanced_settings.py, acestep/ui/gradio/interfaces/generation_external_lm_config_test.py, acestep/ui/gradio/interfaces/__init__.py
Added external-LM controls builder (provider/model/base-URL/API key/status/actions), integrated into advanced settings, added CSS for external API key/status, and tests validating component presence/properties.
UI event handlers & actions
acestep/ui/gradio/events/generation/external_lm_setup.py, acestep/ui/gradio/events/generation/external_lm_setup_actions.py, acestep/ui/gradio/events/generation/external_lm_setup_support.py, acestep/ui/gradio/events/generation/external_lm_setup_test.py
New Gradio handlers/actions for hydrating provider fields, discovering models, saving runtime settings (with secret-store encryption), testing endpoints, and runtime doctor summaries; support helpers and tests added.
Service init & helpers
acestep/ui/gradio/events/generation/service_init.py, acestep/ui/gradio/events/generation/service_init_helpers.py, acestep/ui/gradio/events/generation/service_init_test.py, acestep/ui/gradio/events/generation/service_tier_updates.py
Refactored init flow to support external backend: new UI toggles, provider hydration, external config persistence via configure_external_llm, moved tier logic and post-init helperization; tests added/updated.
Format actions & handler exports
acestep/ui/gradio/events/generation/llm_format_actions.py, acestep/ui/gradio/events/generation_handlers.py, acestep/ui/gradio/events/generation_handlers_test.py
LLM availability check now defers to has_available_text_llm() when present; external-specific unavailable message; re-exported external setup functions; tests cover external path when local LM uninitialized.
Wiring decomposition & support
acestep/ui/gradio/events/wiring/generation_service_wiring.py, acestep/ui/gradio/events/wiring/generation_service_wiring_sections.py, acestep/ui/gradio/events/wiring/generation_service_wiring_support.py, acestep/ui/gradio/events/wiring/generation_service_wiring_test.py
Decomposed Gradio wiring into focused modules, added registration for external-LM buttons/fields, delegated handlers to new support modules; tests assert expected handler wiring.
Service config UI & toggles
acestep/ui/gradio/interfaces/generation_service_config_rows.py, acestep/ui/gradio/interfaces/generation_service_config_toggles.py
Included "external" in backend choices, hide local LM model dropdown when external selected, adjust init info text by backend, and added init_llm_local_state tracking.
Localization & tests
acestep/ui/gradio/i18n/*.json, various test files across modules
Added external-LM localization strings across languages and many unit tests covering external-LM flows and UI behavior.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Gradio UI
    participant Handler as Format Handler
    participant LLM as LLMHandler
    participant Creds as Credential Resolver
    participant Client as External HTTP Client
    participant Provider as External Provider

    UI->>Handler: format_sample_from_input(caption, lyrics)
    Handler->>LLM: has_available_text_llm()
    alt external backend available
        LLM-->>Handler: True
        Handler->>LLM: _format_sample_from_external_llm(caption, lyrics)
        LLM->>Creds: resolve_external_api_key(provider)
        Creds-->>LLM: api_key
        LLM->>Client: request_external_format_plan(provider, model, base_url, api_key, caption, lyrics)
        Client->>Provider: POST JSON (format intent)
        Provider-->>Client: JSON plan response
        Client->>Client: parse ExternalAIPlan
        alt caption needs retry
            Client->>Provider: POST retry with rewrite instruction
            Provider-->>Client: improved response
            Client->>Client: merge retry plan
        end
        Client-->>LLM: ExternalAIPlan
        LLM-->>Handler: metadata + status
    else no available LLM
        LLM-->>Handler: False
        Handler-->>UI: error status
    end
    Handler-->>UI: (metadata, status)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly Related PRs

Suggested Reviewers

  • ChuxiJ

Poem

🐰 Hopped from burrow to cloud with a tweak,

Secrets tucked safe, models ready to speak.
Captions kept faithful, never led astray,
Retries mend echoes, tests light the way.
A rabbit's small cheer for external LLM play.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(gradio): add external LM config UI vertical slice' accurately describes the main change—adding an external LM configuration UI component and integration. It is concise, specific, and clearly summarizes the primary objective of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 93.44% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@1larity 1larity force-pushed the pr808/external-lm-ui-slice branch from 5a1f8c1 to f969afe Compare March 27, 2026 20:42
@1larity 1larity marked this pull request as ready for review March 27, 2026 20:59
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (7)
acestep/text_tasks/external_lm_credentials.py (1)

28-34: Make secret-path return contract consistent (Path vs optional).

resolve_external_secret_store_path() is annotated as Path, but _load_external_api_key_from_secret_store() handles None. Please align one way (prefer typed optional if resolve_existing_default_path(...) can return nullish) to avoid misleading callers.

Also applies to: 63-64

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/text_tasks/external_lm_credentials.py` around lines 28 - 34, The
function resolve_external_secret_store_path currently declares a Path return but
can yield None via EncryptedSecretStore.resolve_existing_default_path, so change
its signature to return Optional[Path> (or otherwise ensure it never returns
None) and update callers accordingly; specifically, update
resolve_external_secret_store_path's type annotation to Optional[Path] and
adjust any uses such as in _load_external_api_key_from_secret_store to match the
optional contract (or alternatively make resolve_existing_default_path always
return a Path and keep the non-optional signature) so the types are consistent
across resolve_external_secret_store_path and
_load_external_api_key_from_secret_store.
acestep/ui/gradio/events/generation/service_tier_updates.py (1)

16-17: Add explicit type hints and full public-function docstring for on_tier_change.

Please annotate inputs/return shape and expand the docstring with Args/Returns to keep this new callback self-describing and easier to validate.

As per coding guidelines: "Type hints: Add type annotations for new/modified functions when practical." and "Docstrings are mandatory for all modules, classes, and public functions. Use concise format with Args, Returns, and exception documentation."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/ui/gradio/events/generation/service_tier_updates.py` around lines 16
- 17, Update the on_tier_change function signature to include explicit type
hints (e.g., def on_tier_change(selected_tier: Any, llm_handler: Optional[Any] =
None) -> None) and import Any and Optional from typing; then expand the existing
docstring into a full public-function docstring with a short description plus
Args (selected_tier: type and meaning, llm_handler: type and optional behavior),
Returns (None) and any raised exceptions, ensuring the docstring uses the
project's concise Args/Returns format so the callback is self-describing and
type-checkable.
acestep/llm_inference_external_format_routing_test.py (1)

21-22: Stub signature doesn't match production keyword-only parameters.

The production _format_sample_from_external_llm uses keyword-only parameters (*, caption, lyrics, user_metadata=None), but this stub uses positional parameters. While the test passes due to how Python handles the binding, the stub doesn't accurately reflect the production contract.

♻️ Proposed fix to match production signature
-        def fake_external(self, caption, lyrics, user_metadata=None):
-            return ({"caption": f"external::{caption}", "lyrics": lyrics}, "external-ok")
+        def fake_external(self, *, caption, lyrics, user_metadata=None):
+            return ({"caption": f"external::{caption}", "lyrics": lyrics}, "external-ok")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/llm_inference_external_format_routing_test.py` around lines 21 - 22,
The test stub fake_external does not match the production function signature
_format_sample_from_external_llm which declares keyword-only params; update
fake_external to use the same keyword-only signature (i.e., accept *, caption,
lyrics, user_metadata=None) and ensure any test invocations supply
caption/lyrics as keywords if needed so the stub faithfully mirrors the
production contract.
acestep/ui/gradio/interfaces/generation_advanced_primary_controls.py (1)

60-83: Consider simplifying the wrapper by returning the implementation result directly.

The wrapper explicitly re-maps all 11 keys from the implementation, which creates maintenance overhead if keys change. If the contract requires exactly these keys, consider adding a comment explaining why the explicit mapping is intentional (e.g., to enforce the public API surface).

Alternatively, if the implementation already returns the correct keys:

♻️ Simpler delegation if API surface matches
 def build_external_lm_controls(
     *,
     service_pre_initialized: bool,
     params: dict[str, Any],
 ) -> dict[str, Any]:
     """Create the external-LM configuration accordion shown above LM controls."""
 
-    components = _build_external_lm_controls_impl(
+    return _build_external_lm_controls_impl(
         service_pre_initialized=service_pre_initialized,
         params=params,
     )
-    return {
-        "external_llm_accordion": components["external_llm_accordion"],
-        "external_llm_provider": components["external_llm_provider"],
-        "external_llm_base_url_preset": components["external_llm_base_url_preset"],
-        "external_llm_model": components["external_llm_model"],
-        "external_llm_fetch_models_btn": components["external_llm_fetch_models_btn"],
-        "external_llm_base_url": components["external_llm_base_url"],
-        "external_llm_api_key": components["external_llm_api_key"],
-        "external_llm_status": components["external_llm_status"],
-        "external_llm_save_btn": components["external_llm_save_btn"],
-        "external_llm_test_btn": components["external_llm_test_btn"],
-        "external_llm_doctor_btn": components["external_llm_doctor_btn"],
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/ui/gradio/interfaces/generation_advanced_primary_controls.py` around
lines 60 - 83, The wrapper build_external_lm_controls currently re-maps all keys
from _build_external_lm_controls_impl; simplify by returning the implementation
result directly (i.e., return components) to avoid maintenance overhead, or if
you must enforce an explicit public API, add a short comment above the mapping
explaining that the explicit key list is intentional to lock the public contract
and keep the current explicit return. Reference build_external_lm_controls and
_build_external_lm_controls_impl when making this change.
acestep/ui/gradio/interfaces/generation_external_lm_controls.py (1)

21-41: Missing blank lines between top-level definitions.

Per PEP 8, there should be two blank lines between top-level function definitions and after imports. Lines 20-21, 127-128, and 176-177 are missing these separators.

However, since no static analysis hints flag this and the code is functional, this is a minor style nit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/ui/gradio/interfaces/generation_external_lm_controls.py` around lines
21 - 41, The file is missing the required two blank lines between top-level
definitions per PEP8; add a blank line separation (two newlines) before and
after the top-level function build_external_lm_controls and similarly insert the
two-line separators between other top-level definitions referenced in the review
so that there are two blank lines after imports and between each top-level def
to satisfy PEP8.
acestep/ui/gradio/events/wiring/generation_service_wiring_sections.py (1)

4-11: Missing blank lines between top-level function definitions.

Per PEP 8, there should be two blank lines between top-level function definitions. This affects readability but doesn't impact functionality.

Also applies to: 12-33

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/ui/gradio/events/wiring/generation_service_wiring_sections.py` around
lines 4 - 11, The top-level function definitions (e.g.,
register_dataset_handlers and the subsequent top-level functions starting around
lines 12-33) lack the required two blank lines between them per PEP 8; update
the file so there are exactly two blank lines separating each top-level function
definition (add one additional blank line where only one exists) to restore
proper spacing and improve readability.
acestep/text_tasks/external_lm_format_client_test.py (1)

45-67: Patch targets may cause test fragility.

The test patches EncryptedSecretStore on the external_lm_credentials module but then also patches resolve_existing_default_path on the class itself. This dual-patching approach works but could become fragile if the import structure changes.

Consider patching at the point of use (external_lm_format_client) instead:

`@patch`("acestep.text_tasks.external_lm_format_client.resolve_runtime_passphrase")
`@patch`("acestep.text_tasks.external_lm_format_client.EncryptedSecretStore")

The static analysis hint about "hardcoded password" on line 67 is a false positive—this is a mock value for testing, not a production credential.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/text_tasks/external_lm_format_client_test.py` around lines 45 - 67,
The test test_resolve_external_api_key_uses_secure_store_fallback should patch
the symbols where resolve_external_api_key actually looks them up, so change the
decorators to patch
"acestep.text_tasks.external_lm_format_client.resolve_runtime_passphrase" and
"acestep.text_tasks.external_lm_format_client.EncryptedSecretStore" (and
similarly patch EncryptedSecretStore.resolve_existing_default_path on that same
module) instead of patching the ones in external_lm_credentials; keep the rest
of the test logic unchanged and, if your linter flags the mock passphrase/secret
as a hardcoded credential, add a short test-only comment or disable rule locally
to mark it as a mock.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@acestep/text_tasks/external_lm_format_client.py`:
- Around line 75-99: When a retry is triggered by caption_needs_retry, make the
retry best-effort: call _request_plan_once inside a try/except so timeouts/parse
errors are caught and logged instead of bubbling out, and do not replace plan
wholesale—merge only non-empty fields from retry_plan (e.g., caption, metadata,
lyrics) into the existing plan object so the original plan remains the fallback;
reference the caption_needs_retry check, the retry_intent construction,
_request_plan_once call, and the plan/retry_plan variables when making these
changes.

In `@acestep/text_tasks/external_lm_http_client.py`:
- Around line 28-36: The code constructs a Request and calls urlopen_impl with a
user-provided base_url without validating the URL scheme, which allows file://
or other dangerous schemes; before creating the urllib_request.Request (and
before calling urlopen_impl/Request), parse base_url (e.g., via
urllib.parse.urlparse) and enforce an allowlist of schemes (only "http" and
"https"), and ensure a valid network location (netloc) is present; if validation
fails, raise a clear exception or return an error instead of performing the
request so urlopen_impl(Request, ...) is never invoked with unsafe schemes.

In `@acestep/ui/gradio/events/generation/external_lm_setup_actions.py`:
- Around line 26-39: The try block around model discovery also contains
save_cache, so a cache write failure can mask a successful discover_models call;
refactor by keeping get_profile and discover_models inside the existing try that
catches ValueError and ExternalModelDiscoveryError, then perform
save_cache(profile=profile.provider_id ???) in a separate try/except that only
handles cache-related errors (e.g., OSError, IOError) and logs or surfaces a
non-fatal warning via status_formatter/t(...) without returning an error; ensure
you do not use a bare except and that the successful discovery path still
returns the models even if save_cache fails, referencing get_profile,
discover_models, save_cache, status_formatter, t, and
ExternalModelDiscoveryError to locate the spots to change.

In `@acestep/ui/gradio/events/generation/service_init_helpers.py`:
- Around line 56-81: The code flips llm_handler to external but doesn't unload
any resident local model, leaving weights in memory; before setting
llm_handler.llm_backend = "external" and llm_handler.llm_initialized = False,
call a cleanup/unload routine on llm_handler (e.g., if
callable(getattr(llm_handler, "unload_local_llm", None)):
llm_handler.unload_local_llm()) or, if no dedicated method exists, explicitly
release known resources on llm_handler (close processes, free model
objects/handles, remove GPU contexts) using attribute names present on the
handler; ensure this unload is executed prior to changing backend/init flags so
no local weights remain resident.

In `@acestep/ui/gradio/events/generation/service_init.py`:
- Around line 41-55: update_llm_backend_ui currently forces the init_llm
checkbox to True for external backends but never restores it when switching
back, leaving init_llm "sticky-checked"; change update_llm_backend_ui to accept
or obtain the prior/init default value for the init_llm checkbox (e.g., add a
parameter prev_init_llm: bool or compute tier default based on backend) and when
not external set checkbox_update = gr.update(info=t(info_key),
value=prev_init_llm) (or value=<tier_default>) instead of leaving value
unset/always True so the checkbox is restored to the previous or tier-default
state; update references to checkbox_update and callers of update_llm_backend_ui
accordingly.

In `@acestep/ui/gradio/events/generation/service_tier_updates.py`:
- Around line 53-54: The new user-facing literals must be localized: replace the
plain string assigned to recommended_suffix and the other new status/info
literals (the LM availability note and the batch/duration info added in the same
block) with calls to the localization helper (e.g., t("...")) so they use the
app's translation pipeline; update occurrences where these variables or literals
are displayed to use the translated values (refer to recommended_suffix and the
nearby LM availability and batch/duration message variables in
service_tier_updates.py).

In `@acestep/ui/gradio/interfaces/generation_service_config_rows.py`:
- Around line 168-184: Normalize the backend once into backend_value and reuse
that normalized value for both the local_lm_column visibility and the
backend_dropdown default value to prevent UI state drift; specifically, compute
backend_value by trimming and lowercasing params.get("backend",
recommended_backend) (or falling back to recommended_backend) and then use this
same backend_value for the visibility check on local_lm_column and as the value
passed to the backend_dropdown Dropdown (referencing backend_value,
local_lm_column, backend_dropdown, params, and recommended_backend).

---

Nitpick comments:
In `@acestep/llm_inference_external_format_routing_test.py`:
- Around line 21-22: The test stub fake_external does not match the production
function signature _format_sample_from_external_llm which declares keyword-only
params; update fake_external to use the same keyword-only signature (i.e.,
accept *, caption, lyrics, user_metadata=None) and ensure any test invocations
supply caption/lyrics as keywords if needed so the stub faithfully mirrors the
production contract.

In `@acestep/text_tasks/external_lm_credentials.py`:
- Around line 28-34: The function resolve_external_secret_store_path currently
declares a Path return but can yield None via
EncryptedSecretStore.resolve_existing_default_path, so change its signature to
return Optional[Path> (or otherwise ensure it never returns None) and update
callers accordingly; specifically, update resolve_external_secret_store_path's
type annotation to Optional[Path] and adjust any uses such as in
_load_external_api_key_from_secret_store to match the optional contract (or
alternatively make resolve_existing_default_path always return a Path and keep
the non-optional signature) so the types are consistent across
resolve_external_secret_store_path and _load_external_api_key_from_secret_store.

In `@acestep/text_tasks/external_lm_format_client_test.py`:
- Around line 45-67: The test
test_resolve_external_api_key_uses_secure_store_fallback should patch the
symbols where resolve_external_api_key actually looks them up, so change the
decorators to patch
"acestep.text_tasks.external_lm_format_client.resolve_runtime_passphrase" and
"acestep.text_tasks.external_lm_format_client.EncryptedSecretStore" (and
similarly patch EncryptedSecretStore.resolve_existing_default_path on that same
module) instead of patching the ones in external_lm_credentials; keep the rest
of the test logic unchanged and, if your linter flags the mock passphrase/secret
as a hardcoded credential, add a short test-only comment or disable rule locally
to mark it as a mock.

In `@acestep/ui/gradio/events/generation/service_tier_updates.py`:
- Around line 16-17: Update the on_tier_change function signature to include
explicit type hints (e.g., def on_tier_change(selected_tier: Any, llm_handler:
Optional[Any] = None) -> None) and import Any and Optional from typing; then
expand the existing docstring into a full public-function docstring with a short
description plus Args (selected_tier: type and meaning, llm_handler: type and
optional behavior), Returns (None) and any raised exceptions, ensuring the
docstring uses the project's concise Args/Returns format so the callback is
self-describing and type-checkable.

In `@acestep/ui/gradio/events/wiring/generation_service_wiring_sections.py`:
- Around line 4-11: The top-level function definitions (e.g.,
register_dataset_handlers and the subsequent top-level functions starting around
lines 12-33) lack the required two blank lines between them per PEP 8; update
the file so there are exactly two blank lines separating each top-level function
definition (add one additional blank line where only one exists) to restore
proper spacing and improve readability.

In `@acestep/ui/gradio/interfaces/generation_advanced_primary_controls.py`:
- Around line 60-83: The wrapper build_external_lm_controls currently re-maps
all keys from _build_external_lm_controls_impl; simplify by returning the
implementation result directly (i.e., return components) to avoid maintenance
overhead, or if you must enforce an explicit public API, add a short comment
above the mapping explaining that the explicit key list is intentional to lock
the public contract and keep the current explicit return. Reference
build_external_lm_controls and _build_external_lm_controls_impl when making this
change.

In `@acestep/ui/gradio/interfaces/generation_external_lm_controls.py`:
- Around line 21-41: The file is missing the required two blank lines between
top-level definitions per PEP8; add a blank line separation (two newlines)
before and after the top-level function build_external_lm_controls and similarly
insert the two-line separators between other top-level definitions referenced in
the review so that there are two blank lines after imports and between each
top-level def to satisfy PEP8.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 820a4a6e-e6f7-432d-a4e6-783fbb3ca1d9

📥 Commits

Reviewing files that changed from the base of the PR and between 946ef47 and f969afe.

📒 Files selected for processing (42)
  • acestep/inference.py
  • acestep/llm_inference.py
  • acestep/llm_inference_external_format_routing_test.py
  • acestep/text_tasks/external_ai_request_helpers.py
  • acestep/text_tasks/external_lm_captioning.py
  • acestep/text_tasks/external_lm_captioning_test.py
  • acestep/text_tasks/external_lm_credentials.py
  • acestep/text_tasks/external_lm_format_client.py
  • acestep/text_tasks/external_lm_format_client_test.py
  • acestep/text_tasks/external_lm_http_client.py
  • acestep/ui/gradio/events/generation/__init__.py
  • acestep/ui/gradio/events/generation/external_lm_setup.py
  • acestep/ui/gradio/events/generation/external_lm_setup_actions.py
  • acestep/ui/gradio/events/generation/external_lm_setup_support.py
  • acestep/ui/gradio/events/generation/external_lm_setup_test.py
  • acestep/ui/gradio/events/generation/llm_format_actions.py
  • acestep/ui/gradio/events/generation/service_init.py
  • acestep/ui/gradio/events/generation/service_init_helpers.py
  • acestep/ui/gradio/events/generation/service_init_test.py
  • acestep/ui/gradio/events/generation/service_tier_updates.py
  • acestep/ui/gradio/events/generation_handlers.py
  • acestep/ui/gradio/events/generation_handlers_test.py
  • acestep/ui/gradio/events/wiring/generation_service_wiring.py
  • acestep/ui/gradio/events/wiring/generation_service_wiring_sections.py
  • acestep/ui/gradio/events/wiring/generation_service_wiring_support.py
  • acestep/ui/gradio/events/wiring/generation_service_wiring_test.py
  • acestep/ui/gradio/i18n/en.json
  • acestep/ui/gradio/i18n/he.json
  • acestep/ui/gradio/i18n/ja.json
  • acestep/ui/gradio/i18n/zh.json
  • acestep/ui/gradio/interfaces/__init__.py
  • acestep/ui/gradio/interfaces/generation_advanced_dit_controls.py
  • acestep/ui/gradio/interfaces/generation_advanced_output_controls.py
  • acestep/ui/gradio/interfaces/generation_advanced_primary_controls.py
  • acestep/ui/gradio/interfaces/generation_advanced_settings.py
  • acestep/ui/gradio/interfaces/generation_decomposition_contract_test.py
  • acestep/ui/gradio/interfaces/generation_external_lm_config_test.py
  • acestep/ui/gradio/interfaces/generation_external_lm_controls.py
  • acestep/ui/gradio/interfaces/generation_service_config_rows.py
  • acestep/ui/gradio/interfaces/generation_service_config_toggles.py
  • acestep/ui/gradio/interfaces/generation_tab_optional_controls.py
  • acestep/ui/gradio/interfaces/generation_tab_source_controls.py
💤 Files with no reviewable changes (3)
  • acestep/ui/gradio/interfaces/generation_tab_optional_controls.py
  • acestep/ui/gradio/interfaces/generation_tab_source_controls.py
  • acestep/inference.py

Comment thread acestep/text_tasks/external_lm_format_client.py Outdated
Comment thread acestep/text_tasks/external_lm_http_client.py
Comment thread acestep/ui/gradio/events/generation/external_lm_setup_actions.py
Comment thread acestep/ui/gradio/events/generation/service_init_helpers.py
Comment thread acestep/ui/gradio/events/generation/service_init.py Outdated
Comment thread acestep/ui/gradio/events/generation/service_tier_updates.py
Comment thread acestep/ui/gradio/interfaces/generation_service_config_rows.py
@1larity 1larity marked this pull request as draft March 27, 2026 21:14
@1larity 1larity marked this pull request as ready for review March 27, 2026 22:00
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (6)
acestep/text_tasks/external_lm_format_client.py (1)

176-177: Minor: Missing blank line between function definitions.

PEP 8 recommends two blank lines between top-level function definitions.

✏️ Formatting fix
     content = extract_protocol_message_content(raw_response=raw_response, protocol=protocol)
     return parse_plan_from_content(content, task_focus="format")
+
+
 def _post_external_request(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/text_tasks/external_lm_format_client.py` around lines 176 - 177,
There is no blank line between the end of parse_plan_from_content usage and the
next top-level function _post_external_request; add one additional blank line so
there are two blank lines separating the top-level definitions (ensure the line
after the return parse_plan_from_content(content, task_focus="format") is blank
before def _post_external_request).
acestep/ui/gradio/events/generation/external_lm_setup_actions.py (1)

185-204: Consider extracting _has_secret_store_value to reduce module size.

The module is at 204 LOC, slightly over the 200 LOC hard cap. The _has_secret_store_value helper could be moved to external_lm_setup_support.py to bring this under the cap.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/ui/gradio/events/generation/external_lm_setup_actions.py` around
lines 185 - 204, The module exceeds the 200 LOC cap because the helper
_has_secret_store_value is defined inline; extract this function into
external_lm_setup_support.py and import it where needed. Move the full function
(including its dependencies: EncryptedSecretStore, SecretStoreError,
resolve_runtime_passphrase) into external_lm_setup_support.py, update the
original file to replace the local definition with "from
.external_lm_setup_support import _has_secret_store_value", and ensure any
relative imports and unit tests still resolve; run linter/tests to verify no
missing imports or name collisions.
acestep/text_tasks/external_lm_format_client_test.py (1)

1-215: Module exceeds 200 LOC hard cap (215 LOC).

Per coding guidelines, modules should target ≤150 LOC with a hard cap of 200. Consider splitting credential-resolution tests (test_resolve_external_api_key_*) into a separate test module from the plan-request tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/text_tasks/external_lm_format_client_test.py` around lines 1 - 215,
The file exceeds the 200-LOC cap because credential-resolution tests are bundled
with plan-request tests; split the credential tests into a new test module by
moving test_resolve_external_api_key_uses_provider_env_fallback and
test_resolve_external_api_key_uses_secure_store_fallback (and their supporting
imports/mocks: resolve_external_api_key, ExternalAIClientError, and the patched
symbols "acestep.text_tasks.external_lm_credentials.resolve_runtime_passphrase"
/ "EncryptedSecretStore") into a separate test file (e.g.,
external_lm_credentials_test) so the original external_lm_format_client_test.py
falls under the LOC cap; ensure you keep required imports and the environment
patching, update any relative imports if needed, and run test discovery to
confirm both modules execute.
acestep/ui/gradio/events/generation/external_lm_setup_test.py (1)

1-253: Module significantly exceeds 200 LOC hard cap (253 LOC).

Per coding guidelines, this test module should be split by responsibility. Consider separating into:

  • external_lm_setup_hydration_test.py (hydration tests)
  • external_lm_setup_fetch_test.py (fetch/discovery tests)
  • external_lm_setup_persistence_test.py (save/doctor tests)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/ui/gradio/events/generation/external_lm_setup_test.py` around lines 1
- 253, The test module external_lm_setup_test.py is over the 200 LOC cap; split
it into three focused test modules: move hydration-related tests (methods:
test_hydrate_external_lm_setup_fields_returns_saved_values_and_preset and any
mocks calling load_external_lm_runtime_settings/load_cached_external_models)
into external_lm_setup_hydration_test.py; move discovery/fetch tests (methods:
test_fetch_external_lm_models_updates_dropdown_and_status,
test_fetch_external_lm_models_keeps_success_when_cache_write_fails,
test_discover_provider_models_uses_resolved_api_key_fallback and any references
to fetch_external_lm_models, discover_provider_models,
external_lm_setup_support.discover_external_models) into
external_lm_setup_fetch_test.py; and move persistence/doctor tests (methods:
test_save_external_lm_settings_reports_runtime_only_when_api_key_blank,
test_save_external_lm_settings_persists_api_key_when_secret_store_available,
test_runtime_doctor_reports_secret_store_presence,
test_runtime_doctor_reports_keyring_secret_without_file and any references to
save_external_lm_runtime_settings, EncryptedSecretStore,
run_external_lm_runtime_doctor, _resolve_secret_store_path,
_has_secret_store_value) into external_lm_setup_persistence_test.py; ensure each
new file imports only the mocks and symbols it needs (external_lm_setup,
external_lm_setup_support, get_external_provider_profile) and retains the
original unittest.TestCase structure and patch decorators so tests run
unchanged.
acestep/ui/gradio/events/generation/service_init_helpers.py (1)

37-43: Fragile path derivation — depends on exact nesting depth.

Six nested dirname calls will break if the module is relocated. Consider a more robust approach (e.g., walking upward to find a marker like pyproject.toml or .git), or document the expected path structure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/ui/gradio/events/generation/service_init_helpers.py` around lines 37
- 43, The resolve_project_root function currently uses six nested
os.path.dirname calls which is brittle; modify resolve_project_root to walk
upward from current_file checking each parent directory for repository/project
markers (e.g., "pyproject.toml", "setup.py", or ".git") and return the first
parent that contains any marker; if no marker is found, fall back to the
original dirname-based heuristic or raise a clear error. Update references in
any callers to handle potential exceptions or None, and keep the function
signature resolve_project_root(current_file: str) -> str unchanged.
acestep/ui/gradio/events/generation/service_init.py (1)

110-114: Consider catching invalid provider early for a friendlier error.

If external_llm_provider is empty or invalid, get_external_provider_profile raises ValueError and the entire init fails with a traceback. A try/except here could return a user-friendly status message instead.

Suggested improvement
     normalized_backend = (backend or "").strip().lower()
     external_profile = None
     if normalized_backend == "external":
-        external_profile = get_external_provider_profile(external_llm_provider)
+        try:
+            external_profile = get_external_provider_profile(external_llm_provider)
+        except ValueError:
+            return build_post_init_result(
+                dit_handler=dit_handler,
+                llm_handler=llm_handler,
+                config_path=config_path,
+                current_mode=current_mode,
+                current_batch_size=current_batch_size,
+                status=f"❌ Invalid external LM provider: {external_llm_provider!r}",
+                enable=False,
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@acestep/ui/gradio/events/generation/service_init.py` around lines 110 - 114,
The code calls get_external_provider_profile(external_llm_provider) without
handling ValueError which leads to a raw traceback; wrap that call (in the block
using normalized_backend and external_profile) in a try/except catching
ValueError, and in the except set external_profile = None and return or populate
a clear user-friendly status/message (e.g., "invalid external provider: <name>")
instead of letting the exception bubble; specifically change the logic around
normalized_backend, external_profile and the get_external_provider_profile call
to handle invalid/empty external_llm_provider gracefully.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@acestep/text_tasks/external_lm_format_client_test.py`:
- Around line 1-215: The file exceeds the 200-LOC cap because
credential-resolution tests are bundled with plan-request tests; split the
credential tests into a new test module by moving
test_resolve_external_api_key_uses_provider_env_fallback and
test_resolve_external_api_key_uses_secure_store_fallback (and their supporting
imports/mocks: resolve_external_api_key, ExternalAIClientError, and the patched
symbols "acestep.text_tasks.external_lm_credentials.resolve_runtime_passphrase"
/ "EncryptedSecretStore") into a separate test file (e.g.,
external_lm_credentials_test) so the original external_lm_format_client_test.py
falls under the LOC cap; ensure you keep required imports and the environment
patching, update any relative imports if needed, and run test discovery to
confirm both modules execute.

In `@acestep/text_tasks/external_lm_format_client.py`:
- Around line 176-177: There is no blank line between the end of
parse_plan_from_content usage and the next top-level function
_post_external_request; add one additional blank line so there are two blank
lines separating the top-level definitions (ensure the line after the return
parse_plan_from_content(content, task_focus="format") is blank before def
_post_external_request).

In `@acestep/ui/gradio/events/generation/external_lm_setup_actions.py`:
- Around line 185-204: The module exceeds the 200 LOC cap because the helper
_has_secret_store_value is defined inline; extract this function into
external_lm_setup_support.py and import it where needed. Move the full function
(including its dependencies: EncryptedSecretStore, SecretStoreError,
resolve_runtime_passphrase) into external_lm_setup_support.py, update the
original file to replace the local definition with "from
.external_lm_setup_support import _has_secret_store_value", and ensure any
relative imports and unit tests still resolve; run linter/tests to verify no
missing imports or name collisions.

In `@acestep/ui/gradio/events/generation/external_lm_setup_test.py`:
- Around line 1-253: The test module external_lm_setup_test.py is over the 200
LOC cap; split it into three focused test modules: move hydration-related tests
(methods: test_hydrate_external_lm_setup_fields_returns_saved_values_and_preset
and any mocks calling
load_external_lm_runtime_settings/load_cached_external_models) into
external_lm_setup_hydration_test.py; move discovery/fetch tests (methods:
test_fetch_external_lm_models_updates_dropdown_and_status,
test_fetch_external_lm_models_keeps_success_when_cache_write_fails,
test_discover_provider_models_uses_resolved_api_key_fallback and any references
to fetch_external_lm_models, discover_provider_models,
external_lm_setup_support.discover_external_models) into
external_lm_setup_fetch_test.py; and move persistence/doctor tests (methods:
test_save_external_lm_settings_reports_runtime_only_when_api_key_blank,
test_save_external_lm_settings_persists_api_key_when_secret_store_available,
test_runtime_doctor_reports_secret_store_presence,
test_runtime_doctor_reports_keyring_secret_without_file and any references to
save_external_lm_runtime_settings, EncryptedSecretStore,
run_external_lm_runtime_doctor, _resolve_secret_store_path,
_has_secret_store_value) into external_lm_setup_persistence_test.py; ensure each
new file imports only the mocks and symbols it needs (external_lm_setup,
external_lm_setup_support, get_external_provider_profile) and retains the
original unittest.TestCase structure and patch decorators so tests run
unchanged.

In `@acestep/ui/gradio/events/generation/service_init_helpers.py`:
- Around line 37-43: The resolve_project_root function currently uses six nested
os.path.dirname calls which is brittle; modify resolve_project_root to walk
upward from current_file checking each parent directory for repository/project
markers (e.g., "pyproject.toml", "setup.py", or ".git") and return the first
parent that contains any marker; if no marker is found, fall back to the
original dirname-based heuristic or raise a clear error. Update references in
any callers to handle potential exceptions or None, and keep the function
signature resolve_project_root(current_file: str) -> str unchanged.

In `@acestep/ui/gradio/events/generation/service_init.py`:
- Around line 110-114: The code calls
get_external_provider_profile(external_llm_provider) without handling ValueError
which leads to a raw traceback; wrap that call (in the block using
normalized_backend and external_profile) in a try/except catching ValueError,
and in the except set external_profile = None and return or populate a clear
user-friendly status/message (e.g., "invalid external provider: <name>") instead
of letting the exception bubble; specifically change the logic around
normalized_backend, external_profile and the get_external_provider_profile call
to handle invalid/empty external_llm_provider gracefully.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a8bb8ad-1ef8-48bf-b5a2-c3ddbaa7de7b

📥 Commits

Reviewing files that changed from the base of the PR and between f969afe and 9a5b8a8.

📒 Files selected for processing (15)
  • acestep/text_tasks/external_lm_format_client.py
  • acestep/text_tasks/external_lm_format_client_test.py
  • acestep/text_tasks/external_lm_http_client.py
  • acestep/text_tasks/external_lm_http_client_test.py
  • acestep/ui/gradio/events/generation/external_lm_setup_actions.py
  • acestep/ui/gradio/events/generation/external_lm_setup_test.py
  • acestep/ui/gradio/events/generation/service_init.py
  • acestep/ui/gradio/events/generation/service_init_helpers.py
  • acestep/ui/gradio/events/generation/service_init_test.py
  • acestep/ui/gradio/events/wiring/generation_service_wiring.py
  • acestep/ui/gradio/i18n/en.json
  • acestep/ui/gradio/i18n/he.json
  • acestep/ui/gradio/i18n/ja.json
  • acestep/ui/gradio/i18n/zh.json
  • acestep/ui/gradio/interfaces/generation_service_config_toggles.py
✅ Files skipped from review due to trivial changes (3)
  • acestep/ui/gradio/i18n/zh.json
  • acestep/ui/gradio/i18n/ja.json
  • acestep/ui/gradio/i18n/en.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • acestep/ui/gradio/i18n/he.json
  • acestep/ui/gradio/interfaces/generation_service_config_toggles.py
  • acestep/ui/gradio/events/generation/service_init_test.py

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