Skip to content

AzureOpenAIStructuredLLM: constructor crashes on None/dict/BaseLlmConfig (AttributeError/TypeError) because azure_kwargs is accessed without AzureOpenAIConfig conversion #7428

Open

Description

@BlueX888

Component

Python SDK

Summary

AzureOpenAIStructuredLLM.__init__ crashes on every config form except an already-built AzureOpenAIConfig: the class's own declared default (config: Optional[BaseLlmConfig] = None), any dict config, and any plain BaseLlmConfig all raise — AttributeError: 'BaseLlmConfig' object has no attribute 'azure_kwargs' (for dicts containing the documented azure_kwargs key: TypeError: BaseLlmConfig.__init__() got an unexpected keyword argument 'azure_kwargs').

The constructor passes the raw config to super().__init__() (mem0/llms/azure_openai_structured.py:18), where LLMBase builds a plain BaseLlmConfig from None/dict (mem0/llms/base.py:19-25), and then immediately reads self.config.azure_kwargs (mem0/llms/azure_openai_structured.py:24) — an attribute only AzureOpenAIConfig has. The sibling class AzureOpenAILLM converts None/dict/BaseLlmConfig to AzureOpenAIConfig before calling super() (mem0/llms/azure_openai.py:20-38), so the exact same inputs work there.

Steps to Reproduce

No Azure credentials or network needed — every crash happens before the AzureOpenAI client is built (the api_key="k" value also steers away from the DefaultAzureCredential branch).

from mem0.configs.llms.base import BaseLlmConfig
from mem0.llms.azure_openai import AzureOpenAILLM
from mem0.llms.azure_openai_structured import AzureOpenAIStructuredLLM

AZURE_DICT = {
    "model": "gpt-4o",
    "api_key": "k",
    "azure_kwargs": {
        "azure_deployment": "dep",
        "azure_endpoint": "https://x.openai.azure.com",
        "api_version": "2024-10-21",
    },
}

for label, arg in [
    ("no-arg (declared default None)", None),
    ("dict with azure_kwargs", AZURE_DICT),
    ("dict without azure_kwargs", {"model": "gpt-4o", "api_key": "k"}),
    ("plain BaseLlmConfig", BaseLlmConfig(model="gpt-4o", api_key="k")),
]:
    try:
        AzureOpenAIStructuredLLM(arg)
        print(f"AzureOpenAIStructuredLLM({label}): OK")
    except Exception as e:
        print(f"AzureOpenAIStructuredLLM({label}): CRASH: {type(e).__name__}: {e}")

# sibling contrast: same dict works for AzureOpenAILLM
try:
    AzureOpenAILLM(AZURE_DICT)
    print("AzureOpenAILLM(dict with azure_kwargs): OK")
except Exception as e:
    print(f"AzureOpenAILLM(dict with azure_kwargs): CRASH: {type(e).__name__}: {e}")

Expected Behavior

AzureOpenAIStructuredLLM should accept the config forms its own signature and its base class declare, converting them to AzureOpenAIConfig the way the sibling AzureOpenAILLM does:

  • AzureOpenAIStructuredLLM() — the declared default config: Optional[BaseLlmConfig] = None (mem0/llms/azure_openai_structured.py:17) should not crash on its own default.
  • dict configs — LLMBase.__init__ documents and implements dict configs: "Handle dict-based configuration (backward compatibility): self.config = BaseLlmConfig(**config)" with the docstring "config: LLM configuration option class or dict, defaults to None" (mem0/llms/base.py:16-23). AzureOpenAIConfig itself is a dict-friendly dataclass carrying azure_kwargs, and azure_kwargs is the documented key for Azure options.
  • plain BaseLlmConfig — accepted by the declared signature's type annotation.

Concrete basis: the sibling class in the same provider family, AzureOpenAILLM.__init__ (mem0/llms/azure_openai.py:20-38):

if config is None:
    config = AzureOpenAIConfig()
elif isinstance(config, dict):
    config = AzureOpenAIConfig(**config)
elif isinstance(config, BaseLlmConfig) and not isinstance(config, AzureOpenAIConfig):
    config = AzureOpenAIConfig(model=..., temperature=..., ...)

With that conversion in place, the identical dict constructs AzureOpenAILLM successfully (verified below).

Actual Behavior

Verifier 1:

AzureOpenAIStructuredLLM(no-arg): CRASH AttributeError: 'BaseLlmConfig' object has no attribute 'azure_kwargs'  [azure_openai_structured.py:24 in __init__]
AzureOpenAIStructuredLLM(dict with azure_kwargs): CRASH TypeError: BaseLlmConfig.__init__() got an unexpected keyword argument 'azure_kwargs'
AzureOpenAIStructuredLLM(dict without azure_kwargs): CRASH AttributeError: 'BaseLlmConfig' object has no attribute 'azure_kwargs'
AzureOpenAIStructuredLLM(plain BaseLlmConfig): CRASH AttributeError: 'BaseLlmConfig' object has no attribute 'azure_kwargs'
AzureOpenAILLM(dict with azure_kwargs): OK

Verifier 2:

AzureOpenAIStructuredLLM(no-arg (declared default None)): CRASH: AttributeError: 'BaseLlmConfig' object has no attribute 'azure_kwargs'
AzureOpenAIStructuredLLM(dict with azure_kwargs): CRASH: TypeError: BaseLlmConfig.__init__() got an unexpected keyword argument 'azure_kwargs'
AzureOpenAIStructuredLLM(plain BaseLlmConfig): CRASH: AttributeError: 'BaseLlmConfig' object has no attribute 'azure_kwargs'
AzureOpenAILLM(dict with azure_kwargs): OK
AzureOpenAIStructuredLLM(full AzureOpenAIConfig): OK, model = gpt-4o

Re-run on c7ee362aff94a369af70f13f2b4f853f6793ff4c before filing — same result:

AzureOpenAIStructuredLLM(no-arg (declared default None)): CRASH: AttributeError: 'BaseLlmConfig' object has no attribute 'azure_kwargs'
AzureOpenAIStructuredLLM(dict with azure_kwargs): CRASH: TypeError: BaseLlmConfig.__init__() got an unexpected keyword argument 'azure_kwargs'
AzureOpenAIStructuredLLM(dict without azure_kwargs): CRASH: AttributeError: 'BaseLlmConfig' object has no attribute 'azure_kwargs'
AzureOpenAIStructuredLLM(plain BaseLlmConfig): CRASH: AttributeError: 'BaseLlmConfig' object has no attribute 'azure_kwargs'
AzureOpenAILLM(dict with azure_kwargs): OK

Tracebacks (same re-run) — the two distinct failure points:

File "mem0/llms/azure_openai_structured.py", line 24, in __init__
    api_key = self.config.azure_kwargs.api_key or os.getenv("LLM_AZURE_OPENAI_API_KEY")
              ^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'BaseLlmConfig' object has no attribute 'azure_kwargs'
File "mem0/llms/base.py", line 23, in __init__
    self.config = BaseLlmConfig(**config)
                  ^^^^^^^^^^^^^^^^^^^^^^^
TypeError: BaseLlmConfig.__init__() got an unexpected keyword argument 'azure_kwargs'

Only passing an already-built AzureOpenAIConfig works.

Environment

  • mem0 version: 2.0.20 (pyproject.toml), commit c7ee362aff94a369af70f13f2b4f853f6793ff4c
  • Python 3.12.14
  • macOS, Darwin 25.6.0
  • No credentials / offline — the crash precedes any client construction or network call

Root Cause

mem0/llms/azure_openai_structured.py:17-24 — __init__ forwards the raw config to super().__init__(config) without converting it to AzureOpenAIConfig. LLMBase.__init__ then turns None/dict into a plain BaseLlmConfig (mem0/llms/base.py:19-25), which has no azure_kwargs attribute, so the first access at line 24 raises AttributeError; a dict that does contain azure_kwargs instead fails earlier inside super() with TypeError, because BaseLlmConfig(**config) does not accept that keyword. The sibling AzureOpenAILLM (mem0/llms/azure_openai.py:20-38) does the conversion first and is unaffected.

How You Verified This

What I Ran

The script in Steps to Reproduce against a checkout of c7ee362aff94a369af70f13f2b4f853f6793ff4c (editable install, Python 3.12.14, azure-identity installed for the module imports), offline. Independently reproduced by two verifiers on the same commit with the same script.

What I Saw

The verbatim outputs in Actual Behavior: all four documented/declared config forms crash AzureOpenAIStructuredLLM.__init__ (AttributeError at azure_openai_structured.py:24, or TypeError at base.py:23 for a dict carrying azure_kwargs), while the identical dict constructs AzureOpenAILLM fine and a full AzureOpenAIConfig constructs AzureOpenAIStructuredLLM fine.

Why This Is a Bug

The constructor violates its own declared contract: the signature is config: Optional[BaseLlmConfig] = None (azure_openai_structured.py:17), so AzureOpenAIStructuredLLM() must work, and LLMBase explicitly documents and supports dict configs "backward compatibility" (base.py:16-23). The direct fix already exists in the same provider family — AzureOpenAILLM.__init__ (azure_openai.py:20-38) converts None → AzureOpenAIConfig(), dict → AzureOpenAIConfig(**config), and plain BaseLlmConfig → AzureOpenAIConfig(...); AzureOpenAIStructuredLLM is the only Azure class missing that conversion. The class is a registered provider ("azure_openai_structured": (..., AzureOpenAIConfig) at mem0/utils/factory.py:52), so anyone instantiating it directly — the documented pattern for the LLM classes — hits the crash.

What I Ruled Out

  • The Memory/factory path — LlmFactory.create pre-builds the provider config class before instantiating (mem0/utils/factory.py:80-88), so from_config-style usage works; only direct construction with the class's own declared signature crashes.
  • Missing environment variables — the os.getenv fallbacks at azure_openai_structured.py:24-27 are only reached after the self.config.azure_kwargs access; no env setting can avoid the AttributeError.
  • Test coverage — tests/llms/test_azure_openai_structured.py only passes a DummyConfig duck-type that happens to carry an azure_kwargs attribute (tests/llms/test_azure_openai_structured.py:26-49), so None/dict/BaseLlmConfig inputs are never exercised.
  • PR fix(llms): honor reasoning-model params in AzureOpenAIStructuredLLM #5548 (merged, touches this file) — fixes reasoning-model params in generate_response; different function, different defect.

Related

Searched issues/PRs for azure_kwargs, AzureOpenAIStructuredLLM, azure_openai_structured: nothing covers this constructor crash. Nearest, none duplicates: #4164 (closed; AzureOpenAIStructuredLLM missing _parse_response method — different defect), merged PR #5548 (same file, generate_response reasoning-model params — different function), PR #2914 (closed; original structured Azure LLM addition).

Happy to open a PR mirroring the sibling conversion from azure_openai.py:20-38 at the top of __init__ (None → AzureOpenAIConfig(), dict → AzureOpenAIConfig(**config), plain BaseLlmConfig → AzureOpenAIConfig(...)) plus tests for the three currently-crashing forms — say the word (per CONTRIBUTING I'll wait for the accepted label first).

AI Assistance

AI helped me find it, and I reproduced it myself afterwards (the re-run in Actual Behavior is my own; two independent verifiers reproduced it on the same commit before filing).

Activity

  1. BlueX888 commented on Sep 23, 2026

    @BlueX888
    Author

    Reproduced this. Root cause: mem0/llms/azure_openai_structured.py:18 passes the raw config to super().init(), so None/dict inputs become a plain BaseLlmConfig (mem0/llms/base.py:19-25), and line 24 then reads self.config.azure_kwargs — an attribute only AzureOpenAIConfig has; the sibling AzureOpenAILLM converts first (mem0/llms/azure_openai.py:20-38).

    Minimal reproduction:

    python -c "from mem0.llms.azure_openai_structured import AzureOpenAIStructuredLLM; AzureOpenAIStructuredLLM()"
    

    Observed output:

    AttributeError: 'BaseLlmConfig' object has no attribute 'azure_kwargs'
    

    Happy to open a PR with this fix, or to be assigned if you prefer to take it.

  2. chenhz01 commented on Oct 1, 2026

    @chenhz01

    Hi @BlueX888 — the crash is that AzureOpenAIStructuredLLM.__init__ reads self.config.azure_kwargs right after super().__init__(), but the base builds a plain BaseLlmConfig for None/dict inputs, which has no azure_kwargs. The sibling AzureOpenAILLM converts None/dict/BaseLlmConfig to AzureOpenAIConfig first — exactly what's missing here. The fix is to normalize the config to AzureOpenAIConfig before touching provider-specific attributes.

    This exact fix pattern — auditing the config/credential path the constructor will touch before the client is built — is what I'd reach for; here's a working reference implementation from a drop-in fix library I maintain (published, runnable):

    # Audit which credential fields _safe_deepcopy_config would blank before a
    # VectorStoreFactory.create() call. Plug in your own config dict.
    SENSITIVE_EXACT = {"api_key", "password", "token", "secret"}
    
    def is_sensitive(field: str) -> bool:
        return field in SENSITIVE_EXACT or field.endswith("_credential")
    
    def audit_config(cfg: dict) -> list:
        """Return credential fields that would be blanked on the clone."""
        at_risk = []
        for k, v in cfg.items():
            if v is not None and is_sensitive(k):
                at_risk.append(k)
        return at_risk
    
    if __name__ == "__main__":
        es_cfg = {"api_key": "AK...", "host": "https://es:9200"}
        os_cfg = {"user": "u", "password": "p", "host": "https://os:9200"}
        print("elasticsearch:", audit_config(es_cfg))   # ['api_key']
        print("opensearch:  ", audit_config(os_cfg))    # ['password']
        # Any field listed here arrives at VectorStoreFactory as None.

    Ref: https://github.com/chenhz01/llm_agent_dropin_fixes/blob/main/src/llm_agent_dropin_fixes/state_persist/mem0_cred_audit.py

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions