Spaces:
Sleeping
A newer version of the Gradio SDK is available: 6.22.0
Lexsi SDK β Issues, Workarounds, and Suggested Fixes
Compiled while building the Lexsi DS Agent (v0). All issues hit with
lexsi-sdk==0.1.46 (PyPI, May 2026). Repo:
Lexsi-Labs/Lexsi-sdk. Docs:
sdk.lexsi.ai.
Severity tags:
- π Bug β reproducible code defect
- π Documentation β code is fine, docs are wrong/misleading
- π§© Design β works as designed but creates a UX cliff for consumers
- β Hypothesis β observed behavior matches this but we don't have a direct confirmation from the SDK code
Each issue records: where it lives in the SDK, our user-visible symptom,
the workaround we landed in lexsi_ds_agent, and a suggested SDK-side
fix where applicable.
1. π UnboundLocalError in TabularProject.model_inference when pod=None β β
RESOLVED upstream
Status: Fixed in lexsi-sdk 0.1.51 via PR #69. The new SDK makes pod a required positional argument on model_inference, eliminating the unbound-variable code path. Our predict tool still passes pod="small" by default, but it's now a required-arg shim rather than a bug-dodge.
Severity: Bug. Crashed on every call unless caller passes a non-None pod.
File: lexsi_sdk/core/tabular.py β inside TabularProject.model_inference.
Symptom seen from agent:
predict: model_inference failed: UnboundLocalError: local variable
'custom_batch_servers' referenced before assignment
Root cause β the validation block references custom_batch_servers
in its else branch, but the variable is only bound inside the if
branch:
if pod and self.metadata.get("modality") == "tabular":
custom_batch_servers = self.api_client.get(AVAILABLE_BATCH_SERVERS_URI)
available_custom_batch_servers = (
custom_batch_servers.get("details", [])
+ custom_batch_servers.get("available_gpu_custom_servers", [])
)
Validate.value_against_list(
"pod", pod,
[server["instance_name"] for server in available_custom_batch_servers],
)
else:
Validate.value_against_list(
"pod", pod,
[
server["instance_name"]
# β NameError: custom_batch_servers never bound on this path
for server in custom_batch_servers.get("details", [])
],
)
When pod=None (the default, the most common call pattern) or when the
project's modality is non-tabular, control falls into the else branch
and Python raises UnboundLocalError.
Our workaround: lexsi_ds/agent/tools/predict.py always passes pod="small" so we hit the if branch and dodge the unbound variable. See the _DEFAULT_PREDICT_POD constant.
Suggested fix β move the fetch out of the conditional and only validate when the caller actually passed a pod:
if pod is not None:
custom_batch_servers = self.api_client.get(AVAILABLE_BATCH_SERVERS_URI)
available_servers = custom_batch_servers.get("details", [])
if self.metadata.get("modality") == "tabular":
available_servers = (
available_servers
+ custom_batch_servers.get("available_gpu_custom_servers", [])
)
Validate.value_against_list(
"pod", pod,
[server["instance_name"] for server in available_servers],
)
# else: pod is None, server uses its default, no validation needed
Three-line patch. Suggested PR title: fix(tabular): UnboundLocalError when model_inference is called without pod.
2. π ProjectConfig.pred_label=None crashes the server-side training pipeline
Severity: Bug. Every upload_data(config=ProjectConfig(...)) call with no pred_label lands here.
Files:
- Type declared with
Optional[str]inlexsi_sdk/common/types.py(class ProjectConfig). - SDK forwards
config.get("pred_label")as-is inlexsi_sdk/core/tabular.pyβupload_datapayload build.
Symptom seen from agent: Activity Log shows
Started building XGBoost_default model
Building Model
'pred_label'
Failed while running explainability. #05-013
or for foundation models:
Started building TabPFN_v3 model
Building Model
'pred_label'
Failed while building model. #05-001
The bare 'pred_label' log line is a Python KeyError/NoneType access being logged. The wrapping step labels the failure as #05-013 (XAI) or #05-001 (model build) depending on where the access happens.
Root cause: the server-side handler does direct subscript access like cfg["pred_label"] or attribute access without first checking for None. Even though the type is documented as Optional[str], the server can't actually accept None.
Our workaround: Set pred_label="Prediction" unconditionally in
_build_project_config (lexsi_ds/agent/tools/train_tabular_model.py). Combined with workaround #3 below, training succeeds.
Suggested fix: server-side handler should cfg.get("pred_label") and gate any column-name logic on a non-None value. If pred_label is genuinely required for the pipeline, change the docstring + TypedDict to remove Optional and make this a validation error at SDK level, not a server crash.
3. π§© No way to delete or update a saved project config (update_config is commented-out dead code)
Severity: Design β forces consumers to use one-project-per-run.
File: lexsi_sdk/core/project.py β def update_config exists but the entire body is commented out:
def update_config(self, compute_type: str, config: DataConfig) -> str:
# """Update the project configurations. Accepts a config dictionary.
# ...
# """
# if not config:
# raise Exception("Please upload config")
# ...
Symptom seen from agent: After the first successful upload_data(config=β¦), the project has a saved config. On the second call:
train_tabular_model: upload_data failed: Exception: Config already exists, please remove config
The SDK source confirms:
if project_config != "Not Found" and config:
raise Exception("Config already exists, please remove config")
β¦and there's no method to "remove" it. Documentation says to "update" but the implementation is dead.
Our workaround: Create a fresh per-run Lexsi project via Workspace.create_project(name="agent<task><runid>", modality="tabular", project_type=<task>). Every agent run gets a clean slate. See _get_or_create_run_project in train_tabular_model.py. This works but accumulates projects on the Lexsi side that the user manually cleans up β there's no Workspace.delete_project (only Project.delete_project, which works but isn't exposed in our cleanup flow yet).
Suggested fixes:
- Resurrect
update_configβ finish the implementation that's commented out. The endpoint clearly existed at some point. - OR add
Project.delete_configβ single-purpose, "wipe my saved config so I can upload a new one". Maps to whatever DELETE endpoint exists. - OR allow
upload_data(config=...)to replace an existing config when the caller passesforce=Trueor similar.
4. π Server-side runs XAI by default even when xai_method is absent / empty
Severity: Bug β or undocumented behavior, depending on intent.
Files: SDK side is correct β upload_data only includes explainability_method in the payload when truthy:
if config.get("xai_method"):
payload["metadata"]["explainability_method"] = config.get("xai_method")
So passing xai_method=[] or omitting the key should mean "don't run XAI." But:
Symptom seen from agent: We tried xai_method=[] and the server still ran XAI and crashed (#05-013 Failed while running explainability with could not convert string to float: 'class_0' in the Activity Log).
Hypothesis: the server-side training pipeline runs SHAP by default whenever a classification model is trained, regardless of what the upload metadata says. There's no platform-level "don't run XAI" flag we could find.
Our workaround: train_tabular_model.py's xai_method default is []. It's a no-op against the current server behavior but documents intent and is the right path forward if/when the server is fixed.
Suggested fixes:
- Make the server honor "no XAI requested" explicitly: when
explainability_methodis absent OR empty list, skip the XAI step. - Add an explicit
skip_xai: boolflag onProjectConfigfor unambiguous opt-out. - OR document the current behavior in the ProjectConfig docstring: "If absent or empty, server defaults to
['shap']for classification and['shap']for regression."
5. π§© xai_method=['lime'] "not available for classic models default version" β tier gate not surfaced upfront
Severity: Design β error message is reasonable but consumers can't discover the constraint via the SDK before making the call.
Symptom seen from agent:
Lime is not available for classic models default version.
when we tried xai_method=["lime"] with model_type="XGBoost".
The four valid XAI methods per lexsi_sdk/core/tabular.py validation: ["shap", "lime", "ig", "dlb"]. But LIME requires a higher Lexsi tier when used with classic ML (XGBoost, LGBoost, etc.), and IG / DLB are deep-learning-only. So in practice on the default tier, SHAP is the only working XAI for classic ML.
Our workaround: Default to xai_method=[] (skip training-time XAI entirely, see issue #6).
Suggested fixes:
- Per-tier capability endpoint:
LEXSI.available_xai_methods(model_type)β["shap"]on default,["shap", "lime"]on premium, etc. So SDK can validate before the round-trip. - Document the tier matrix in
xai_method's docstring onProjectConfig.
6. π TabPFN inference fails with CUDA-on-CPU mismatch (#05-004)
Severity: Bug β server-side or compute-pod-config issue.
Symptom seen from agent: After successfully training TabPFN on compute_type="T4.small" (GPU), predict fails with:
Model Inference Failed. #05-004
Activity Log details:
Attempting to deserialize object on a CUDA device but
torch.cuda.is_available() is False. If you are running on a CPU-only
machine, please use torch.load with map_location=torch.device('cpu')
to map your storages to the CPU.
Root cause: The TabPFN model checkpoint was saved with CUDA tensors. The inference pod Lexsi spun up doesn't have a GPU, but the loader does torch.load(..., map_location=DEFAULT) instead of map_location='cpu'. PyTorch refuses to deserialize.
Our workaround: None really. We currently pass pod="small" (CPU) to predict to dodge the UnboundLocalError from issue #1. The user is steered to use XGBoost rather than TabPFN until this is fixed.
Suggested fixes:
- Inference-side fix: when loading a model checkpoint, use
map_location='cpu' if not torch.cuda.is_available() else Noneso a CUDA-saved model can be loaded on a CPU pod. - Or: route TabPFN inference to a GPU pod automatically when the model was trained on one β
model_inferencecould look atmodels()['compute_type']and reject CPU pods for GPU-trained models with a clearer error.
7. π case_predict can't parse its own response for an unlabeled (prediction) case β CaseTabular.true_value is required
Severity: Bug β breaks explain_prediction entirely for prediction-tag cases (the common case: explaining a row whose outcome isn't known yet).
File: lexsi_sdk/core/tabular.py β the CaseTabular pydantic model. On lexsi-sdk==0.1.51:
CaseTabular.true_value: str | int # required=True, no default
CaseTabular.pred_value: str | int # required=True
CaseTabular.pred_category: str | int # required=True
Symptom seen from the repro script (case_predict(unique_identifier='5494', tag=<predict_tag>, model_name='TabICL_v1', xai=['shap']) on a tag with no ground-truth column):
FAIL case_predict in 36.5s. ValidationError: 2 validation errors for CaseTabular
true_value.str
Input should be a valid string [input_value=None, input_type=NoneType]
true_value.int
Input should be a valid integer [input_value=None, input_type=NoneType]
(It's one field, not two errors β pydantic reports both arms of the str | int union failing against None.)
Root cause: a case from a predict tag has no observed label, so the server legitimately returns true_value = null. But the response model types true_value as a required str | int, so pydantic raises while deserializing β case_predict blows up before it can return the CaseTabular. The agent's explain_prediction tool calls case_predict on exactly these unlabeled rows, so every case fails β all_cases_failed.
Our workaround (shipped): explain_prediction._relax_casetabular_validation() relaxes true_value / pred_value / pred_category to Optional[str | int] at runtime (pydantic-v2 model_fields patch + model_rebuild(force=True)) before calling case_predict, so unlabeled prediction cases parse. It's idempotent and best-effort (guarded: if the SDK internals change, the original error surfaces). Remove once the SDK ships the fix.
Suggested fix (SDK): make the label-dependent fields optional on the response model β
true_value: Optional[str | int] = None (and pred_value / pred_category, which are also null for unscored cases). A case without ground truth should parse with true_value=None, not raise.
Note (attribute surface β verified, no mismatch): CaseTabular does expose the fields explain_prediction.py reads β shap_feature_importance (Optional[Dict]), pred_value, pred_category, summary β plus methods xai_shap(), xai_summary(), xai_similar_cases(). (feature_importance(feature: str) -> float is a per-feature method, not the dict.) So once the validation bug above is worked around, the tool reads the right surface.