Skip to content

chore(llm-providers): Add env OPENAI_BASE_URL for openai ; unify llm provider configs#1577

Merged
rapids-bot[bot] merged 6 commits intoNVIDIA:developfrom
willkill07:wkk_add-env-configurable-oai-base_url
Feb 11, 2026
Merged

chore(llm-providers): Add env OPENAI_BASE_URL for openai ; unify llm provider configs#1577
rapids-bot[bot] merged 6 commits intoNVIDIA:developfrom
willkill07:wkk_add-env-configurable-oai-base_url

Conversation

@willkill07
Copy link
Member

@willkill07 willkill07 commented Feb 9, 2026

Description

Add this so we can override OPENAI_BASE_URL and OPENAI_API_KEY when testing in nightly CI.

Also update the logic for passthrough of OPENAI_API_KEY

Closes

By Submitting this PR I confirm:

  • I am familiar with the Contributing Guidelines.
  • We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license.
    • Any contribution which contains commits that are not Signed-Off will not be accepted.
  • When the PR is ready for review, new or existing tests cover these changes.
  • When the PR is ready for review, the documentation is up to date with these changes.

Summary by CodeRabbit

  • New Features

    • LLM plugins now accept API key and base URL from environment variables as fallbacks across providers.
    • Semantic Kernel integration now uses an asynchronous OpenAI client for improved async handling.
  • Refactor

    • Unified config construction and cleanup across multiple LLM integrations for more consistent instantiation.
  • Tests

    • Test suite updated to ensure API-key-related test setup is applied.

Signed-off-by: Will Killian <wkillian@nvidia.com>
@willkill07 willkill07 self-assigned this Feb 9, 2026
@willkill07 willkill07 added the improvement Improvement to existing functionality label Feb 9, 2026
@willkill07 willkill07 requested a review from a team as a code owner February 9, 2026 16:41
@willkill07 willkill07 added the non-breaking Non-breaking change label Feb 9, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

Walkthrough

Multiple LLM plugin modules now derive OpenAI api_key and base_url from llm_config (including secret values) or environment variables (OPENAI_API_KEY, OPENAI_BASE_URL). Many plugins construct a unified config_dict (excluding sensitive fields) before client instantiation. Semantic Kernel now creates and injects an async OpenAI client into OpenAIChatCompletion.

Changes

Cohort / File(s) Summary
ADK & Strands
packages/nvidia_nat_adk/src/nat/plugins/adk/llm.py, packages/nvidia_nat_strands/src/nat/plugins/strands/llm.py
Add os import; derive api_key/base_url from config or OPENAI_* env vars; include exclude_unset=True on model dumps and pass resolved values into client construction.
AGNO
packages/nvidia_nat_agno/src/nat/plugins/agno/llm.py
Import os and get_secret_value; omit api_key/base_url from initial model_dump and inject them from secret or env prior to client instantiation.
Autogen
packages/nvidia_nat_autogen/src/nat/plugins/autogen/llm.py
Import os; use exclude_unset/explicit config dicts; move api_key/base_url population to post-dump injection; refactor Bedrock/NIM flows to build explicit config objects and apply tool patches (e.g., remove strict).
Crewai / LangChain / Llama-Index
packages/nvidia_nat_crewai/.../llm.py, packages/nvidia_nat_langchain/.../llm.py, packages/nvidia_nat_llama_index/.../llm.py
Create config_dict via llm_config.model_dump(...) excluding sensitive fields; populate api_key/base_url from secrets or env; instantiate OpenAI/OpenAIResponses with **config_dict, preserving RESPONSES vs non-RESPONSES branches.
Semantic Kernel
packages/nvidia_nat_semantic_kernel/src/nat/plugins/semantic_kernel/llm.py, packages/nvidia_nat_semantic_kernel/tests/test_llm_sk.py
Resolve api credentials from config/env; create AsyncOpenAI in an async context and pass it as async_client to OpenAIChatCompletion(ai_model_id=..., async_client=...). Tests: add class-level fixture set_test_api_keys.
Other minor adjustments
...
Small import additions (os, get_secret_value) and consistent use of exclude_unset=True and centralized config assembly across multiple LLM plugin modules.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant App as App/Caller
participant SK as SemanticKernel
participant AsyncClient as AsyncOpenAI
participant SKClient as OpenAIChatCompletion
participant OpenAIAPI as OpenAI Service

App->>SK: request LLM operation
SK->>AsyncClient: create AsyncOpenAI(api_key, base_url)
SK->>SKClient: instantiate OpenAIChatCompletion(ai_model_id, async_client)
SKClient->>AsyncClient: send async request
AsyncClient->>OpenAIAPI: HTTP call (async)
OpenAIAPI-->>AsyncClient: async response
AsyncClient-->>SKClient: return result
SK-->>App: deliver response

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)
Check name Status Explanation Resolution
Title check ❌ Error Title exceeds the 72 character limit at 89 characters; it is also overly specific about one environment variable while understating the comprehensive unification of LLM provider configurations across multiple packages. Revise the title to be concise and under 72 characters, such as 'chore(llm-providers): Unify LLM provider configurations with env overrides' or 'chore(llm-providers): Add env overrides for OpenAI config'.
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉


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.

Signed-off-by: Will Killian <wkillian@nvidia.com>
Copy link
Contributor

@mnajafian-nv mnajafian-nv left a comment

Choose a reason for hiding this comment

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

Great work!

You mentioned you're open to suggestions for Semantic Kernel. Here's how I'd extend the same pattern there.

Based on my understanding the main difference is that unlike LangChain/LlamaIndex which accept base_url directly, Semantic Kernel's OpenAIChatCompletion needs a custom AsyncOpenAI client to handle non-default endpoints.

Suggestions:

File: packages/nvidia_nat_semantic_kernel/src/nat/plugins/semantic_kernel/llm.py

Add these imports at the top:

import os

from typing import TypeVar

from openai import AsyncOpenAI

from nat.builder.builder import Builder

Update the openai_semantic_kernel function:

@register_llm_client(config_type=OpenAIModelConfig, wrapper_type=LLMFrameworkEnum.SEMANTIC_KERNEL)
async def openai_semantic_kernel(llm_config: OpenAIModelConfig, _builder: Builder):

    from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion

    validate_no_responses_api(llm_config, LLMFrameworkEnum.SEMANTIC_KERNEL)

    # Determine base_url (config takes precedence, env var as fallback)
    base_url = llm_config.base_url
    if not base_url:
        base_url = os.getenv("OPENAI_BASE_URL")

    # Create custom AsyncOpenAI client if base_url is specified
    # This follows the official Semantic Kernel pattern for custom endpoints
    async_client = None
    if base_url:
        api_key = get_secret_value(llm_config.api_key) or os.getenv("OPENAI_API_KEY")
        async_client = AsyncOpenAI(
            api_key=api_key,
            base_url=base_url,
        )

    llm = OpenAIChatCompletion(
        ai_model_id=llm_config.model_name,
        async_client=async_client,
    )

    yield _patch_llm_based_on_config(llm, llm_config)

This approach:
• Matches Semantic Kernel's official pattern (https://github.com/microsoft/semantic-kernel/blob/main/python/samples/concepts/setup/chat_completion_services.py) used for DeepSeek/OpenRouter
• Uses the same fallback logic as your Strands implementation, but cleaner with a single os.getenv() call
• Non-breaking when base_url isn't set, it passes None and uses default behavior

Happy to open a PR with this if that's easier, or feel free to just copy it. Either way works!

Copy link

@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: 1

🤖 Fix all issues with AI agents
In `@packages/nvidia_nat_semantic_kernel/src/nat/plugins/semantic_kernel/llm.py`:
- Around line 115-120: The AsyncOpenAI instance (async_client) is not closed,
risking an httpx AsyncClient leak; wrap the generator's yield in a try/finally:
create async_client = AsyncOpenAI(...), construct llm =
OpenAIChatCompletion(...), then do try: yield llm finally: await
async_client.aclose() (or call the appropriate close method on AsyncOpenAI if
named differently) to ensure the underlying httpx.AsyncClient is closed when the
generator finishes.
🧹 Nitpick comments (2)
packages/nvidia_nat_llama_index/src/nat/plugins/llama_index/llm.py (1)

134-144: base_url not excluded from model_dump — inconsistent with sibling plugins.

In the agno plugin, base_url is excluded from model_dump (line 110 of agno/llm.py) and then conditionally re-added. Here, base_url is left in the dump and potentially overwritten on line 144. This works but is inconsistent and could silently double-set the key.

Consider excluding base_url from the model_dump call to match the pattern used in the other plugins changed in this PR:

Proposed fix
     config_dict = llm_config.model_dump(
-        exclude={"type", "thinking", "api_type", "api_key"},
+        exclude={"type", "thinking", "api_type", "api_key", "base_url"},
         by_alias=True,
         exclude_none=True,
         exclude_unset=True,
     )
packages/nvidia_nat_semantic_kernel/src/nat/plugins/semantic_kernel/llm.py (1)

115-116: Use imported get_secret_value() utility for consistency and null safety.

Line 115 calls .get_secret_value() directly on the SecretStr field, but the same file imports the get_secret_value utility from nat.data_models.common (line 22) and uses it elsewhere in the file. The utility safely handles None values, whereas direct method calls risk AttributeError if the field is None. Use the imported helper for consistency:

-    api_key = llm_config.api_key.get_secret_value() or os.getenv("OPENAI_API_KEY")
+    api_key = get_secret_value(llm_config.api_key) or os.getenv("OPENAI_API_KEY")

This also applies to the agno and llama_index plugins, which exhibit the same pattern.

@willkill07 willkill07 force-pushed the wkk_add-env-configurable-oai-base_url branch 2 times, most recently from dc387e4 to d87d40a Compare February 10, 2026 15:42
Copy link

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/nvidia_nat_adk/src/nat/plugins/adk/llm.py (1)

118-128: ⚠️ Potential issue | 🟡 Minor

api_key should be excluded from model_dump for consistency with other plugins.

The other plugin files in this PR (llama_index, agno, etc.) explicitly exclude "api_key" from model_dump before conditionally re-adding it. Here, api_key is not excluded, so when it's set on the config, it will appear in config_dict as a SecretStr object from the dump and then be overwritten by line 126 with the plain string. While the overwrite masks the issue, passing a SecretStr to LiteLlm (if the overwrite branch isn't reached) could cause problems.

Proposed fix
     config_dict = config.model_dump(
-        exclude={"type", "max_retries", "thinking", "model_name", "model", "base_url", "api_type"},
+        exclude={"type", "max_retries", "thinking", "model_name", "model", "base_url", "api_key", "api_type"},
         by_alias=True,
         exclude_none=True,
         exclude_unset=True,
     )
🤖 Fix all issues with AI agents
In `@packages/nvidia_nat_llama_index/src/nat/plugins/llama_index/llm.py`:
- Around line 134-149: The config_dict created by llm_config.model_dump
currently does not exclude "base_url", which can lead to duplicate/stale values;
update the call to llm_config.model_dump (the model_dump call that builds
config_dict) to include "base_url" in its exclude set so base_url is omitted
from the dump, then keep the existing conditional that sets
config_dict["base_url"] from llm_config.base_url or
os.getenv("OPENAI_BASE_URL"); this change affects the symbols config_dict,
llm_config.model_dump, and the subsequent conditional that assigns
config_dict["base_url"] before instantiating OpenAI or OpenAIResponses.

In `@packages/nvidia_nat_semantic_kernel/src/nat/plugins/semantic_kernel/llm.py`:
- Around line 115-118: The code calls llm_config.api_key.get_secret_value()
unconditionally which can raise AttributeError if llm_config.api_key is None;
update the initialization for api_key to first check if llm_config.api_key is
truthy (e.g., if llm_config.api_key) and only then call get_secret_value(),
otherwise fall back to os.getenv("OPENAI_API_KEY"); keep base_url logic the same
and pass the resolved api_key into AsyncOpenAI(api_key=api_key,
base_url=base_url).

Signed-off-by: Will Killian <wkillian@nvidia.com>
@willkill07 willkill07 force-pushed the wkk_add-env-configurable-oai-base_url branch from d87d40a to ddbef53 Compare February 10, 2026 15:43
Signed-off-by: Will Killian <wkillian@nvidia.com>
@willkill07 willkill07 changed the title chore(llm-providers): Add env config for OPENAI_BASE_URL for openai chore(llm-providers): Align env OPENAI_BASE_URL for openai ; align llm provider expansions Feb 10, 2026
Copy link

@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: 2

🤖 Fix all issues with AI agents
In `@packages/nvidia_nat_llama_index/src/nat/plugins/llama_index/llm.py`:
- Around line 142-145: Update the env-var lookups to prefer NAT_-prefixed names
while retaining OPENAI_* as fallback: when building config_dict use
get_secret_value(llm_config.api_key) or os.getenv("NAT_OPENAI_API_KEY") (fall
back to os.getenv("OPENAI_API_KEY") if needed) to set "api_key", and use
llm_config.base_url or os.getenv("NAT_OPENAI_BASE_URL") (fall back to
os.getenv("OPENAI_BASE_URL")) to set "base_url"; keep the same keys in
config_dict and the same conditional structure around get_secret_value and
llm_config to minimize changes.

In `@packages/nvidia_nat_semantic_kernel/src/nat/plugins/semantic_kernel/llm.py`:
- Around line 115-116: Update the environment variable lookups in the LLM
initialization so they use the NAT_ prefix first and fall back to the existing
OPENAI_* names: when resolving the API key
(get_secret_value(llm_config.api_key)) and base URL (llm_config.base_url) prefer
NAT_OPENAI_API_KEY and NAT_OPENAI_BASE_URL, respectively, and only if those are
not set fall back to OPENAI_API_KEY / OPENAI_BASE_URL; adjust the calls around
get_secret_value(llm_config.api_key) and the base_url assignment to reflect this
lookup order.

Copy link

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/nvidia_nat_autogen/src/nat/plugins/autogen/llm.py (1)

322-335: 🛠️ Refactor suggestion | 🟠 Major

api_key / base_url not excluded from model_dump — inconsistent with openai_autogen.

In openai_autogen (line 150), api_key and base_url are explicitly excluded from model_dump before being conditionally re-added from config/env. Here, nim_autogen relies on exclude_unset=True to keep them out when not explicitly set, but if they are set, the raw Pydantic field values (potentially SecretStr objects) will land in config_obj alongside (or before) the overwrite on lines 334–335.

When get_secret_value(llm_config.api_key) and the env var are both falsy (e.g., key is SecretStr("")), the if on line 334 is false, and the SecretStr object from model_dump remains in config_obj — which is unlikely to be accepted as a plain string by OpenAIChatCompletionClient.

Consider aligning with the openai_autogen pattern:

Proposed fix
     config_obj = {
         **llm_config.model_dump(
-            exclude={"type", "model_name", "thinking"},
+            exclude={"type", "model_name", "thinking", "api_key", "base_url"},
             by_alias=True,
             exclude_none=True,
             exclude_unset=True,
         ),
     }
 
-    if llm_config.base_url is None:
-        config_obj["base_url"] = "https://integrate.api.nvidia.com/v1"
+    config_obj["base_url"] = llm_config.base_url or "https://integrate.api.nvidia.com/v1"
     if (api_key := get_secret_value(llm_config.api_key) or os.getenv("NVIDIA_API_KEY")):
         config_obj["api_key"] = api_key
🧹 Nitpick comments (1)
packages/nvidia_nat_autogen/src/nat/plugins/autogen/llm.py (1)

199-211: Azure path passes raw llm_config.api_key (likely SecretStr) — pre-existing but worth noting.

Line 201 passes llm_config.api_key directly (a SecretStr / OptionalSecretStr) rather than resolving it via get_secret_value(). This is not a regression from this PR, but it's inconsistent with the new openai_autogen pattern and could cause issues if AzureOpenAIChatCompletionClient doesn't handle SecretStr. Consider harmonizing in a follow-up.

@willkill07 willkill07 changed the title chore(llm-providers): Align env OPENAI_BASE_URL for openai ; align llm provider expansions chore(llm-providers): Add env OPENAI_BASE_URL for openai ; unify llm provider configs Feb 11, 2026
Signed-off-by: Will Killian <wkillian@nvidia.com>
@willkill07
Copy link
Member Author

/merge

@rapids-bot rapids-bot bot merged commit b4f24ab into NVIDIA:develop Feb 11, 2026
17 checks passed
pastorsj pushed a commit to pastorsj/NeMo-Agent-Toolkit that referenced this pull request Feb 13, 2026
…llm provider configs (NVIDIA#1577)

Add this so we can override `OPENAI_BASE_URL` and `OPENAI_API_KEY` when testing in nightly CI.

Also update the logic for passthrough of `OPENAI_API_KEY`

Closes

## By Submitting this PR I confirm:
- I am familiar with the [Contributing Guidelines](https://github.com/NVIDIA/NeMo-Agent-Toolkit/blob/develop/docs/source/resources/contributing/index.md).
- We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license.
  - Any contribution which contains commits that are not Signed-Off will not be accepted.
- When the PR is ready for review, new or existing tests cover these changes.
- When the PR is ready for review, the documentation is up to date with these changes.

## Summary by CodeRabbit

* **New Features**
  * LLM plugins now accept API key and base URL from environment variables as fallbacks across providers.
  * Semantic Kernel integration now uses an asynchronous OpenAI client for improved async handling.

* **Refactor**
  * Unified config construction and cleanup across multiple LLM integrations for more consistent instantiation.

* **Tests**
  * Test suite updated to ensure API-key-related test setup is applied.

Authors:
  - Will Killian (https://github.com/willkill07)

Approvers:
  - https://github.com/mnajafian-nv

URL: NVIDIA#1577
Signed-off-by: Sam Pastoriza <spastoriza@nvidia.com>
@willkill07 willkill07 deleted the wkk_add-env-configurable-oai-base_url branch February 25, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improvement to existing functionality non-breaking Non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants