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
3 changes: 2 additions & 1 deletion docs/reference/commands-nemohermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1519,7 +1519,7 @@ Under the `nemohermes` alias, it uses the registered Hermes sandbox when exactly
By default, the command syncs the default registered sandbox.

```bash
nemohermes inference set --provider <provider> --model <model> [--sandbox <name>] [--no-verify]
nemohermes inference set --provider <provider> --model <model> [--sandbox <name>] [--no-verify] [--endpoint-url <url>] [--credential-env <ENV>] [--inference-api <api>]
```

Pass both `--provider` and `--model` when you want NemoClaw to update the OpenShell inference route and sync the selected sandbox's agent config.
Expand All @@ -1530,6 +1530,7 @@ If the in-sandbox config sync fails, NemoClaw keeps the gateway and registry ali

Supported provider names are `nvidia-prod`, `nvidia-nim`, `nvidia-router`, `openai-api`, `anthropic-prod`, `compatible-anthropic-endpoint`, `gemini-api`, `compatible-endpoint`, `hermes-provider`, `ollama-local`, and `vllm-local`.
Use `--no-verify` only when OpenShell cannot verify the provider at switch time but you have already confirmed the provider and credential.
When switching to `compatible-endpoint` or `compatible-anthropic-endpoint` from a different provider family, pass `--endpoint-url` with the trusted custom provider URL so NemoClaw can persist durable rebuild metadata. `--credential-env` and `--inference-api` may also be supplied for the compatible provider metadata; supported API values are `openai-completions`, `anthropic-messages`, and `openai-responses`.

### `nemohermes setup`

Expand Down
3 changes: 2 additions & 1 deletion docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1874,7 +1874,7 @@ Under the `nemohermes` alias, it uses the registered Hermes sandbox when exactly
By default, the command syncs the default registered sandbox.

```bash
$$nemoclaw inference set --provider <provider> --model <model> [--sandbox <name>] [--no-verify]
$$nemoclaw inference set --provider <provider> --model <model> [--sandbox <name>] [--no-verify] [--endpoint-url <url>] [--credential-env <ENV>] [--inference-api <api>]
```

Pass both `--provider` and `--model` when you want NemoClaw to update the OpenShell inference route and sync the selected sandbox's agent config.
Expand All @@ -1885,6 +1885,7 @@ If the in-sandbox config sync fails, NemoClaw keeps the gateway and registry ali

Supported provider names are `nvidia-prod`, `nvidia-nim`, `nvidia-router`, `openai-api`, `anthropic-prod`, `compatible-anthropic-endpoint`, `gemini-api`, `compatible-endpoint`, `hermes-provider`, `ollama-local`, and `vllm-local`.
Use `--no-verify` only when OpenShell cannot verify the provider at switch time but you have already confirmed the provider and credential.
When switching to `compatible-endpoint` or `compatible-anthropic-endpoint` from a different provider family, pass `--endpoint-url` with the trusted custom provider URL so NemoClaw can persist durable rebuild metadata. `--credential-env` and `--inference-api` may also be supplied for the compatible provider metadata; supported API values are `openai-completions`, `anthropic-messages`, and `openai-responses`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### `$$nemoclaw setup`

Expand Down
9 changes: 9 additions & 0 deletions src/commands/global-oclif-command-adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,12 @@ describe("global oclif command adapters", () => {
"--sandbox",
"alpha",
"--no-verify",
"--endpoint-url",
"https://example.test/v1",
"--credential-env",
"COMPATIBLE_API_KEY",
"--inference-api",
"openai-completions",
],
rootDir,
);
Expand All @@ -240,6 +246,9 @@ describe("global oclif command adapters", () => {
model: "nvidia/nemotron-3-super-120b-a12b",
sandboxName: "alpha",
noVerify: true,
endpointUrl: "https://example.test/v1",
credentialEnv: "COMPATIBLE_API_KEY",
inferenceApi: "openai-completions",
});
});

Expand Down
16 changes: 15 additions & 1 deletion src/commands/inference/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default class InferenceSetCommand extends NemoClawCommand {
static description =
"Update the OpenShell inference route and sync the running OpenClaw or Hermes sandbox config.";
static usage = [
"inference set --provider <provider> --model <model> [--sandbox <name>] [--no-verify]",
"inference set --provider <provider> --model <model> [--sandbox <name>] [--no-verify] [--endpoint-url <url>] [--credential-env <ENV>] [--inference-api <api>]",
];
static examples = [
"<%= config.bin %> inference set --provider nvidia-prod --model nvidia/nemotron-3-super-120b-a12b",
Expand All @@ -41,6 +41,17 @@ export default class InferenceSetCommand extends NemoClawCommand {
"no-verify": Flags.boolean({
description: "Pass --no-verify through to openshell inference set",
}),
"endpoint-url": Flags.string({
description: "Trusted endpoint URL to persist when switching to a compatible custom provider",
}),
"credential-env": Flags.string({
description:
"Trusted credential env name to persist when switching to a compatible custom provider",
}),
"inference-api": Flags.string({
description:
"Trusted API family to persist for compatible custom providers (openai-completions, anthropic-messages, openai-responses)",
}),
};

public async run(): Promise<void> {
Expand All @@ -55,6 +66,9 @@ export default class InferenceSetCommand extends NemoClawCommand {
model: flags.model,
sandboxName: flags.sandbox ?? null,
noVerify: flags["no-verify"] === true,
endpointUrl: flags["endpoint-url"] ?? null,
credentialEnv: flags["credential-env"] ?? null,
inferenceApi: flags["inference-api"] ?? null,
});
} catch (error) {
if (error instanceof InferenceSetError) {
Expand Down
152 changes: 152 additions & 0 deletions src/lib/actions/inference-set.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,158 @@ describe("runInferenceSet", () => {
expect(deps.calls.updateSandbox).not.toHaveBeenCalled();
});

it("rejects Anthropic Messages metadata for OpenAI-compatible endpoint switches", async () => {
const deps = createDeps({
config: { agents: { defaults: { model: { primary: "inference/nvidia/model-a" } } } },
entry: {
name: "alpha",
agent: "openclaw",
provider: "nvidia-prod",
model: "nvidia/model-a",
},
session: baseSession({
provider: "nvidia-prod",
model: "nvidia/model-a",
endpointUrl: "https://integrate.api.nvidia.com/v1",
credentialEnv: "NVIDIA_INFERENCE_API_KEY",
}),
});

await expect(
runInferenceSet(
{
provider: "compatible-endpoint",
model: "mock-openai-model",
noVerify: true,
endpointUrl: "https://compatible.example/v1",
credentialEnv: "COMPATIBLE_API_KEY",
inferenceApi: "anthropic-messages",
},
deps,
),
).rejects.toThrow(
/inference-api for 'compatible-endpoint' must be one of: openai-completions, openai-responses/,
);

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

it("preserves explicit inference API through the final registry and session sync", async () => {
const config: ConfigObject = {
agents: { defaults: { model: { primary: "inference/nvidia/model-a" } } },
models: { providers: { inference: { api: "openai-completions", models: [] } } },
};
const deps = createDeps({
config,
entry: {
name: "alpha",
agent: "openclaw",
provider: "nvidia-prod",
model: "nvidia/model-a",
},
session: baseSession({
provider: "nvidia-prod",
model: "nvidia/model-a",
endpointUrl: "https://integrate.api.nvidia.com/v1",
credentialEnv: "NVIDIA_INFERENCE_API_KEY",
preferredInferenceApi: "openai-completions",
}),
});

await runInferenceSet(
{
provider: "compatible-endpoint",
model: "mock-responses-model",
noVerify: true,
endpointUrl: "https://compatible.example/v1",
credentialEnv: "COMPATIBLE_API_KEY",
inferenceApi: "openai-responses",
},
deps,
);

expect(config.models).toMatchObject({
providers: {
inference: {
api: "openai-responses",
models: [{ id: "mock-responses-model", name: "inference/mock-responses-model" }],
},
},
});
expect(deps.calls.updateSandbox.mock.calls.at(-1)).toEqual([
"alpha",
expect.objectContaining({
provider: "compatible-endpoint",
model: "mock-responses-model",
endpointUrl: "https://compatible.example/v1",
credentialEnv: "COMPATIBLE_API_KEY",
preferredInferenceApi: "openai-responses",
}),
]);
expect(deps.getSession()).toMatchObject({
provider: "compatible-endpoint",
model: "mock-responses-model",
endpointUrl: "https://compatible.example/v1",
credentialEnv: "COMPATIBLE_API_KEY",
preferredInferenceApi: "openai-responses",
});
});

it("accepts explicit compatible Anthropic endpoint metadata for provider-family switches", async () => {
const config: ConfigObject = {
agents: { defaults: { model: { primary: "inference/nvidia/model-a" } } },
models: { providers: { inference: { api: "openai-completions", models: [] } } },
};
const deps = createDeps({
config,
entry: {
name: "alpha",
agent: "openclaw",
provider: "nvidia-prod",
model: "nvidia/model-a",
},
session: baseSession({
provider: "nvidia-prod",
model: "nvidia/model-a",
endpointUrl: "https://integrate.api.nvidia.com/v1",
credentialEnv: "NVIDIA_INFERENCE_API_KEY",
}),
});

await runInferenceSet(
{
provider: "compatible-anthropic-endpoint",
model: "mock-anthropic-model",
noVerify: true,
endpointUrl: "http://host.openshell.internal:18767/",
credentialEnv: "COMPATIBLE_ANTHROPIC_API_KEY",
inferenceApi: "anthropic-messages",
},
deps,
);

expect(deps.calls.updateSandbox.mock.calls.at(-1)).toEqual([
"alpha",
expect.objectContaining({
provider: "compatible-anthropic-endpoint",
model: "mock-anthropic-model",
endpointUrl: "http://host.openshell.internal:18767",
credentialEnv: "COMPATIBLE_ANTHROPIC_API_KEY",
preferredInferenceApi: "anthropic-messages",
nimContainer: null,
}),
]);
expect(deps.getSession()).toMatchObject({
provider: "compatible-anthropic-endpoint",
model: "mock-anthropic-model",
endpointUrl: "http://host.openshell.internal:18767",
credentialEnv: "COMPATIBLE_ANTHROPIC_API_KEY",
preferredInferenceApi: "anthropic-messages",
nimContainer: null,
});
});

it("preserves same-provider Bedrock Runtime adapter routing for OpenClaw switches", async () => {
const config: ConfigObject = {
agents: {
Expand Down
Loading
Loading