Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/inference/inference-options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,16 @@ If your local server implements the Anthropic Messages API (`/v1/messages`), cho
$$nemoclaw onboard
```

<AgentOnly variant="hermes">
For `compatible-anthropic-endpoint`, Hermes uses the managed OpenAI Chat Completions frontend at `https://inference.local/v1`.
During onboarding, NemoClaw verifies that the endpoint also serves `/v1/chat/completions`, then registers that surface with OpenShell as `type=openai` using `OPENAI_BASE_URL`.
The route retains `COMPATIBLE_ANTHROPIC_API_KEY` as its credential binding.
This avoids duplicate Anthropic SSE `message_start` events.
If the endpoint only serves Anthropic Messages, onboarding stops with guidance instead of creating a Hermes sandbox with an unroutable or broken streaming path.
OpenClaw custom Anthropic routes and first-party Anthropic routes remain on the native Anthropic Messages frontend.
AWS Bedrock routes retain their existing OpenAI-compatible adapter behavior.
</AgentOnly>

For non-interactive setup, use `NEMOCLAW_PROVIDER=anthropicCompatible` and set `COMPATIBLE_ANTHROPIC_API_KEY`.

```bash
Expand Down
18 changes: 16 additions & 2 deletions docs/inference/switch-inference-providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,22 @@ For OpenClaw, `inference set` syncs the provider API family and primary model re
For Hermes, `inference set` writes `model.api_mode: anthropic_messages` for Anthropic Messages routes, `model.api_mode: codex_responses` for OpenAI Responses routes, and removes `api_mode` for OpenAI-style chat-completions routes.
Hermes also keeps `model.api_key` on the OpenShell proxy placeholder so dashboard and API sessions continue to authenticate through the gateway after a route change.

Amazon Bedrock Runtime routes created through `compatible-anthropic-endpoint` are the exception.
When you switch within the same Bedrock Runtime compatible provider, NemoClaw keeps the route OpenAI-compatible and does not set Hermes to Anthropic Messages mode.
<AgentOnly variant="hermes">
For `compatible-anthropic-endpoint`, NemoClaw selects the managed OpenAI Chat Completions frontend at `https://inference.local/v1` and verifies the endpoint's `/v1/chat/completions` surface before registering it with OpenShell as `type=openai` using `OPENAI_BASE_URL`.
The route retains `COMPATIBLE_ANTHROPIC_API_KEY` as its credential binding.
Hermes omits `model.api_mode` for this route.
OpenClaw custom Anthropic routes and first-party Anthropic routes remain on the native Anthropic Messages frontend.
AWS Bedrock routes retain their existing OpenAI-compatible adapter behavior.

To migrate an already affected Hermes sandbox, rebuild it so NemoClaw can verify the OpenAI surface, repair the gateway provider protocol, and recreate the sandbox configuration:

```bash
$$nemoclaw <sandbox-name> rebuild
```

Resuming onboarding also detects and repairs a stale route.
`inference set` can select this provider after it has been registered on the verified OpenAI surface, but it fails before mutation for a legacy Anthropic registration because that command cannot change a gateway provider's protocol type.
</AgentOnly>

#### Switching from Responses API to Chat Completions

Expand Down
49 changes: 47 additions & 2 deletions src/lib/actions/inference-route-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,56 @@ describe("resolveRuntimeInferenceApi", () => {
).toBe("openai-responses");
});

it("reads Hermes api_mode for same-provider Hermes switches", () => {
it("reads Hermes api_mode for other same-provider Hermes switches", () => {
expect(
resolve(
{ model: { api_mode: "anthropic_messages" } },
{ agentName: "hermes", session: null },
{
agentName: "hermes",
currentProvider: "compatible-endpoint",
provider: "compatible-endpoint",
session: null,
},
),
).toBe("anthropic-messages");
});

it("keeps Hermes custom Anthropic routes off the managed Anthropic SSE frontend (#6289)", () => {
expect(
resolve(
{ model: { api_mode: "anthropic_messages" } },
{
agentName: "hermes",
session: session({ preferredInferenceApi: "anthropic-messages" }),
},
),
).toBe("openai-completions");
});

it("uses the Hermes override when switching into a custom Anthropic route (#6289)", () => {
expect(
resolve(
{},
{
agentName: "hermes",
currentProvider: "nvidia-prod",
session: session({
provider: "nvidia-prod",
preferredInferenceApi: "openai-completions",
}),
},
),
).toBe("openai-completions");
});

it("preserves native Anthropic routing for OpenClaw custom endpoints (#6289)", () => {
expect(
resolve(
{ models: { providers: { anthropic: { api: "anthropic-messages" } } } },
{
agentName: "openclaw",
session: session({ preferredInferenceApi: "anthropic-messages" }),
},
),
).toBe("anthropic-messages");
});
Expand Down
4 changes: 3 additions & 1 deletion src/lib/actions/inference-route-api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { getSandboxInferenceConfig } from "../inference/config";
import { getSandboxInferenceConfig, resolveAgentInferenceApi } from "../inference/config";
import type { ConfigObject } from "../security/credential-filter";
import { isConfigObject } from "../security/credential-filter";
import type { Session } from "../state/onboard-session";
Expand Down Expand Up @@ -109,6 +109,8 @@ export function resolveRuntimeInferenceApi(options: {
}): InferenceApi | null {
const { agentName, config, currentProvider, provider, sandboxName, session } = options;
if (provider === "anthropic-prod") return "anthropic-messages";
const agentApi = resolveAgentInferenceApi(agentName, provider, null);
if (agentApi) return normalizeInferenceApi(agentApi);

const sameProvider = currentProvider === provider;
const sessionApi = sameProvider ? sessionRouteApi(session, sandboxName, provider) : null;
Expand Down
108 changes: 101 additions & 7 deletions src/lib/actions/inference-set-hermes-run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe("runInferenceSet Hermes routing", () => {
});
});

it("syncs Hermes compatible Anthropic switches to Anthropic Messages when changing provider families", async () => {
it("keeps Hermes custom Anthropic switches off the managed Anthropic SSE frontend (#6289)", async () => {
const config: ConfigObject = {
model: {
default: "openai/gpt-5.4-mini",
Expand Down Expand Up @@ -132,6 +132,17 @@ describe("runInferenceSet Hermes routing", () => {
preferredInferenceApi: "anthropic-messages",
}),
});
deps.calls.captureOpenshell.mockImplementation((args: string[]) =>
args[0] === "provider" && args[1] === "get"
? {
status: 0,
output:
"Name: compatible-anthropic-endpoint\nType: openai\nCredential keys: COMPATIBLE_ANTHROPIC_API_KEY\nConfig keys: OPENAI_BASE_URL",
stdout: "",
stderr: "",
}
: { status: 0, output: "", stdout: "", stderr: "" },
);

const result = await runInferenceSet(
{
Expand All @@ -146,9 +157,8 @@ describe("runInferenceSet Hermes routing", () => {
expect(config.model).toEqual({
default: "claude-sonnet-proxy",
provider: "custom",
base_url: "https://inference.local",
base_url: "https://inference.local/v1",
api_key: HERMES_PROXY_API_KEY_PLACEHOLDER,
api_mode: "anthropic_messages",
});
// The upstream annotation must track the selected provider together with
// the API-family field, so the two cannot drift apart on later switches.
Expand All @@ -163,18 +173,100 @@ describe("runInferenceSet Hermes routing", () => {
model: "claude-sonnet-proxy",
endpointUrl: "https://anthropic-compatible.example/v1",
credentialEnv: "COMPATIBLE_ANTHROPIC_API_KEY",
preferredInferenceApi: "anthropic-messages",
preferredInferenceApi: "openai-completions",
}),
]);
expect(deps.getSession()).toMatchObject({
provider: "compatible-anthropic-endpoint",
model: "claude-sonnet-proxy",
preferredInferenceApi: "anthropic-messages",
preferredInferenceApi: "openai-completions",
});
expect(result).toMatchObject({
providerKey: "anthropic",
primaryModelRef: "anthropic/claude-sonnet-proxy",
providerKey: "inference",
primaryModelRef: "inference/claude-sonnet-proxy",
});
});

it("rejects inference set before mutating a legacy Anthropic provider (#6289)", async () => {
const config: ConfigObject = { model: {} };
const deps = createDeps({
config,
entry: {
name: "hermes",
agent: "hermes",
provider: "compatible-anthropic-endpoint",
model: "claude-sonnet-proxy",
endpointUrl: "https://anthropic-compatible.example/v1",
credentialEnv: "COMPATIBLE_ANTHROPIC_API_KEY",
preferredInferenceApi: "openai-completions",
},
defaultSandbox: "hermes",
target: HERMES_TARGET,
session: baseSession({ agent: "hermes", sandboxName: "hermes" }),
});
deps.calls.captureOpenshell.mockReturnValue({
status: 0,
output:
"Name: compatible-anthropic-endpoint\nType: anthropic\nCredential keys: COMPATIBLE_ANTHROPIC_API_KEY\nConfig keys: ANTHROPIC_BASE_URL",
stdout: "",
stderr: "",
});

await expect(
runInferenceSet(
{
provider: "compatible-anthropic-endpoint",
model: "claude-sonnet-proxy",
sandboxName: "hermes",
noVerify: true,
},
deps,
),
).rejects.toThrow("Run 'nemoclaw hermes rebuild'");

expect(deps.calls.captureOpenshell).toHaveBeenCalledTimes(1);
expect(deps.calls.updateSandbox).not.toHaveBeenCalled();
expect(deps.calls.writeSandboxConfig).not.toHaveBeenCalled();
});

it("rejects an explicit Anthropic frontend request for Hermes custom endpoints (#6289)", async () => {
const config: ConfigObject = {
model: {
default: "openai/gpt-5.4-mini",
provider: "custom",
base_url: "https://inference.local/v1",
},
};
const deps = createDeps({
config,
entry: {
name: "hermes",
agent: "hermes",
provider: "hermes-provider",
model: "openai/gpt-5.4-mini",
},
defaultSandbox: "hermes",
target: HERMES_TARGET,
session: baseSession({ agent: "hermes", sandboxName: "hermes" }),
});

await expect(
runInferenceSet(
{
provider: "compatible-anthropic-endpoint",
model: "nvidia/nvidia/nemotron-3-super-v3",
sandboxName: "hermes",
noVerify: true,
endpointUrl: "https://inference-api.nvidia.com",
credentialEnv: "COMPATIBLE_ANTHROPIC_API_KEY",
inferenceApi: "anthropic-messages",
},
deps,
),
).rejects.toThrow("require the managed openai-completions frontend");

expect(deps.calls.captureOpenshell).not.toHaveBeenCalled();
expect(deps.calls.updateSandbox).not.toHaveBeenCalled();
});

it("preserves same-provider Bedrock Runtime adapter routing for Hermes switches", async () => {
Expand All @@ -192,6 +284,7 @@ describe("runInferenceSet Hermes routing", () => {
agent: "hermes",
provider: "compatible-anthropic-endpoint",
model: "anthropic.claude-3-5-sonnet-20240620-v1:0",
endpointUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
},
defaultSandbox: "hermes",
target: HERMES_TARGET,
Expand All @@ -200,6 +293,7 @@ describe("runInferenceSet Hermes routing", () => {
sandboxName: "hermes",
provider: "compatible-anthropic-endpoint",
model: "anthropic.claude-3-5-sonnet-20240620-v1:0",
endpointUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
preferredInferenceApi: "openai-completions",
}),
});
Expand Down
Loading
Loading