Skip to content
Closed
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
2 changes: 2 additions & 0 deletions docs/inference/choose-inference-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Use this status table to distinguish validated provider integrations from adapte
| NVIDIA Endpoints | Tested | OpenAI-compatible | Hosted models on integrate.api.nvidia.com |
| OpenRouter | Tested | OpenAI-compatible | First-class onboarding route for OpenClaw, Hermes, and LangChain Deep Agents Code. NemoClaw registers the `openrouter-api` provider through OpenShell's `openai` profile with a host runtime adapter URL; host-side validation and runtime traffic send the default OpenRouter attribution headers. |
| OpenAI | Tested | Native OpenAI-compatible | Uses OpenAI model IDs |
| GitHub Copilot | Tested with limitations | OpenShell Copilot profile | Uses OpenShell's `copilot` provider profile and host GitHub token discovery. NemoClaw registers the route and skips generic `/v1` probing because Copilot authentication is profile-owned. |
| Other OpenAI-compatible endpoint | Tested with limitations | Custom OpenAI-compatible | Custom base-URL adapter for servers that implement OpenAI-compatible `/v1/chat/completions` or `/v1/responses`. Behavior on OpenAI-compatible proxies, gateways, and self-hosted implementations may vary; this row claims the adapter, not the universe of compatible endpoints. |
| Anthropic | Tested | Native Anthropic | Uses anthropic-messages |
| Other Anthropic-compatible endpoint | Tested with limitations | Custom Anthropic-compatible | Adapter path validated with AWS Bedrock (`src/lib/onboard/bedrock-runtime.ts`). Behavior on other Anthropic-compatible proxies and gateways may vary; this row claims the adapter, not the universe of compatible endpoints. |
Expand All @@ -44,6 +45,7 @@ Hosted providers use provider-managed endpoints and curated model choices.
| NVIDIA Endpoints | OpenAI-compatible Chat Completions | `NVIDIA_INFERENCE_API_KEY` | [Use NVIDIA Endpoints](../hosted-inference/use-nvidia-endpoints) |
| OpenRouter | OpenAI-compatible Chat Completions | `OPENROUTER_API_KEY` | [Use OpenRouter](../hosted-inference/use-openrouter) |
| OpenAI | Native OpenAI-compatible | `OPENAI_API_KEY` | [Use OpenAI](../hosted-inference/use-openai) |
| GitHub Copilot | OpenShell Copilot profile | `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN` | Use a GitHub token from an account with Copilot access. |
| Anthropic | Anthropic Messages | `ANTHROPIC_API_KEY` | [Use Anthropic](../hosted-inference/use-anthropic) |
| Google Gemini | Google OpenAI-compatible Chat Completions | `GEMINI_API_KEY` | [Use Google Gemini](../hosted-inference/use-google-gemini) |

Expand Down
4 changes: 2 additions & 2 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ After successful recovery, the installer skips generic onboarding.
For a manually prepared upgrade, set `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1` only after preserving every registered sandbox and retiring the old gateway.

The wizard prompts for a provider first, then collects the provider credential if needed.
Supported non-experimental choices include NVIDIA Endpoints, OpenRouter, OpenAI, Anthropic, Google Gemini, and compatible OpenAI or Anthropic endpoints.
Supported non-experimental choices include NVIDIA Endpoints, OpenRouter, OpenAI, GitHub Copilot, Anthropic, Google Gemini, and compatible OpenAI or Anthropic endpoints.
Credentials are registered with the OpenShell gateway and never persisted to host disk.
Refer to [Credential Storage](../security/credential-storage) for details on inspection, rotation, and migration from earlier releases.
The legacy `$$nemoclaw setup` command is deprecated; use `$$nemoclaw onboard` instead.
Expand Down Expand Up @@ -3188,7 +3188,7 @@ Set them before running `$$nemoclaw onboard`.

| Variable | Format | Effect |
|----------|--------|--------|
| `NEMOCLAW_PROVIDER` | provider key (e.g. `build`, `openrouter`, `openai`, `anthropic`, `anthropicCompatible`, `gemini`, `ollama`, `custom`, `vllm`, `nim-local`, `routed`, `hermes-provider`, `install-vllm`, `install-ollama`, `install-windows-ollama`, `start-windows-ollama`) | Selects the inference provider during onboarding. The wizard skips the provider menu in both interactive and non-interactive runs when this is set. Aliases: `cloud` → `build`, `open-router` / `openrouterai` → `openrouter`, `nim` → `nim-local`, `hermes` / `nous` / `nous-portal` → `hermes-provider`, `anthropiccompatible` → `anthropicCompatible`. Invalid values fail fast with the list of accepted keys. |
| `NEMOCLAW_PROVIDER` | provider key (e.g. `build`, `openrouter`, `openai`, `copilot`, `anthropic`, `anthropicCompatible`, `gemini`, `ollama`, `custom`, `vllm`, `nim-local`, `routed`, `hermes-provider`, `install-vllm`, `install-ollama`, `install-windows-ollama`, `start-windows-ollama`) | Selects the inference provider during onboarding. The wizard skips the provider menu in both interactive and non-interactive runs when this is set. Aliases: `cloud` → `build`, `open-router` / `openrouterai` → `openrouter`, `github` / `github-copilot` → `copilot`, `nim` → `nim-local`, `hermes` / `nous` / `nous-portal` → `hermes-provider`, `anthropiccompatible` → `anthropicCompatible`. Invalid values fail fast with the list of accepted keys. |
| `NEMOCLAW_TOOL_DISCLOSURE` | `progressive` or `direct` | Selects progressive tool discovery or the prior direct-exposure behavior. Defaults to `progressive`; `--tool-disclosure` takes precedence when both are set. |
| `NEMOCLAW_ENDPOINT_URL` | URL | Custom endpoint URL. Used together with `NEMOCLAW_PROVIDER=custom` for OpenAI-compatible endpoints or `NEMOCLAW_PROVIDER=anthropicCompatible` for Anthropic-compatible endpoints. |
| `NEMOCLAW_PREFERRED_API` | `completions` (currently the only honored value) | Forces the validation probe to use the `/v1/chat/completions` API path instead of the newer `/v1/responses` API. |
Expand Down
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ bootstrap_usage() {
printf " NEMOCLAW_CONFIRM_LEGACY_MANAGED_RECREATE\n"
printf " Exact JSON array of pre-fingerprint managed sandbox names\n"
printf " NEMOCLAW_PROVIDER build | openrouter | openai | anthropic | anthropicCompatible\n"
printf " | gemini | ollama | custom | nim-local | vllm | routed\n"
printf " | gemini | copilot | ollama | custom | nim-local | vllm | routed\n"
printf " | hermes-provider\n"
printf " (aliases: cloud -> build, nim -> nim-local)\n"
printf " NEMOCLAW_POLICY_MODE suggested | custom | skip\n"
Expand Down
2 changes: 1 addition & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ usage() {
printf " Example: curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_INSTALL_TAG=%s bash\n" "$INSTALL_TAG_EXAMPLE"
printf " NEMOCLAW_INSTALL_REF Exact Git ref/SHA to install\n"
printf " NEMOCLAW_PROVIDER build | openrouter | openai | anthropic | anthropicCompatible\n"
printf " | gemini | ollama | custom | nim-local | vllm | routed\n"
printf " | gemini | copilot | ollama | custom | nim-local | vllm | routed\n"
printf " | hermes-provider\n"
printf " (aliases: cloud -> build, nim -> nim-local)\n"
printf " NEMOCLAW_MODEL Inference model to configure\n"
Expand Down
24 changes: 24 additions & 0 deletions src/lib/inference/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,16 @@ describe("inference selection config", () => {
expect(getProviderSelectionConfig("gemini-api", "gemini-2.5-pro")).toEqual(
expect.objectContaining({ model: "gemini-2.5-pro", providerLabel: "Google Gemini" }),
);
expect(getProviderSelectionConfig("github-copilot", "gpt-5.4-mini")).toEqual({
endpointType: "custom",
endpointUrl: INFERENCE_ROUTE_URL,
ncpPartner: null,
model: "gpt-5.4-mini",
profile: DEFAULT_ROUTE_PROFILE,
credentialEnv: "COPILOT_GITHUB_TOKEN",
provider: "github-copilot",
providerLabel: "GitHub Copilot",
});
expect(getProviderSelectionConfig("compatible-endpoint", "openrouter/auto")).toEqual({
endpointType: "custom",
endpointUrl: INFERENCE_ROUTE_URL,
Expand Down Expand Up @@ -267,6 +277,7 @@ describe("inference selection config", () => {
"anthropic-prod",
"compatible-anthropic-endpoint",
"gemini-api",
"github-copilot",
"compatible-endpoint",
"hermes-provider",
"vllm-local",
Expand Down Expand Up @@ -301,6 +312,7 @@ describe("inference selection config", () => {
expect(getProviderSelectionConfig("openrouter-api")?.model).toBe(DEFAULT_CLOUD_MODEL);
expect(getProviderSelectionConfig("anthropic-prod")?.model).toBe("claude-sonnet-4-6");
expect(getProviderSelectionConfig("gemini-api")?.model).toBe("gemini-2.5-flash");
expect(getProviderSelectionConfig("github-copilot")?.model).toBe("gpt-5.4");
expect(getProviderSelectionConfig("compatible-endpoint")?.model).toBe("custom-model");
expect(getProviderSelectionConfig("compatible-anthropic-endpoint")?.model).toBe(
"custom-anthropic-model",
Expand Down Expand Up @@ -352,6 +364,18 @@ describe("getSandboxInferenceConfig", () => {
});
});

it("maps GitHub Copilot to the managed inference provider", () => {
expect(getSandboxInferenceConfig("gpt-5.4", "github-copilot")).toEqual({
providerKey: MANAGED_PROVIDER_ID,
primaryModelRef: `${MANAGED_PROVIDER_ID}/gpt-5.4`,
inferenceBaseUrl: INFERENCE_ROUTE_URL,
inferenceApi: "openai-completions",
inferenceCompat: {
supportsStore: false,
},
});
});

it("maps Model Router sandboxes through managed inference.local", () => {
expect(getSandboxInferenceConfig("nvidia-routed", "nvidia-router")).toEqual({
providerKey: MANAGED_PROVIDER_ID,
Expand Down
8 changes: 8 additions & 0 deletions src/lib/inference/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ export function getProviderSelectionConfig(
credentialEnv: "GEMINI_API_KEY",
providerLabel: "Google Gemini",
};
case "github-copilot":
return {
...base,
model: model || "gpt-5.4",
credentialEnv: "COPILOT_GITHUB_TOKEN",
providerLabel: "GitHub Copilot",
};
case "compatible-endpoint":
return {
...base,
Expand Down Expand Up @@ -267,6 +274,7 @@ export function getSandboxInferenceConfig(
inferenceApi = "anthropic-messages";
break;
case "gemini-api":
case "github-copilot":
case OPENROUTER_PROVIDER_NAME:
case "hermes-provider":
providerKey = MANAGED_PROVIDER_ID;
Expand Down
1 change: 1 addition & 0 deletions src/lib/inference/model-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type ModelPromptResult = string | BackToSelection;

export const REMOTE_MODEL_OPTIONS: Record<string, string[]> = {
openai: ["gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.4-pro-2026-03-05"],
copilot: ["gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano", "claude-sonnet-4-6"],
anthropic: ["claude-sonnet-4-6", "claude-haiku-4-5", "claude-opus-4-6"],
gemini: [
"gemini-3.1-pro-preview",
Expand Down
7 changes: 7 additions & 0 deletions src/lib/onboard/provider-key-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
import { resolveProviderCredential, saveCredential } from "../credentials/store";

const {
COPILOT_CREDENTIAL_ENV,
isProviderKeyCredentialCandidate,
stageCopilotCredentialEnv,
}: {
COPILOT_CREDENTIAL_ENV: string;
isProviderKeyCredentialCandidate: (value: string | null | undefined) => boolean;
stageCopilotCredentialEnv: () => boolean;
} = require("./providers");

// NEMOCLAW_PROVIDER_KEY is a permanent compatibility fallback for callers that
Expand Down Expand Up @@ -39,6 +43,9 @@ export function stageBuildProviderKeyBridge(): void {

export function stageRemoteProviderKeyBridge(credentialEnv: string | null): void {
if (!credentialEnv) return;
if (credentialEnv === COPILOT_CREDENTIAL_ENV) {
stageCopilotCredentialEnv();
}
const providerKeyHint = getProviderKeyBridgeHint();
if (!isProviderKeyCredentialCandidate(providerKeyHint)) return;
if (resolveProviderCredential(credentialEnv)) return;
Expand Down
7 changes: 5 additions & 2 deletions src/lib/onboard/provider-menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const REMOTE_PROVIDER_CONFIG = {
anthropic: { label: "Anthropic" },
anthropicCompatible: { label: "Other Anthropic-compatible endpoint" },
gemini: { label: "Google Gemini" },
copilot: { label: "GitHub Copilot" },
hermesProvider: { label: "Hermes Provider" },
};

Expand Down Expand Up @@ -54,6 +55,7 @@ describe("buildInferenceProviderMenu", () => {
"anthropic",
"anthropicCompatible",
"gemini",
"copilot",
]);
});

Expand Down Expand Up @@ -86,6 +88,7 @@ describe("buildInferenceProviderMenu", () => {
"install-ollama",
"routed",
"hermesProvider",
"copilot",
]);
expect(result.options.find((option) => option.key === "build")?.label).toBe("NVIDIA Endpoints");
expect(result.options.find((option) => option.key === "hermesProvider")?.label).toBe(
Expand All @@ -100,7 +103,7 @@ describe("buildInferenceProviderMenu", () => {
windowsHostInstallLabel: "Install Ollama on Windows host (requires Docker Desktop)",
});

expect(result.options.at(-1)).toEqual({
expect(result.options).toContainEqual({
key: "install-windows-ollama",
label: "Install Ollama on Windows host (requires Docker Desktop)",
});
Expand All @@ -116,7 +119,7 @@ describe("buildInferenceProviderMenu", () => {
reachable ? "Use Ollama on Windows host - running" : "Start Ollama on Windows host",
});

expect(result.options.at(-1)).toEqual({
expect(result.options).toContainEqual({
key: "start-windows-ollama",
label: "Use Ollama on Windows host - running",
});
Expand Down
1 change: 1 addition & 0 deletions src/lib/onboard/provider-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export function buildInferenceProviderMenu(
for (const providerKey of input.agentProviderOptions) {
pushUniqueRemoteProviderOption(options, input.remoteProviderConfig, providerKey);
}
pushUniqueRemoteProviderOption(options, input.remoteProviderConfig, "copilot");

return {
options,
Expand Down
56 changes: 56 additions & 0 deletions src/lib/onboard/providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ type RunOpenshell = (command: string[], opts?: RunOptions) => RunResult;
const {
HOSTED_INFERENCE_ENDPOINT_URL,
HOSTED_INFERENCE_MODEL,
COPILOT_CREDENTIAL_ENV,
COPILOT_CREDENTIAL_SOURCE_ENVS,
NON_INTERACTIVE_PROVIDER_ALIASES,
NON_INTERACTIVE_PROVIDER_KEYS,
REMOTE_PROVIDER_CONFIG,
Expand All @@ -18,12 +20,15 @@ const {
getRequestedProviderHint,
isProviderKeyCredentialCandidate,
providerExistsInGateway,
stageCopilotCredentialEnv,
stageHostedInferenceSourceSecretEnv,
upsertProvider,
upsertMessagingProviders,
} = require("./providers") as {
HOSTED_INFERENCE_ENDPOINT_URL: string;
HOSTED_INFERENCE_MODEL: string;
COPILOT_CREDENTIAL_ENV: string;
COPILOT_CREDENTIAL_SOURCE_ENVS: string[];
NON_INTERACTIVE_PROVIDER_ALIASES: Record<string, string>;
NON_INTERACTIVE_PROVIDER_KEYS: Set<string>;
REMOTE_PROVIDER_CONFIG: Record<
Expand Down Expand Up @@ -51,6 +56,7 @@ const {
) => string | null;
isProviderKeyCredentialCandidate: (value: string | null | undefined) => boolean;
providerExistsInGateway: (name: string, runOpenshell: RunOpenshell) => boolean;
stageCopilotCredentialEnv: () => boolean;
stageHostedInferenceSourceSecretEnv: () => boolean;
upsertProvider: (
name: string,
Expand Down Expand Up @@ -86,6 +92,9 @@ function withProviderEnv(next: Record<string, string | undefined>, testBody: ()
"NEMOCLAW_CLOUD_EXPERIMENTAL_MODEL",
"NEMOCLAW_E2E_USE_HOSTED_INFERENCE",
"COMPATIBLE_API_KEY",
"COPILOT_GITHUB_TOKEN",
"GH_TOKEN",
"GITHUB_TOKEN",
...Object.keys(next),
]);
const previous = new Map<string, string | undefined>();
Expand Down Expand Up @@ -157,6 +166,53 @@ describe("onboard provider helpers", () => {
).toContain("ANTHROPIC_BASE_URL=https://inference-api.nvidia.com");
});

it("registers GitHub Copilot through OpenShell's Copilot provider profile (#5799)", () => {
const provider = REMOTE_PROVIDER_CONFIG.copilot;

expect(provider).toMatchObject({
providerName: "github-copilot",
providerType: "copilot",
credentialEnv: "COPILOT_GITHUB_TOKEN",
});
expect(NON_INTERACTIVE_PROVIDER_KEYS.has("copilot")).toBe(true);
expect(NON_INTERACTIVE_PROVIDER_ALIASES.github).toBe("copilot");
expect(NON_INTERACTIVE_PROVIDER_ALIASES["github-copilot"]).toBe("copilot");
expect(
buildProviderArgs(
"create",
provider.providerName,
provider.providerType,
provider.credentialEnv,
"https://api.githubcopilot.com",
),
).not.toContain("--config");
});

it("stages GitHub token aliases into the Copilot credential env (#5799)", () => {
expect(COPILOT_CREDENTIAL_ENV).toBe("COPILOT_GITHUB_TOKEN");
expect(COPILOT_CREDENTIAL_SOURCE_ENVS).toEqual(["GH_TOKEN", "GITHUB_TOKEN"]);

withProviderEnv({ GH_TOKEN: "ghp-test-token" }, () => {
expect(stageCopilotCredentialEnv()).toBe(true);
expect(process.env.COPILOT_GITHUB_TOKEN).toBe("ghp-test-token");
});

withProviderEnv({ GITHUB_TOKEN: "github-fallback-token" }, () => {
expect(stageCopilotCredentialEnv()).toBe(true);
expect(process.env.COPILOT_GITHUB_TOKEN).toBe("github-fallback-token");
});

withProviderEnv({ GH_TOKEN: "gh-token", GITHUB_TOKEN: "github-token" }, () => {
expect(stageCopilotCredentialEnv()).toBe(true);
expect(process.env.COPILOT_GITHUB_TOKEN).toBe("gh-token");
});

withProviderEnv({ COPILOT_GITHUB_TOKEN: "explicit-token", GH_TOKEN: "ignored-token" }, () => {
expect(stageCopilotCredentialEnv()).toBe(false);
expect(process.env.COPILOT_GITHUB_TOKEN).toBe("explicit-token");
});
});

it("builds create arguments for generic providers", () => {
const args = buildProviderArgs(
"create",
Expand Down
Loading
Loading