From 9451269d2a8d50a58a92bef878aeb2b69b98e080 Mon Sep 17 00:00:00 2001 From: Gaurav Singh Date: Wed, 8 Apr 2026 12:16:17 +0530 Subject: [PATCH 1/4] feat(inference): add Azure OpenAI as a first-class inference provider Azure OpenAI uses the OpenAI-compatible API behind per-customer endpoint URLs. This adds it alongside the existing providers in: - REMOTE_PROVIDER_CONFIG (onboard menu, credential prompting) - getProviderSelectionConfig (inference routing) - getSandboxInferenceConfig (Dockerfile patching) - openclaw-sandbox.yaml (network policy for *.openai.azure.com) - printDashboard (provider label display) Users select "Azure OpenAI" during onboarding, supply their endpoint URL and API key, then choose a deployment model name. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/lib/onboard.js | 43 +++++++++++++++- .../policies/openclaw-sandbox.yaml | 20 ++++++++ src/lib/inference-config.test.ts | 16 +++++- src/lib/inference-config.ts | 7 +++ test/onboard-selection.test.js | 50 +++++++++---------- 5 files changed, 108 insertions(+), 28 deletions(-) diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 7ad960a7f..8b07469e2 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -161,6 +161,17 @@ const REMOTE_PROVIDER_CONFIG = { defaultModel: "gemini-2.5-flash", skipVerify: true, }, + azureOpenAi: { + label: "Azure OpenAI", + providerName: "azure-openai", + providerType: "openai", + credentialEnv: "AZURE_OPENAI_API_KEY", + endpointUrl: "", + helpUrl: "https://portal.azure.com/", + modelMode: "input", + defaultModel: "gpt-4o", + skipVerify: true, + }, custom: { label: "Other OpenAI-compatible endpoint", providerName: "compatible-endpoint", @@ -865,6 +876,7 @@ function getSandboxInferenceConfig(model, provider = null, preferredInferenceApi switch (provider) { case "openai-api": + case "azure-openai": providerKey = "openai"; primaryModelRef = `openai/${model}`; break; @@ -2442,6 +2454,7 @@ async function setupNim(gpu) { const options = []; options.push({ key: "build", label: "NVIDIA Endpoints" }); options.push({ key: "openai", label: "OpenAI" }); + options.push({ key: "azureOpenAi", label: "Azure OpenAI" }); options.push({ key: "custom", label: "Other OpenAI-compatible endpoint" }); options.push({ key: "anthropic", label: "Anthropic" }); options.push({ key: "anthropicCompatible", label: "Other Anthropic-compatible endpoint" }); @@ -2513,7 +2526,31 @@ async function setupNim(gpu) { endpointUrl = remoteConfig.endpointUrl; preferredInferenceApi = null; - if (selected.key === "custom") { + if (selected.key === "azureOpenAi") { + const endpointInput = isNonInteractive() + ? (process.env.NEMOCLAW_ENDPOINT_URL || "").trim() + : await prompt( + " Azure OpenAI endpoint URL (e.g., https://my-resource.openai.azure.com/v1): ", + ); + const navigation = getNavigationChoice(endpointInput); + if (navigation === "back") { + console.log(" Returning to provider selection."); + console.log(""); + continue selectionLoop; + } + if (navigation === "exit") { + exitOnboardFromPrompt(); + } + endpointUrl = normalizeProviderBaseUrl(endpointInput, "openai"); + if (!endpointUrl) { + console.error(" Endpoint URL is required for Azure OpenAI."); + if (isNonInteractive()) { + process.exit(1); + } + console.log(""); + continue selectionLoop; + } + } else if (selected.key === "custom") { const endpointInput = isNonInteractive() ? (process.env.NEMOCLAW_ENDPOINT_URL || "").trim() : await prompt(" OpenAI-compatible base URL (e.g., https://openrouter.ai/api/v1): "); @@ -2637,7 +2674,7 @@ async function setupNim(gpu) { continue selectionLoop; } - if (selected.key === "custom") { + if (selected.key === "azureOpenAi" || selected.key === "custom") { const validation = await validateCustomOpenAiLikeSelection( remoteConfig.label, endpointUrl, @@ -3029,6 +3066,7 @@ async function setupInference( provider === "nvidia-prod" || provider === "nvidia-nim" || provider === "openai-api" || + provider === "azure-openai" || provider === "anthropic-prod" || provider === "compatible-anthropic-endpoint" || provider === "gemini-api" || @@ -3843,6 +3881,7 @@ function printDashboard(sandboxName, model, provider, nimContainer = null) { let providerLabel = provider; if (provider === "nvidia-prod" || provider === "nvidia-nim") providerLabel = "NVIDIA Endpoints"; else if (provider === "openai-api") providerLabel = "OpenAI"; + else if (provider === "azure-openai") providerLabel = "Azure OpenAI"; else if (provider === "anthropic-prod") providerLabel = "Anthropic"; else if (provider === "compatible-anthropic-endpoint") providerLabel = "Other Anthropic-compatible endpoint"; diff --git a/nemoclaw-blueprint/policies/openclaw-sandbox.yaml b/nemoclaw-blueprint/policies/openclaw-sandbox.yaml index d54ffd703..0259ebe1a 100644 --- a/nemoclaw-blueprint/policies/openclaw-sandbox.yaml +++ b/nemoclaw-blueprint/policies/openclaw-sandbox.yaml @@ -104,6 +104,26 @@ network_policies: - { path: /usr/local/bin/claude } - { path: /usr/local/bin/openclaw } + azure_openai: + name: azure_openai + endpoints: + - host: "*.openai.azure.com" + port: 443 + protocol: rest + enforcement: enforce + tls: terminate + rules: + - allow: { method: POST, path: "/openai/deployments/*/chat/completions" } + - allow: { method: POST, path: "/openai/deployments/*/completions" } + - allow: { method: POST, path: "/openai/deployments/*/embeddings" } + - allow: { method: GET, path: "/openai/deployments" } + - allow: { method: GET, path: "/openai/deployments/**" } + - allow: { method: GET, path: "/openai/models" } + - allow: { method: GET, path: "/openai/models/**" } + binaries: + - { path: /usr/local/bin/claude } + - { path: /usr/local/bin/openclaw } + github: name: github endpoints: diff --git a/src/lib/inference-config.test.ts b/src/lib/inference-config.test.ts index 81dc9d044..8b836467c 100644 --- a/src/lib/inference-config.test.ts +++ b/src/lib/inference-config.test.ts @@ -68,6 +68,19 @@ describe("inference selection config", () => { }); }); + it("maps azure-openai to the sandbox inference route", () => { + expect(getProviderSelectionConfig("azure-openai", "gpt-4o")).toEqual({ + endpointType: "custom", + endpointUrl: INFERENCE_ROUTE_URL, + ncpPartner: null, + model: "gpt-4o", + profile: DEFAULT_ROUTE_PROFILE, + credentialEnv: "AZURE_OPENAI_API_KEY", + provider: "azure-openai", + providerLabel: "Azure OpenAI", + }); + }); + it("maps the remaining hosted providers to the sandbox inference route", () => { // Full-object assertion for one hosted provider to catch structural regressions expect(getProviderSelectionConfig("openai-api", "gpt-5.4-mini")).toEqual({ @@ -114,6 +127,7 @@ describe("inference selection config", () => { "nvidia-prod", "nvidia-nim", "openai-api", + "azure-openai", "anthropic-prod", "compatible-anthropic-endpoint", "gemini-api", @@ -128,7 +142,6 @@ describe("inference selection config", () => { "bedrock", "vertex", "azure", - "azure-openai", "deepseek", "mistral", "cohere", @@ -147,6 +160,7 @@ describe("inference selection config", () => { it("falls back to provider defaults when model is omitted", () => { expect(getProviderSelectionConfig("openai-api")?.model).toBe("gpt-5.4"); + expect(getProviderSelectionConfig("azure-openai")?.model).toBe("gpt-4o"); expect(getProviderSelectionConfig("anthropic-prod")?.model).toBe("claude-sonnet-4-6"); expect(getProviderSelectionConfig("gemini-api")?.model).toBe("gemini-2.5-flash"); expect(getProviderSelectionConfig("compatible-endpoint")?.model).toBe("custom-model"); diff --git a/src/lib/inference-config.ts b/src/lib/inference-config.ts index c300f1982..db62cc562 100644 --- a/src/lib/inference-config.ts +++ b/src/lib/inference-config.ts @@ -87,6 +87,13 @@ export function getProviderSelectionConfig( credentialEnv: "GEMINI_API_KEY", providerLabel: "Google Gemini", }; + case "azure-openai": + return { + ...base, + model: model || "gpt-4o", + credentialEnv: "AZURE_OPENAI_API_KEY", + providerLabel: "Azure OpenAI", + }; case "compatible-endpoint": return { ...base, diff --git a/test/onboard-selection.test.js b/test/onboard-selection.test.js index 6b5023358..3e9fddedb 100644 --- a/test/onboard-selection.test.js +++ b/test/onboard-selection.test.js @@ -290,7 +290,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["1", "7", "custom/provider-model"]; +const answers = ["1", "8", "custom/provider-model"]; const messages = []; credentials.prompt = async (message) => { @@ -383,7 +383,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["1", "7", "bad/model", "z-ai/glm5"]; +const answers = ["1", "8", "bad/model", "z-ai/glm5"]; const messages = []; credentials.prompt = async (message) => { @@ -482,7 +482,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); - const answers = ["6", "7", "gemini-custom"]; + const answers = ["7", "8", "gemini-custom"]; const messages = []; credentials.prompt = async (message) => { @@ -568,7 +568,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["7", "1"]; +const answers = ["8", "1"]; const messages = []; const commands = []; @@ -665,7 +665,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["7", "2", "back", "1", ""]; +const answers = ["8", "2", "back", "1", ""]; const messages = []; credentials.prompt = async (message) => { @@ -767,7 +767,7 @@ exit 0 const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["7", "1"]; +const answers = ["8", "1"]; const messages = []; credentials.prompt = async (message) => { @@ -874,7 +874,7 @@ exit 0 const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["7", "1", "2", "llama3.2:3b"]; +const answers = ["8", "1", "2", "llama3.2:3b"]; const messages = []; credentials.prompt = async (message) => { @@ -976,7 +976,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["2", "5", "bad-model", "gpt-5.4-mini"]; +const answers = ["2", "6", "bad-model", "gpt-5.4-mini"]; const messages = []; credentials.prompt = async (message) => { @@ -1059,7 +1059,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["4", "4", "claude-bad", "claude-haiku-4-5"]; +const answers = ["5", "5", "claude-bad", "claude-haiku-4-5"]; const messages = []; credentials.prompt = async (message) => { @@ -1154,7 +1154,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["4", "", "4", "2"]; +const answers = ["5", "", "5", "2"]; const messages = []; credentials.prompt = async (message) => { @@ -1237,7 +1237,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["5", "https://proxy.example.com/v1/messages?token=secret#frag", "claude-sonnet-proxy"]; +const answers = ["6", "https://proxy.example.com/v1/messages?token=secret#frag", "claude-sonnet-proxy"]; const messages = []; credentials.prompt = async (message) => { @@ -1332,7 +1332,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["3", "https://proxy.example.com/v1/chat/completions?token=secret#frag", "bad-model", "good-model"]; +const answers = ["4", "https://proxy.example.com/v1/chat/completions?token=secret#frag", "bad-model", "good-model"]; const messages = []; credentials.prompt = async (message) => { @@ -1445,7 +1445,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["3", "https://proxy.example.com/v1", "custom-model"]; +const answers = ["4", "https://proxy.example.com/v1", "custom-model"]; const messages = []; credentials.prompt = async (message) => { @@ -1529,7 +1529,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["3", "", "", ""]; +const answers = ["4", "", "", ""]; const messages = []; credentials.prompt = async (message) => { @@ -1625,7 +1625,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["5", "https://proxy.example.com/v1/messages?token=secret#frag", "bad-claude", "good-claude"]; +const answers = ["6", "https://proxy.example.com/v1/messages?token=secret#frag", "bad-claude", "good-claude"]; const messages = []; credentials.prompt = async (message) => { @@ -1727,7 +1727,7 @@ printf '%s' "$status" const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["3", "https://proxy.example.com/v1", "back", "1", ""]; +const answers = ["4", "https://proxy.example.com/v1", "back", "1", ""]; const messages = []; credentials.prompt = async (message) => { @@ -2288,7 +2288,7 @@ const { setupNim } = require(${onboardPath}); const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["4", "", "retry", "anthropic-good", ""]; +const answers = ["5", "", "retry", "anthropic-good", ""]; const messages = []; credentials.prompt = async (message) => { @@ -2366,7 +2366,7 @@ const { setupNim } = require(${onboardPath}); const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["6", "", "retry", "gemini-good", ""]; +const answers = ["7", "", "retry", "gemini-good", ""]; const messages = []; credentials.prompt = async (message) => { @@ -2446,7 +2446,7 @@ const { setupNim } = require(${onboardPath}); const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["3", "https://proxy.example.com/v1/chat/completions?token=secret#frag", "custom-model", "retry", "proxy-good", "custom-model"]; +const answers = ["4", "https://proxy.example.com/v1/chat/completions?token=secret#frag", "custom-model", "retry", "proxy-good", "custom-model"]; const messages = []; credentials.prompt = async (message) => { @@ -2540,7 +2540,7 @@ const { setupNim } = require(${onboardPath}); const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["5", "https://proxy.example.com/v1/messages?token=secret#frag", "claude-proxy", "retry", "anthropic-proxy-good", "claude-proxy"]; +const answers = ["6", "https://proxy.example.com/v1/messages?token=secret#frag", "claude-proxy", "retry", "anthropic-proxy-good", "claude-proxy"]; const messages = []; credentials.prompt = async (message) => { @@ -2655,12 +2655,12 @@ printf '%s' "$status" { mode: 0o755 }, ); - // vLLM is option 7 (build, openai, custom, anthropic, anthropicCompatible, gemini, vllm) + // vLLM is option 8 (build, openai, azureOpenAi, custom, anthropic, anthropicCompatible, gemini, vllm) const script = String.raw` const credentials = require(${credentialsPath}); const runner = require(${runnerPath}); -const answers = ["7"]; +const answers = ["8"]; const messages = []; credentials.prompt = async (message) => { @@ -2754,7 +2754,7 @@ printf '%s' "$status" { mode: 0o755 }, ); - // NIM-local is option 7 (build, openai, custom, anthropic, anthropicCompatible, gemini, nim-local) + // NIM-local is option 8 (build, openai, azureOpenAi, custom, anthropic, anthropicCompatible, gemini, nim-local) // No ollama, no vLLM — only NIM-local shows up as experimental option const script = String.raw` const credentials = require(${credentialsPath}); @@ -2768,8 +2768,8 @@ nimMod.containerName = () => "nemoclaw-nim-test"; nimMod.startNimContainerByName = () => "container-123"; nimMod.waitForNimHealth = () => true; -// Select option 7 (nim-local), then model 1 -const answers = ["7", "1"]; +// Select option 8 (nim-local), then model 1 +const answers = ["8", "1"]; const messages = []; credentials.prompt = async (message) => { From e6dec7c533305bc56a5f52f3f3e64457a37966e6 Mon Sep 17 00:00:00 2001 From: Gaurav Singh Date: Wed, 8 Apr 2026 12:23:31 +0530 Subject: [PATCH 2/4] docs(inference): document Azure OpenAI provider Add Azure OpenAI to the inference options table, validation table, runtime switch examples, and network policy reference. Regenerate agent skills. Co-Authored-By: Claude Opus 4.6 (1M context) --- .agents/skills/nemoclaw-configure-inference/SKILL.md | 12 +++++++++++- .../references/inference-options.md | 4 +++- .../references/network-policies.md | 5 +++++ docs/inference/inference-options.md | 4 +++- docs/inference/switch-inference-providers.md | 6 ++++++ docs/reference/network-policies.md | 5 +++++ 6 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.agents/skills/nemoclaw-configure-inference/SKILL.md b/.agents/skills/nemoclaw-configure-inference/SKILL.md index 4e971d1af..878e830db 100644 --- a/.agents/skills/nemoclaw-configure-inference/SKILL.md +++ b/.agents/skills/nemoclaw-configure-inference/SKILL.md @@ -25,13 +25,14 @@ The sandbox does not receive your API key. ## Provider Options The onboard wizard presents the following provider options by default. -The first six are always available. +The first seven are always available. Ollama appears when it is installed or running on the host. | Option | Description | Curated models | |--------|-------------|----------------| | NVIDIA Endpoints | Routes to models hosted on [build.nvidia.com](https://build.nvidia.com). You can also enter any model ID from the catalog. Set `NVIDIA_API_KEY`. | Nemotron 3 Super 120B, Kimi K2.5, GLM-5, MiniMax M2.5, GPT-OSS 120B | | OpenAI | Routes to the OpenAI API. Set `OPENAI_API_KEY`. | `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.4-pro-2026-03-05` | +| Azure OpenAI | Routes to an Azure OpenAI deployment. The wizard prompts for your resource endpoint URL (`https://.openai.azure.com/v1`) and a deployment model name. Set `AZURE_OPENAI_API_KEY`. | You provide the deployment model name. | | Other OpenAI-compatible endpoint | Routes to any server that implements `/v1/chat/completions`. If the endpoint also supports `/responses` with OpenClaw-style tool calling, NemoClaw can use that path; otherwise it falls back to `/chat/completions`. The wizard prompts for a base URL and model name. Works with OpenRouter, LocalAI, llama.cpp, or any compatible proxy. Set `COMPATIBLE_API_KEY`. | You provide the model name. | | Anthropic | Routes to the Anthropic Messages API. Set `ANTHROPIC_API_KEY`. | `claude-sonnet-4-6`, `claude-haiku-4-5`, `claude-opus-4-6` | | Other Anthropic-compatible endpoint | Routes to any server that implements the Anthropic Messages API (`/v1/messages`). The wizard prompts for a base URL and model name. Set `COMPATIBLE_ANTHROPIC_API_KEY`. | You provide the model name. | @@ -57,6 +58,7 @@ If validation fails, the wizard returns to provider selection. | Provider type | Validation method | |---|---| | OpenAI | Tries `/responses` first, then `/chat/completions`. | +| Azure OpenAI | Tries `/responses` first with a tool-calling probe. Falls back to `/chat/completions` if the endpoint does not return a compatible tool call. | | NVIDIA Endpoints | Tries `/responses` first with a tool-calling probe that matches OpenClaw behavior. Falls back to `/chat/completions` if the endpoint does not return a compatible tool call. | | Google Gemini | Tries `/responses` first with a tool-calling probe that matches OpenClaw behavior. Falls back to `/chat/completions` if the endpoint does not return a compatible tool call. | | Other OpenAI-compatible endpoint | Tries `/responses` first with a tool-calling probe that matches OpenClaw behavior. Falls back to `/chat/completions` if the endpoint does not return a compatible tool call. | @@ -64,6 +66,8 @@ If validation fails, the wizard returns to provider selection. | NVIDIA Endpoints (manual model entry) | Validates the model name against the catalog API. | | Compatible endpoints | Sends a real inference request because many proxies do not expose a `/models` endpoint. For OpenAI-compatible endpoints, the probe includes tool calling before NemoClaw favors `/responses`. | +*Full details in `references/inference-options.md`.* + ## Prerequisites - A running NemoClaw sandbox. @@ -91,6 +95,12 @@ $ openshell inference set --provider nvidia-prod --model nvidia/nemotron-3-super $ openshell inference set --provider openai-api --model gpt-5.4 ``` +### Azure OpenAI + +```console +$ openshell inference set --provider azure-openai --model +``` + ### Anthropic ```console diff --git a/.agents/skills/nemoclaw-configure-inference/references/inference-options.md b/.agents/skills/nemoclaw-configure-inference/references/inference-options.md index 09f131aa6..2f9a25272 100644 --- a/.agents/skills/nemoclaw-configure-inference/references/inference-options.md +++ b/.agents/skills/nemoclaw-configure-inference/references/inference-options.md @@ -16,13 +16,14 @@ The sandbox does not receive your API key. ## Provider Options The onboard wizard presents the following provider options by default. -The first six are always available. +The first seven are always available. Ollama appears when it is installed or running on the host. | Option | Description | Curated models | |--------|-------------|----------------| | NVIDIA Endpoints | Routes to models hosted on [build.nvidia.com](https://build.nvidia.com). You can also enter any model ID from the catalog. Set `NVIDIA_API_KEY`. | Nemotron 3 Super 120B, Kimi K2.5, GLM-5, MiniMax M2.5, GPT-OSS 120B | | OpenAI | Routes to the OpenAI API. Set `OPENAI_API_KEY`. | `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.4-pro-2026-03-05` | +| Azure OpenAI | Routes to an Azure OpenAI deployment. The wizard prompts for your resource endpoint URL (`https://.openai.azure.com/v1`) and a deployment model name. Set `AZURE_OPENAI_API_KEY`. | You provide the deployment model name. | | Other OpenAI-compatible endpoint | Routes to any server that implements `/v1/chat/completions`. If the endpoint also supports `/responses` with OpenClaw-style tool calling, NemoClaw can use that path; otherwise it falls back to `/chat/completions`. The wizard prompts for a base URL and model name. Works with OpenRouter, LocalAI, llama.cpp, or any compatible proxy. Set `COMPATIBLE_API_KEY`. | You provide the model name. | | Anthropic | Routes to the Anthropic Messages API. Set `ANTHROPIC_API_KEY`. | `claude-sonnet-4-6`, `claude-haiku-4-5`, `claude-opus-4-6` | | Other Anthropic-compatible endpoint | Routes to any server that implements the Anthropic Messages API (`/v1/messages`). The wizard prompts for a base URL and model name. Set `COMPATIBLE_ANTHROPIC_API_KEY`. | You provide the model name. | @@ -48,6 +49,7 @@ If validation fails, the wizard returns to provider selection. | Provider type | Validation method | |---|---| | OpenAI | Tries `/responses` first, then `/chat/completions`. | +| Azure OpenAI | Tries `/responses` first with a tool-calling probe. Falls back to `/chat/completions` if the endpoint does not return a compatible tool call. | | NVIDIA Endpoints | Tries `/responses` first with a tool-calling probe that matches OpenClaw behavior. Falls back to `/chat/completions` if the endpoint does not return a compatible tool call. | | Google Gemini | Tries `/responses` first with a tool-calling probe that matches OpenClaw behavior. Falls back to `/chat/completions` if the endpoint does not return a compatible tool call. | | Other OpenAI-compatible endpoint | Tries `/responses` first with a tool-calling probe that matches OpenClaw behavior. Falls back to `/chat/completions` if the endpoint does not return a compatible tool call. | diff --git a/.agents/skills/nemoclaw-reference/references/network-policies.md b/.agents/skills/nemoclaw-reference/references/network-policies.md index 7e895cf43..fd2f66551 100644 --- a/.agents/skills/nemoclaw-reference/references/network-policies.md +++ b/.agents/skills/nemoclaw-reference/references/network-policies.md @@ -41,6 +41,11 @@ The following endpoint groups are allowed by default: - `/usr/local/bin/claude`, `/usr/local/bin/openclaw` - All methods +* - `azure_openai` + - `*.openai.azure.com:443` + - `/usr/local/bin/claude`, `/usr/local/bin/openclaw` + - POST on `/openai/deployments/*/chat/completions`, GET on `/openai/models` + * - `github` - `github.com:443` - `/usr/bin/gh`, `/usr/bin/git` diff --git a/docs/inference/inference-options.md b/docs/inference/inference-options.md index 828dbefdf..3a37c7f6b 100644 --- a/docs/inference/inference-options.md +++ b/docs/inference/inference-options.md @@ -38,13 +38,14 @@ The sandbox does not receive your API key. ## Provider Options The onboard wizard presents the following provider options by default. -The first six are always available. +The first seven are always available. Ollama appears when it is installed or running on the host. | Option | Description | Curated models | |--------|-------------|----------------| | NVIDIA Endpoints | Routes to models hosted on [build.nvidia.com](https://build.nvidia.com). You can also enter any model ID from the catalog. Set `NVIDIA_API_KEY`. | Nemotron 3 Super 120B, Kimi K2.5, GLM-5, MiniMax M2.5, GPT-OSS 120B | | OpenAI | Routes to the OpenAI API. Set `OPENAI_API_KEY`. | `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.4-pro-2026-03-05` | +| Azure OpenAI | Routes to an Azure OpenAI deployment. The wizard prompts for your resource endpoint URL (`https://.openai.azure.com/v1`) and a deployment model name. Set `AZURE_OPENAI_API_KEY`. | You provide the deployment model name. | | Other OpenAI-compatible endpoint | Routes to any server that implements `/v1/chat/completions`. If the endpoint also supports `/responses` with OpenClaw-style tool calling, NemoClaw can use that path; otherwise it falls back to `/chat/completions`. The wizard prompts for a base URL and model name. Works with OpenRouter, LocalAI, llama.cpp, or any compatible proxy. Set `COMPATIBLE_API_KEY`. | You provide the model name. | | Anthropic | Routes to the Anthropic Messages API. Set `ANTHROPIC_API_KEY`. | `claude-sonnet-4-6`, `claude-haiku-4-5`, `claude-opus-4-6` | | Other Anthropic-compatible endpoint | Routes to any server that implements the Anthropic Messages API (`/v1/messages`). The wizard prompts for a base URL and model name. Set `COMPATIBLE_ANTHROPIC_API_KEY`. | You provide the model name. | @@ -70,6 +71,7 @@ If validation fails, the wizard returns to provider selection. | Provider type | Validation method | |---|---| | OpenAI | Tries `/responses` first, then `/chat/completions`. | +| Azure OpenAI | Tries `/responses` first with a tool-calling probe. Falls back to `/chat/completions` if the endpoint does not return a compatible tool call. | | NVIDIA Endpoints | Tries `/responses` first with a tool-calling probe that matches OpenClaw behavior. Falls back to `/chat/completions` if the endpoint does not return a compatible tool call. | | Google Gemini | Tries `/responses` first with a tool-calling probe that matches OpenClaw behavior. Falls back to `/chat/completions` if the endpoint does not return a compatible tool call. | | Other OpenAI-compatible endpoint | Tries `/responses` first with a tool-calling probe that matches OpenClaw behavior. Falls back to `/chat/completions` if the endpoint does not return a compatible tool call. | diff --git a/docs/inference/switch-inference-providers.md b/docs/inference/switch-inference-providers.md index 60f058cee..da1a22d26 100644 --- a/docs/inference/switch-inference-providers.md +++ b/docs/inference/switch-inference-providers.md @@ -47,6 +47,12 @@ $ openshell inference set --provider nvidia-prod --model nvidia/nemotron-3-super $ openshell inference set --provider openai-api --model gpt-5.4 ``` +### Azure OpenAI + +```console +$ openshell inference set --provider azure-openai --model +``` + ### Anthropic ```console diff --git a/docs/reference/network-policies.md b/docs/reference/network-policies.md index 68c052686..14bb2fdc7 100644 --- a/docs/reference/network-policies.md +++ b/docs/reference/network-policies.md @@ -63,6 +63,11 @@ The following endpoint groups are allowed by default: - `/usr/local/bin/claude`, `/usr/local/bin/openclaw` - All methods +* - `azure_openai` + - `*.openai.azure.com:443` + - `/usr/local/bin/claude`, `/usr/local/bin/openclaw` + - POST on `/openai/deployments/*/chat/completions`, GET on `/openai/models` + * - `github` - `github.com:443` - `/usr/bin/gh`, `/usr/bin/git` From d8ee8ac6cd1aa0bda93e5c115245f3915a892458 Mon Sep 17 00:00:00 2001 From: Gaurav Singh Date: Wed, 8 Apr 2026 12:52:37 +0530 Subject: [PATCH 3/4] fix(docs): address CodeRabbit review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix SPDX header year in inference-options.md (2025-2026 → 2026) - List all Azure OpenAI network policy rules in docs (was missing completions, embeddings, and deployment listing paths) - Regenerate agent skills Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/nemoclaw-reference/references/network-policies.md | 2 +- docs/inference/inference-options.md | 2 +- docs/reference/network-policies.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.agents/skills/nemoclaw-reference/references/network-policies.md b/.agents/skills/nemoclaw-reference/references/network-policies.md index fd2f66551..bcf5a96b9 100644 --- a/.agents/skills/nemoclaw-reference/references/network-policies.md +++ b/.agents/skills/nemoclaw-reference/references/network-policies.md @@ -44,7 +44,7 @@ The following endpoint groups are allowed by default: * - `azure_openai` - `*.openai.azure.com:443` - `/usr/local/bin/claude`, `/usr/local/bin/openclaw` - - POST on `/openai/deployments/*/chat/completions`, GET on `/openai/models` + - POST on `/openai/deployments/*/chat/completions`, `/openai/deployments/*/completions`, `/openai/deployments/*/embeddings`; GET on `/openai/deployments`, `/openai/deployments/**`, `/openai/models`, `/openai/models/**` * - `github` - `github.com:443` diff --git a/docs/inference/inference-options.md b/docs/inference/inference-options.md index 3a37c7f6b..84cf1a246 100644 --- a/docs/inference/inference-options.md +++ b/docs/inference/inference-options.md @@ -16,7 +16,7 @@ status: published --- diff --git a/docs/reference/network-policies.md b/docs/reference/network-policies.md index 14bb2fdc7..61fe55f51 100644 --- a/docs/reference/network-policies.md +++ b/docs/reference/network-policies.md @@ -66,7 +66,7 @@ The following endpoint groups are allowed by default: * - `azure_openai` - `*.openai.azure.com:443` - `/usr/local/bin/claude`, `/usr/local/bin/openclaw` - - POST on `/openai/deployments/*/chat/completions`, GET on `/openai/models` + - POST on `/openai/deployments/*/chat/completions`, `/openai/deployments/*/completions`, `/openai/deployments/*/embeddings`; GET on `/openai/deployments`, `/openai/deployments/**`, `/openai/models`, `/openai/models/**` * - `github` - `github.com:443` From b1a0042b5c2aae29fb8b923791788dad74e26ae3 Mon Sep 17 00:00:00 2001 From: Gaurav Singh Date: Wed, 8 Apr 2026 12:54:29 +0530 Subject: [PATCH 4/4] docs(inference): add JSDoc to exported functions in inference-config Addresses CodeRabbit docstring coverage check (was 60%, threshold 80%). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/inference-config.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/inference-config.ts b/src/lib/inference-config.ts index db62cc562..4344d0002 100644 --- a/src/lib/inference-config.ts +++ b/src/lib/inference-config.ts @@ -38,6 +38,10 @@ export interface GatewayInference { model: string | null; } +/** + * Return the inference routing config for a known provider, or null + * if the provider ID is not in the approved set. + */ export function getProviderSelectionConfig( provider: string, model?: string, @@ -120,12 +124,14 @@ export function getProviderSelectionConfig( } } +/** Build the qualified `/` ref used by OpenClaw. */ export function getOpenClawPrimaryModel(provider: string, model?: string): string { const resolvedModel = model || (provider === "ollama-local" ? DEFAULT_OLLAMA_MODEL : DEFAULT_CLOUD_MODEL); return `${MANAGED_PROVIDER_ID}/${resolvedModel}`; } +/** Parse provider and model from `openshell inference get` CLI output. */ export function parseGatewayInference(output: string | null | undefined): GatewayInference | null { if (!output) return null; // eslint-disable-next-line no-control-regex