diff --git a/docs/inference/choose-inference-provider.mdx b/docs/inference/choose-inference-provider.mdx index 614f67c32ea..d8081afd8d4 100644 --- a/docs/inference/choose-inference-provider.mdx +++ b/docs/inference/choose-inference-provider.mdx @@ -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. | @@ -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) | diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index cc217d27a50..dd9b511c5d2 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -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. @@ -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. | diff --git a/install.sh b/install.sh index 44619f21d4c..2d062d2f333 100755 --- a/install.sh +++ b/install.sh @@ -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" diff --git a/scripts/install.sh b/scripts/install.sh index 89160af13de..4ff06cf2cde 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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" diff --git a/src/lib/inference/config.test.ts b/src/lib/inference/config.test.ts index e3f9a494c3d..0ca8ad5a4d1 100644 --- a/src/lib/inference/config.test.ts +++ b/src/lib/inference/config.test.ts @@ -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, @@ -267,6 +277,7 @@ describe("inference selection config", () => { "anthropic-prod", "compatible-anthropic-endpoint", "gemini-api", + "github-copilot", "compatible-endpoint", "hermes-provider", "vllm-local", @@ -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", @@ -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, diff --git a/src/lib/inference/config.ts b/src/lib/inference/config.ts index 5b6e0d90d00..b25f57ac1cc 100644 --- a/src/lib/inference/config.ts +++ b/src/lib/inference/config.ts @@ -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, @@ -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; diff --git a/src/lib/inference/model-prompts.ts b/src/lib/inference/model-prompts.ts index 3abff9b8caf..caafe3b339e 100644 --- a/src/lib/inference/model-prompts.ts +++ b/src/lib/inference/model-prompts.ts @@ -17,6 +17,7 @@ export type ModelPromptResult = string | BackToSelection; export const REMOTE_MODEL_OPTIONS: Record = { 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", diff --git a/src/lib/onboard/provider-key-bridge.ts b/src/lib/onboard/provider-key-bridge.ts index 5f158cadfdd..10572a74968 100644 --- a/src/lib/onboard/provider-key-bridge.ts +++ b/src/lib/onboard/provider-key-bridge.ts @@ -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 @@ -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; diff --git a/src/lib/onboard/provider-menu.test.ts b/src/lib/onboard/provider-menu.test.ts index e1fcf94d899..7f3224898e4 100644 --- a/src/lib/onboard/provider-menu.test.ts +++ b/src/lib/onboard/provider-menu.test.ts @@ -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" }, }; @@ -54,6 +55,7 @@ describe("buildInferenceProviderMenu", () => { "anthropic", "anthropicCompatible", "gemini", + "copilot", ]); }); @@ -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( @@ -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)", }); @@ -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", }); diff --git a/src/lib/onboard/provider-menu.ts b/src/lib/onboard/provider-menu.ts index ebaecfa13fb..082fddeff25 100644 --- a/src/lib/onboard/provider-menu.ts +++ b/src/lib/onboard/provider-menu.ts @@ -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, diff --git a/src/lib/onboard/providers.test.ts b/src/lib/onboard/providers.test.ts index b6953b7476b..2da12bcd5c7 100644 --- a/src/lib/onboard/providers.test.ts +++ b/src/lib/onboard/providers.test.ts @@ -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, @@ -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; NON_INTERACTIVE_PROVIDER_KEYS: Set; REMOTE_PROVIDER_CONFIG: Record< @@ -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, @@ -86,6 +92,9 @@ function withProviderEnv(next: Record, 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(); @@ -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", diff --git a/src/lib/onboard/providers.ts b/src/lib/onboard/providers.ts index b9e6125140e..4625037d85e 100644 --- a/src/lib/onboard/providers.ts +++ b/src/lib/onboard/providers.ts @@ -25,6 +25,9 @@ const OPENAI_ENDPOINT_URL = "https://api.openai.com/v1"; const ANTHROPIC_ENDPOINT_URL = "https://api.anthropic.com"; const GEMINI_ENDPOINT_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"; const HERMES_INFERENCE_ENDPOINT_URL = "https://inference-api.nousresearch.com/v1"; +const COPILOT_ENDPOINT_URL = "https://api.githubcopilot.com"; +const COPILOT_CREDENTIAL_ENV = "COPILOT_GITHUB_TOKEN"; +const COPILOT_CREDENTIAL_SOURCE_ENVS = ["GH_TOKEN", "GITHUB_TOKEN"]; const HOSTED_INFERENCE_SOURCE_ENV = "NVIDIA_INFERENCE_API_KEY"; const HOSTED_INFERENCE_PROVIDER_KEY_ENV = "NEMOCLAW_PROVIDER_KEY"; const HOSTED_INFERENCE_CREDENTIAL_ENV = "COMPATIBLE_API_KEY"; @@ -39,6 +42,8 @@ const NON_INTERACTIVE_PROVIDER_ALIASES = { vllm: "vllm", "open-router": "openrouter", openrouterai: "openrouter", + github: "copilot", + "github-copilot": "copilot", anthropiccompatible: "anthropicCompatible", hermes: "hermesProvider", "hermes-provider": "hermesProvider", @@ -53,6 +58,7 @@ const NON_INTERACTIVE_PROVIDER_KEYS = new Set([ "anthropic", "anthropicCompatible", "gemini", + "copilot", "hermesProvider", "ollama", "custom", @@ -65,7 +71,7 @@ const NON_INTERACTIVE_PROVIDER_KEYS = new Set([ "start-windows-ollama", ]); const NON_INTERACTIVE_PROVIDER_VALID_VALUES = - "Valid values: build, openrouter, openai, anthropic, anthropicCompatible, gemini, hermes-provider, ollama, custom, nim-local, vllm, routed, install-vllm, install-ollama, install-windows-ollama, start-windows-ollama"; + "Valid values: build, openrouter, openai, anthropic, anthropicCompatible, gemini, copilot, hermes-provider, ollama, custom, nim-local, vllm, routed, install-vllm, install-ollama, install-windows-ollama, start-windows-ollama"; const PROVIDER_KEY_ROUTE_VALUES = new Set( [ "inference", @@ -139,6 +145,17 @@ const REMOTE_PROVIDER_CONFIG = { defaultModel: "gemini-2.5-flash", skipVerify: true, }, + copilot: { + label: "GitHub Copilot", + providerName: "github-copilot", + providerType: "copilot", + credentialEnv: COPILOT_CREDENTIAL_ENV, + endpointUrl: COPILOT_ENDPOINT_URL, + helpUrl: "https://docs.github.com/copilot", + modelMode: "curated", + defaultModel: "gpt-5.4", + skipVerify: true, + }, // Hermes Provider is a single menu entry by design: every model family it // serves (Moonshot, Z-AI, MiniMax, Qwen, Xiaomi, Tencent, StepFun, xAI, // Arcee) routes through the same Nous portal endpoint and the same @@ -300,6 +317,24 @@ function stageHostedInferenceSourceSecretEnv() { return true; } +function stageCopilotCredentialEnv() { + const current = normalizeCredentialValue( + // check-direct-credential-env-ignore -- Copilot's canonical env wins over generic GitHub token aliases. + process.env[COPILOT_CREDENTIAL_ENV] ?? "", + ); + if (current) return false; + for (const sourceEnv of COPILOT_CREDENTIAL_SOURCE_ENVS) { + const value = normalizeCredentialValue( + // check-direct-credential-env-ignore -- Copilot provider accepts GitHub token aliases documented by OpenShell. + process.env[sourceEnv] ?? "", + ); + if (!value) continue; + process.env[COPILOT_CREDENTIAL_ENV] = value; + return true; + } + return false; +} + function isHostedInferenceProviderKeyCredentialCandidate(value) { if (!value) return false; return !PROVIDER_KEY_ROUTE_VALUES.has(value.trim().toLowerCase()); @@ -494,6 +529,9 @@ module.exports = { OPENAI_ENDPOINT_URL, ANTHROPIC_ENDPOINT_URL, GEMINI_ENDPOINT_URL, + COPILOT_ENDPOINT_URL, + COPILOT_CREDENTIAL_ENV, + COPILOT_CREDENTIAL_SOURCE_ENVS, REMOTE_PROVIDER_CONFIG, LOCAL_INFERENCE_PROVIDERS, OLLAMA_PROXY_CREDENTIAL_ENV, @@ -508,6 +546,7 @@ module.exports = { getProviderLabel, getEffectiveProviderName, stageHostedInferenceSourceSecretEnv, + stageCopilotCredentialEnv, getNonInteractiveProvider, getNonInteractiveModel, getRequestedProviderHint, diff --git a/test/onboard-provider-key-bridge.test.ts b/test/onboard-provider-key-bridge.test.ts index fbf0ac69ad8..b47558c9808 100644 --- a/test/onboard-provider-key-bridge.test.ts +++ b/test/onboard-provider-key-bridge.test.ts @@ -15,7 +15,10 @@ const TRACKED_ENV_KEYS = [ "ANTHROPIC_API_KEY", "COMPATIBLE_API_KEY", "COMPATIBLE_ANTHROPIC_API_KEY", + "COPILOT_GITHUB_TOKEN", "GEMINI_API_KEY", + "GH_TOKEN", + "GITHUB_TOKEN", "NEMOCLAW_MODEL", "NEMOCLAW_NON_INTERACTIVE", "NEMOCLAW_PROVIDER", @@ -196,6 +199,23 @@ describe("onboard provider-key compatibility bridges", () => { }, ); + it( + "stages GitHub token aliases before Copilot remote provider validation", + testTimeoutOptions(90_000), + () => { + const payload = runSetupNimBridgeScenario({ + NEMOCLAW_PROVIDER: "copilot", + GH_TOKEN: "ghp-copilot-fallback", + NEMOCLAW_PROVIDER_KEY: "legacy-provider-key", + }); + + assert.equal(payload.outcome, "completed"); + assert.equal(payload.result?.provider, "github-copilot"); + assert.equal(payload.result?.credentialEnv, "COPILOT_GITHUB_TOKEN"); + assert.equal(payload.env.COPILOT_GITHUB_TOKEN, "ghp-copilot-fallback"); + }, + ); + it( "does not copy selector-like NEMOCLAW_PROVIDER_KEY into remote-provider credential env", testTimeoutOptions(90_000), diff --git a/test/onboard-resume-provider-recovery.test.ts b/test/onboard-resume-provider-recovery.test.ts index c9325d588e5..477644dbbd9 100644 --- a/test/onboard-resume-provider-recovery.test.ts +++ b/test/onboard-resume-provider-recovery.test.ts @@ -81,6 +81,7 @@ describe("providerNameToOptionKey", () => { expect(providerNameToOptionKey("openai-api")).toBe("openai"); expect(providerNameToOptionKey("anthropic-prod")).toBe("anthropic"); expect(providerNameToOptionKey("gemini-api")).toBe("gemini"); + expect(providerNameToOptionKey("github-copilot")).toBe("copilot"); expect(providerNameToOptionKey("compatible-endpoint")).toBe("custom"); expect(providerNameToOptionKey("compatible-anthropic-endpoint")).toBe("anthropicCompatible"); });