feat(github-copilot): add embedding provider for memory search (#61718)

Merged via squash.

Prepared head SHA: 05a78ce7f2
Co-authored-by: feiskyer <676637+feiskyer@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
This commit is contained in:
Pengfei Ni
2026-04-15 17:39:28 +08:00
committed by GitHub
parent 7821fae05d
commit 88d3620a85
14 changed files with 1094 additions and 69 deletions

View File

@@ -0,0 +1,96 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn());
const listProfilesForProviderMock = vi.hoisted(() => vi.fn());
const coerceSecretRefMock = vi.hoisted(() => vi.fn());
const resolveRequiredConfiguredSecretRefInputStringMock = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
coerceSecretRef: coerceSecretRefMock,
ensureAuthProfileStore: ensureAuthProfileStoreMock,
listProfilesForProvider: listProfilesForProviderMock,
}));
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
resolveRequiredConfiguredSecretRefInputString: resolveRequiredConfiguredSecretRefInputStringMock,
}));
import { resolveFirstGithubToken } from "./auth.js";
describe("resolveFirstGithubToken", () => {
beforeEach(() => {
ensureAuthProfileStoreMock.mockReturnValue({
profiles: {
"github-copilot:github": {
type: "token",
tokenRef: { source: "file", provider: "default", id: "/providers/github-copilot/token" },
},
},
});
listProfilesForProviderMock.mockReturnValue(["github-copilot:github"]);
coerceSecretRefMock.mockReturnValue({
source: "file",
provider: "default",
id: "/providers/github-copilot/token",
});
resolveRequiredConfiguredSecretRefInputStringMock.mockResolvedValue("resolved-profile-token");
});
afterEach(() => {
vi.restoreAllMocks();
ensureAuthProfileStoreMock.mockReset();
listProfilesForProviderMock.mockReset();
coerceSecretRefMock.mockReset();
resolveRequiredConfiguredSecretRefInputStringMock.mockReset();
});
it("prefers env tokens when available", async () => {
const result = await resolveFirstGithubToken({
env: { GH_TOKEN: "env-token" } as NodeJS.ProcessEnv,
});
expect(result).toEqual({
githubToken: "env-token",
hasProfile: true,
});
expect(resolveRequiredConfiguredSecretRefInputStringMock).not.toHaveBeenCalled();
});
it("returns direct profile tokens before resolving SecretRefs", async () => {
ensureAuthProfileStoreMock.mockReturnValue({
profiles: {
"github-copilot:github": {
type: "token",
token: "profile-token",
},
},
});
coerceSecretRefMock.mockReturnValue(null);
const result = await resolveFirstGithubToken({
env: {} as NodeJS.ProcessEnv,
});
expect(result).toEqual({
githubToken: "profile-token",
hasProfile: true,
});
});
it("resolves non-env SecretRefs when config is available", async () => {
const result = await resolveFirstGithubToken({
config: { secrets: { defaults: { provider: "default" } } } as never,
env: {} as NodeJS.ProcessEnv,
});
expect(result).toEqual({
githubToken: "resolved-profile-token",
hasProfile: true,
});
expect(resolveRequiredConfiguredSecretRefInputStringMock).toHaveBeenCalledWith(
expect.objectContaining({
path: "providers.github-copilot.authProfiles.github-copilot:github.tokenRef",
}),
);
});
});

View File

@@ -0,0 +1,65 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { resolveRequiredConfiguredSecretRefInputString } from "openclaw/plugin-sdk/config-runtime";
import {
coerceSecretRef,
ensureAuthProfileStore,
listProfilesForProvider,
} from "openclaw/plugin-sdk/provider-auth";
import { PROVIDER_ID } from "./models.js";
export async function resolveFirstGithubToken(params: {
agentDir?: string;
config?: OpenClawConfig;
env: NodeJS.ProcessEnv;
}): Promise<{
githubToken: string;
hasProfile: boolean;
}> {
const authStore = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const profileIds = listProfilesForProvider(authStore, PROVIDER_ID);
const hasProfile = profileIds.length > 0;
const envToken =
params.env.COPILOT_GITHUB_TOKEN ?? params.env.GH_TOKEN ?? params.env.GITHUB_TOKEN ?? "";
const githubToken = envToken.trim();
if (githubToken || !hasProfile) {
return { githubToken, hasProfile };
}
const profileId = profileIds[0];
const profile = profileId ? authStore.profiles[profileId] : undefined;
if (profile?.type !== "token") {
return { githubToken: "", hasProfile };
}
const directToken = profile.token?.trim() ?? "";
if (directToken) {
return { githubToken: directToken, hasProfile };
}
const tokenRef = coerceSecretRef(profile.tokenRef);
if (tokenRef?.source === "env" && tokenRef.id.trim()) {
return {
githubToken: (params.env[tokenRef.id] ?? process.env[tokenRef.id] ?? "").trim(),
hasProfile,
};
}
if (tokenRef && params.config) {
try {
const resolved = await resolveRequiredConfiguredSecretRefInputString({
config: params.config,
env: params.env,
value: profile.tokenRef,
path: `providers.github-copilot.authProfiles.${profileId ?? "default"}.tokenRef`,
});
return {
githubToken: resolved?.trim() ?? "",
hasProfile,
};
} catch {
return { githubToken: "", hasProfile };
}
}
return { githubToken: "", hasProfile };
}

View File

@@ -0,0 +1,279 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const resolveFirstGithubTokenMock = vi.hoisted(() => vi.fn());
const resolveCopilotApiTokenMock = vi.hoisted(() => vi.fn());
const resolveConfiguredSecretInputStringMock = vi.hoisted(() => vi.fn());
const fetchWithSsrFGuardMock = vi.hoisted(() => vi.fn());
const createGitHubCopilotEmbeddingProviderMock = vi.hoisted(() => vi.fn());
vi.mock("./auth.js", () => ({
resolveFirstGithubToken: resolveFirstGithubTokenMock,
}));
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
resolveConfiguredSecretInputString: resolveConfiguredSecretInputStringMock,
}));
vi.mock("openclaw/plugin-sdk/github-copilot-token", () => ({
DEFAULT_COPILOT_API_BASE_URL: "https://api.githubcopilot.test",
resolveCopilotApiToken: resolveCopilotApiTokenMock,
}));
vi.mock("openclaw/plugin-sdk/memory-core-host-engine-embeddings", () => ({
createGitHubCopilotEmbeddingProvider: createGitHubCopilotEmbeddingProviderMock,
}));
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
fetchWithSsrFGuard: fetchWithSsrFGuardMock,
}));
import { githubCopilotMemoryEmbeddingProviderAdapter } from "./embeddings.js";
const TEST_BASE_URL = "https://api.githubcopilot.test";
function buildModelsResponse(models: Array<{ id: string; supported_endpoints?: unknown }>) {
return { data: models };
}
function mockDiscoveryResponse(spec: {
ok: boolean;
status?: number;
json?: unknown;
text?: string;
}) {
fetchWithSsrFGuardMock.mockImplementationOnce(async () => ({
response: {
ok: spec.ok,
status: spec.status ?? (spec.ok ? 200 : 500),
json: async () => spec.json,
text: async () => spec.text ?? "",
},
release: vi.fn(async () => {}),
}));
}
function defaultCreateOptions() {
return {
config: {} as Record<string, unknown>,
agentDir: "/tmp/test-agent",
model: "",
};
}
describe("githubCopilotMemoryEmbeddingProviderAdapter", () => {
beforeEach(() => {
resolveConfiguredSecretInputStringMock.mockResolvedValue({});
resolveFirstGithubTokenMock.mockResolvedValue({
githubToken: "gh_test_token_123",
hasProfile: false,
});
resolveCopilotApiTokenMock.mockResolvedValue({
token: "copilot_test_token_abc",
expiresAt: Date.now() + 3_600_000,
source: "test",
baseUrl: TEST_BASE_URL,
});
createGitHubCopilotEmbeddingProviderMock.mockImplementation(async (client) => ({
provider: {
id: "github-copilot",
model: client.model,
embedQuery: async () => [0.1, 0.2, 0.3],
embedBatch: async (texts: string[]) => texts.map(() => [0.1, 0.2, 0.3]),
},
client,
}));
});
afterEach(() => {
vi.restoreAllMocks();
resolveConfiguredSecretInputStringMock.mockReset();
resolveFirstGithubTokenMock.mockReset();
resolveCopilotApiTokenMock.mockReset();
createGitHubCopilotEmbeddingProviderMock.mockReset();
fetchWithSsrFGuardMock.mockReset();
});
it("registers the expected adapter metadata", () => {
expect(githubCopilotMemoryEmbeddingProviderAdapter.id).toBe("github-copilot");
expect(githubCopilotMemoryEmbeddingProviderAdapter.transport).toBe("remote");
expect(githubCopilotMemoryEmbeddingProviderAdapter.autoSelectPriority).toBe(15);
expect(githubCopilotMemoryEmbeddingProviderAdapter.allowExplicitWhenConfiguredAuto).toBe(true);
});
it("picks text-embedding-3-small when available", async () => {
mockDiscoveryResponse({
ok: true,
json: buildModelsResponse([
{ id: "text-embedding-3-large", supported_endpoints: ["/v1/embeddings"] },
{ id: "text-embedding-3-small", supported_endpoints: ["/v1/embeddings"] },
{ id: "gpt-4o", supported_endpoints: ["/v1/chat/completions"] },
]),
});
const result = await githubCopilotMemoryEmbeddingProviderAdapter.create(defaultCreateOptions());
expect(result.provider?.model).toBe("text-embedding-3-small");
expect(createGitHubCopilotEmbeddingProviderMock).toHaveBeenCalledWith(
expect.objectContaining({
baseUrl: TEST_BASE_URL,
githubToken: "gh_test_token_123",
model: "text-embedding-3-small",
}),
);
});
it("matches embedding-capable models when supported_endpoints is missing or malformed", async () => {
mockDiscoveryResponse({
ok: true,
json: buildModelsResponse([
{ id: "gpt-4o", supported_endpoints: { broken: true } },
{ id: "text-embedding-3-small", supported_endpoints: [] },
{ id: "text-embedding-ada-002" },
]),
});
const result = await githubCopilotMemoryEmbeddingProviderAdapter.create(defaultCreateOptions());
expect(result.provider?.model).toBe("text-embedding-3-small");
});
it("strips the provider prefix from a user-selected model", async () => {
mockDiscoveryResponse({
ok: true,
json: buildModelsResponse([
{ id: "text-embedding-3-small", supported_endpoints: ["/v1/embeddings"] },
]),
});
const result = await githubCopilotMemoryEmbeddingProviderAdapter.create({
...defaultCreateOptions(),
model: "github-copilot/text-embedding-3-small",
} as never);
expect(result.provider?.model).toBe("text-embedding-3-small");
});
it("throws when the user-selected model is unavailable", async () => {
mockDiscoveryResponse({
ok: true,
json: buildModelsResponse([
{ id: "text-embedding-3-small", supported_endpoints: ["/v1/embeddings"] },
]),
});
await expect(
githubCopilotMemoryEmbeddingProviderAdapter.create({
...defaultCreateOptions(),
model: "gpt-4o",
} as never),
).rejects.toThrow('GitHub Copilot embedding model "gpt-4o" is not available');
});
it("throws when discovery finds no embedding models", async () => {
mockDiscoveryResponse({
ok: true,
json: buildModelsResponse([{ id: "gpt-4o", supported_endpoints: ["/v1/chat/completions"] }]),
});
await expect(
githubCopilotMemoryEmbeddingProviderAdapter.create(defaultCreateOptions()),
).rejects.toThrow("No embedding models available from GitHub Copilot");
});
it("wraps invalid discovery JSON as a setup error", async () => {
fetchWithSsrFGuardMock.mockImplementationOnce(async () => ({
response: {
ok: true,
status: 200,
json: async () => {
throw new SyntaxError("bad json");
},
text: async () => "",
},
release: vi.fn(async () => {}),
}));
await expect(
githubCopilotMemoryEmbeddingProviderAdapter.create(defaultCreateOptions()),
).rejects.toThrow("GitHub Copilot model discovery returned invalid JSON");
});
it("honors remote overrides when creating the provider", async () => {
resolveConfiguredSecretInputStringMock.mockResolvedValue({ value: "gh_remote_token" });
mockDiscoveryResponse({
ok: true,
json: buildModelsResponse([
{ id: "text-embedding-3-small", supported_endpoints: ["/v1/embeddings"] },
]),
});
await githubCopilotMemoryEmbeddingProviderAdapter.create({
...defaultCreateOptions(),
remote: {
apiKey: "ignored-at-runtime",
baseUrl: "https://proxy.example/v1",
headers: { "X-Proxy-Token": "proxy" },
},
} as never);
expect(resolveFirstGithubTokenMock).toHaveBeenCalled();
expect(createGitHubCopilotEmbeddingProviderMock).toHaveBeenCalledWith({
baseUrl: "https://proxy.example/v1",
env: process.env,
fetchImpl: fetch,
githubToken: "gh_remote_token",
headers: { "X-Proxy-Token": "proxy" },
model: "text-embedding-3-small",
});
const discoveryCall = fetchWithSsrFGuardMock.mock.calls[0]?.[0] as {
init: { headers: Record<string, string> };
url: string;
};
expect(discoveryCall.url).toBe("https://proxy.example/v1/models");
expect(discoveryCall.init.headers["X-Proxy-Token"]).toBe("proxy");
});
it("includes provider, baseUrl, and model in runtime cache data", async () => {
mockDiscoveryResponse({
ok: true,
json: buildModelsResponse([
{ id: "text-embedding-3-small", supported_endpoints: ["/v1/embeddings"] },
]),
});
const result = await githubCopilotMemoryEmbeddingProviderAdapter.create(defaultCreateOptions());
expect(result.runtime).toEqual({
id: "github-copilot",
cacheKeyData: {
provider: "github-copilot",
baseUrl: TEST_BASE_URL,
model: "text-embedding-3-small",
},
});
});
it("treats token parsing and discovery failures as auto-fallback errors", () => {
expect(
githubCopilotMemoryEmbeddingProviderAdapter.shouldContinueAutoSelection!(
new Error("Copilot token response missing token"),
),
).toBe(true);
expect(
githubCopilotMemoryEmbeddingProviderAdapter.shouldContinueAutoSelection!(
new Error("Unexpected response from GitHub Copilot token endpoint"),
),
).toBe(true);
expect(
githubCopilotMemoryEmbeddingProviderAdapter.shouldContinueAutoSelection!(
new Error("GitHub Copilot model discovery returned invalid JSON"),
),
).toBe(true);
expect(
githubCopilotMemoryEmbeddingProviderAdapter.shouldContinueAutoSelection!(
new Error("Network timeout"),
),
).toBe(false);
});
});

View File

@@ -0,0 +1,215 @@
import { resolveConfiguredSecretInputString } from "openclaw/plugin-sdk/config-runtime";
import {
DEFAULT_COPILOT_API_BASE_URL,
resolveCopilotApiToken,
} from "openclaw/plugin-sdk/github-copilot-token";
import {
createGitHubCopilotEmbeddingProvider,
type MemoryEmbeddingProviderAdapter,
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
import { resolveFirstGithubToken } from "./auth.js";
const COPILOT_EMBEDDING_PROVIDER_ID = "github-copilot";
/**
* Preferred embedding models in order. The first available model wins.
*/
const PREFERRED_MODELS = [
"text-embedding-3-small",
"text-embedding-3-large",
"text-embedding-ada-002",
] as const;
const COPILOT_HEADERS_STATIC: Record<string, string> = {
"Content-Type": "application/json",
"Editor-Version": "vscode/1.96.2",
"User-Agent": "GitHubCopilotChat/0.26.7",
};
function buildSsrfPolicy(baseUrl: string): SsrFPolicy | undefined {
try {
const parsed = new URL(baseUrl);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return undefined;
}
return { allowedHostnames: [parsed.hostname] };
} catch {
return undefined;
}
}
type CopilotModelEntry = {
id?: unknown;
supported_endpoints?: unknown;
};
function isCopilotSetupError(err: unknown): boolean {
if (!(err instanceof Error)) {
return false;
}
// All Copilot-specific setup failures should allow auto-selection to
// fall through to the next provider (e.g. OpenAI). This covers: missing
// GitHub token, token exchange failures, no embedding models on the plan,
// model discovery errors, and user-pinned model not available on Copilot.
return (
err.message.includes("No GitHub token available") ||
err.message.includes("Copilot token exchange failed") ||
err.message.includes("Copilot token response") ||
err.message.includes("No embedding models available") ||
err.message.includes("GitHub Copilot model discovery") ||
err.message.includes("GitHub Copilot embedding model") ||
err.message.includes("Unexpected response from GitHub Copilot token endpoint")
);
}
async function discoverEmbeddingModels(params: {
baseUrl: string;
copilotToken: string;
headers?: Record<string, string>;
ssrfPolicy?: SsrFPolicy;
}): Promise<string[]> {
const url = `${params.baseUrl.replace(/\/$/, "")}/models`;
const { response, release } = await fetchWithSsrFGuard({
url,
init: {
method: "GET",
headers: {
...COPILOT_HEADERS_STATIC,
...params.headers,
Authorization: `Bearer ${params.copilotToken}`,
},
},
policy: params.ssrfPolicy,
auditContext: "memory-remote",
});
try {
if (!response.ok) {
throw new Error(
`GitHub Copilot model discovery HTTP ${response.status}: ${await response.text()}`,
);
}
let payload: unknown;
try {
payload = await response.json();
} catch {
throw new Error("GitHub Copilot model discovery returned invalid JSON");
}
const allModels = Array.isArray((payload as { data?: unknown })?.data)
? ((payload as { data: CopilotModelEntry[] }).data ?? [])
: [];
// Filter for embedding models. The Copilot API may list embedding models
// with an explicit /v1/embeddings endpoint, or with an empty
// supported_endpoints array. Match both: endpoint-declared embedding
// models and models whose ID indicates embedding capability.
return allModels.flatMap((entry) => {
const id = typeof entry.id === "string" ? entry.id.trim() : "";
if (!id) {
return [];
}
const endpoints = Array.isArray(entry.supported_endpoints)
? entry.supported_endpoints.filter((value): value is string => typeof value === "string")
: [];
return endpoints.some((ep) => ep.includes("embeddings")) || /\bembedding/i.test(id)
? [id]
: [];
});
} finally {
await release();
}
}
function pickBestModel(available: string[], userModel?: string): string {
if (userModel) {
const normalized = userModel.trim();
// Strip the provider prefix if users set "github-copilot/model-name".
const stripped = normalized.startsWith(`${COPILOT_EMBEDDING_PROVIDER_ID}/`)
? normalized.slice(`${COPILOT_EMBEDDING_PROVIDER_ID}/`.length)
: normalized;
if (available.length === 0) {
throw new Error("No embedding models available from GitHub Copilot");
}
if (!available.includes(stripped)) {
throw new Error(
`GitHub Copilot embedding model "${stripped}" is not available. Available: ${available.join(", ")}`,
);
}
return stripped;
}
for (const preferred of PREFERRED_MODELS) {
if (available.includes(preferred)) {
return preferred;
}
}
if (available.length > 0) {
return available[0];
}
throw new Error("No embedding models available from GitHub Copilot");
}
export const githubCopilotMemoryEmbeddingProviderAdapter: MemoryEmbeddingProviderAdapter = {
id: COPILOT_EMBEDDING_PROVIDER_ID,
transport: "remote",
autoSelectPriority: 15,
allowExplicitWhenConfiguredAuto: true,
shouldContinueAutoSelection: (err: unknown) => isCopilotSetupError(err),
create: async (options) => {
const remoteGithubToken = await resolveConfiguredSecretInputString({
config: options.config,
env: process.env,
value: options.remote?.apiKey,
path: "agents.*.memorySearch.remote.apiKey",
});
const { githubToken: profileGithubToken } = await resolveFirstGithubToken({
agentDir: options.agentDir,
config: options.config,
env: process.env,
});
const githubToken = remoteGithubToken.value || profileGithubToken;
if (!githubToken) {
throw new Error("No GitHub token available for Copilot embedding provider");
}
const { token: copilotToken, baseUrl: resolvedBaseUrl } = await resolveCopilotApiToken({
githubToken,
env: process.env,
});
const baseUrl =
options.remote?.baseUrl?.trim() || resolvedBaseUrl || DEFAULT_COPILOT_API_BASE_URL;
const ssrfPolicy = buildSsrfPolicy(baseUrl);
// Always discover models even when the user pins one: this validates
// the Copilot token and confirms the plan supports embeddings before
// we attempt any embedding requests.
const availableModels = await discoverEmbeddingModels({
baseUrl,
copilotToken,
headers: options.remote?.headers,
ssrfPolicy,
});
const userModel = options.model?.trim() || undefined;
const model = pickBestModel(availableModels, userModel);
const { provider } = await createGitHubCopilotEmbeddingProvider({
baseUrl,
env: process.env,
fetchImpl: fetch,
githubToken,
headers: options.remote?.headers,
model,
});
return {
provider,
runtime: {
id: COPILOT_EMBEDDING_PROVIDER_ID,
cacheKeyData: {
provider: COPILOT_EMBEDDING_PROVIDER_ID,
baseUrl,
model,
},
},
};
},
};

View File

@@ -36,6 +36,27 @@ function registerProviderWithPluginConfig(pluginConfig: Record<string, unknown>)
}
describe("github-copilot plugin", () => {
it("registers embedding provider", () => {
const registerMemoryEmbeddingProviderMock = vi.fn();
plugin.register(
createTestPluginApi({
id: "github-copilot",
name: "GitHub Copilot",
source: "test",
config: {},
pluginConfig: {},
runtime: {} as never,
registerProvider: vi.fn(),
registerMemoryEmbeddingProvider: registerMemoryEmbeddingProviderMock,
}),
);
expect(registerMemoryEmbeddingProviderMock).toHaveBeenCalledTimes(1);
const adapter = registerMemoryEmbeddingProviderMock.mock.calls[0]?.[0];
expect(adapter.id).toBe("github-copilot");
});
it("skips catalog discovery when plugin discovery is disabled", async () => {
const provider = registerProviderWithPluginConfig({ discovery: { enabled: false } });

View File

@@ -1,10 +1,8 @@
import { definePluginEntry, type ProviderAuthContext } from "openclaw/plugin-sdk/plugin-entry";
import {
coerceSecretRef,
ensureAuthProfileStore,
listProfilesForProvider,
} from "openclaw/plugin-sdk/provider-auth";
import { ensureAuthProfileStore } from "openclaw/plugin-sdk/provider-auth";
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
import { resolveFirstGithubToken } from "./auth.js";
import { githubCopilotMemoryEmbeddingProviderAdapter } from "./embeddings.js";
import { PROVIDER_ID, resolveCopilotForwardCompatModel } from "./models.js";
import { buildGithubCopilotReplayPolicy } from "./replay-policy.js";
import { wrapCopilotProviderStream } from "./stream.js";
@@ -27,39 +25,6 @@ export default definePluginEntry({
description: "Bundled GitHub Copilot provider plugin",
register(api) {
const pluginConfig = (api.pluginConfig ?? {}) as GithubCopilotPluginConfig;
function resolveFirstGithubToken(params: { agentDir?: string; env: NodeJS.ProcessEnv }): {
githubToken: string;
hasProfile: boolean;
} {
const authStore = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const hasProfile = listProfilesForProvider(authStore, PROVIDER_ID).length > 0;
const envToken =
params.env.COPILOT_GITHUB_TOKEN ?? params.env.GH_TOKEN ?? params.env.GITHUB_TOKEN ?? "";
const githubToken = envToken.trim();
if (githubToken || !hasProfile) {
return { githubToken, hasProfile };
}
const profileId = listProfilesForProvider(authStore, PROVIDER_ID)[0];
const profile = profileId ? authStore.profiles[profileId] : undefined;
if (profile?.type !== "token") {
return { githubToken: "", hasProfile };
}
const directToken = profile.token?.trim() ?? "";
if (directToken) {
return { githubToken: directToken, hasProfile };
}
const tokenRef = coerceSecretRef(profile.tokenRef);
if (tokenRef?.source === "env" && tokenRef.id.trim()) {
return {
githubToken: (params.env[tokenRef.id] ?? process.env[tokenRef.id] ?? "").trim(),
hasProfile,
};
}
return { githubToken: "", hasProfile };
}
async function runGitHubCopilotAuth(ctx: ProviderAuthContext) {
const { githubCopilotLoginCommand } = await loadGithubCopilotRuntime();
@@ -108,6 +73,8 @@ export default definePluginEntry({
};
}
api.registerMemoryEmbeddingProvider(githubCopilotMemoryEmbeddingProviderAdapter);
api.registerProvider({
id: PROVIDER_ID,
label: "GitHub Copilot",
@@ -140,8 +107,9 @@ export default definePluginEntry({
}
const { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken } =
await loadGithubCopilotRuntime();
const { githubToken, hasProfile } = resolveFirstGithubToken({
const { githubToken, hasProfile } = await resolveFirstGithubToken({
agentDir: ctx.agentDir,
config: ctx.config,
env: ctx.env,
});
if (!hasProfile && !githubToken) {

View File

@@ -2,6 +2,9 @@
"id": "github-copilot",
"enabledByDefault": true,
"providers": ["github-copilot"],
"contracts": {
"memoryEmbeddingProviders": ["github-copilot"]
},
"providerAuthEnvVars": {
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"]
},