import { resolveConfiguredSecretInputString } from "openclaw/plugin-sdk/config-runtime"; import {
DEFAULT_COPILOT_API_BASE_URL,
resolveCopilotApiToken,
} from "openclaw/plugin-sdk/github-copilot-token"; import {
buildRemoteBaseUrlPolicy,
sanitizeAndNormalizeEmbedding,
withRemoteHttpResponse,
type MemoryEmbeddingProvider,
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";
/** * 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;
function isCopilotSetupError(err: unknown): boolean { if (!(err instanceof Error)) { returnfalse;
} // 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) { thrownew Error(
`GitHub Copilot model discovery HTTP ${response.status}: ${await response.text()}`,
);
}
let payload: unknown; try {
payload = await response.json();
} catch { thrownew 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) { thrownew Error("No embedding models available from GitHub Copilot");
} if (!available.includes(stripped)) { thrownew 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];
} thrownew Error("No embedding models available from GitHub Copilot");
}
function parseGitHubCopilotEmbeddingPayload(payload: unknown, expectedCount: number): number[][] { if (!payload || typeof payload !== "object") { thrownew Error("GitHub Copilot embeddings response missing data[]");
} const data = (payload as { data?: unknown }).data; if (!Array.isArray(data)) { thrownew Error("GitHub Copilot embeddings response missing data[]");
}
const vectors = Array.from<number[] | undefined>({ length: expectedCount }); for (const entry of data) { if (!entry || typeof entry !== "object") { thrownew Error("GitHub Copilot embeddings response contains an invalid entry");
} const indexValue = (entry as { index?: unknown }).index; const embedding = (entry as { embedding?: unknown }).embedding; const index = typeof indexValue === "number" ? indexValue : Number.NaN; if (!Number.isInteger(index)) { thrownew Error("GitHub Copilot embeddings response contains an invalid index");
} if (index < 0 || index >= expectedCount) { thrownew Error("GitHub Copilot embeddings response contains an out-of-range index");
} if (vectors[index] !== undefined) { thrownew Error("GitHub Copilot embeddings response contains duplicate indexes");
} if (!Array.isArray(embedding) || !embedding.every((value) => typeof value === "number")) { thrownew Error("GitHub Copilot embeddings response contains an invalid embedding");
}
vectors[index] = sanitizeAndNormalizeEmbedding(embedding);
}
for (let index = 0; index < expectedCount; index += 1) { if (vectors[index] === undefined) { thrownew Error("GitHub Copilot embeddings response missing vectors for some inputs");
}
} return vectors as number[][];
}
// 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,
});
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.