@@ -4,7 +4,10 @@ import {
44 resolveCopilotApiToken ,
55} from "openclaw/plugin-sdk/github-copilot-token" ;
66import {
7- createGitHubCopilotEmbeddingProvider ,
7+ buildRemoteBaseUrlPolicy ,
8+ sanitizeAndNormalizeEmbedding ,
9+ withRemoteHttpResponse ,
10+ type MemoryEmbeddingProvider ,
811 type MemoryEmbeddingProviderAdapter ,
912} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings" ;
1013import { fetchWithSsrFGuard , type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime" ;
@@ -44,6 +47,15 @@ type CopilotModelEntry = {
4447 supported_endpoints ?: unknown ;
4548} ;
4649
50+ type GitHubCopilotEmbeddingClient = {
51+ githubToken : string ;
52+ model : string ;
53+ baseUrl ?: string ;
54+ headers ?: Record < string , string > ;
55+ env ?: NodeJS . ProcessEnv ;
56+ fetchImpl ?: typeof fetch ;
57+ } ;
58+
4759function isCopilotSetupError ( err : unknown ) : boolean {
4860 if ( ! ( err instanceof Error ) ) {
4961 return false ;
@@ -147,9 +159,126 @@ function pickBestModel(available: string[], userModel?: string): string {
147159 throw new Error ( "No embedding models available from GitHub Copilot" ) ;
148160}
149161
162+ function parseGitHubCopilotEmbeddingPayload ( payload : unknown , expectedCount : number ) : number [ ] [ ] {
163+ if ( ! payload || typeof payload !== "object" ) {
164+ throw new Error ( "GitHub Copilot embeddings response missing data[]" ) ;
165+ }
166+ const data = ( payload as { data ?: unknown } ) . data ;
167+ if ( ! Array . isArray ( data ) ) {
168+ throw new Error ( "GitHub Copilot embeddings response missing data[]" ) ;
169+ }
170+
171+ const vectors = Array . from < number [ ] | undefined > ( { length : expectedCount } ) ;
172+ for ( const entry of data ) {
173+ if ( ! entry || typeof entry !== "object" ) {
174+ throw new Error ( "GitHub Copilot embeddings response contains an invalid entry" ) ;
175+ }
176+ const indexValue = ( entry as { index ?: unknown } ) . index ;
177+ const embedding = ( entry as { embedding ?: unknown } ) . embedding ;
178+ const index = typeof indexValue === "number" ? indexValue : Number . NaN ;
179+ if ( ! Number . isInteger ( index ) ) {
180+ throw new Error ( "GitHub Copilot embeddings response contains an invalid index" ) ;
181+ }
182+ if ( index < 0 || index >= expectedCount ) {
183+ throw new Error ( "GitHub Copilot embeddings response contains an out-of-range index" ) ;
184+ }
185+ if ( vectors [ index ] !== undefined ) {
186+ throw new Error ( "GitHub Copilot embeddings response contains duplicate indexes" ) ;
187+ }
188+ if ( ! Array . isArray ( embedding ) || ! embedding . every ( ( value ) => typeof value === "number" ) ) {
189+ throw new Error ( "GitHub Copilot embeddings response contains an invalid embedding" ) ;
190+ }
191+ vectors [ index ] = sanitizeAndNormalizeEmbedding ( embedding ) ;
192+ }
193+
194+ for ( let index = 0 ; index < expectedCount ; index += 1 ) {
195+ if ( vectors [ index ] === undefined ) {
196+ throw new Error ( "GitHub Copilot embeddings response missing vectors for some inputs" ) ;
197+ }
198+ }
199+ return vectors as number [ ] [ ] ;
200+ }
201+
202+ async function resolveGitHubCopilotEmbeddingSession ( client : GitHubCopilotEmbeddingClient ) : Promise < {
203+ baseUrl : string ;
204+ headers : Record < string , string > ;
205+ } > {
206+ const token = await resolveCopilotApiToken ( {
207+ githubToken : client . githubToken ,
208+ env : client . env ,
209+ fetchImpl : client . fetchImpl ,
210+ } ) ;
211+ const baseUrl = client . baseUrl ?. trim ( ) || token . baseUrl || DEFAULT_COPILOT_API_BASE_URL ;
212+ return {
213+ baseUrl,
214+ headers : {
215+ ...COPILOT_HEADERS_STATIC ,
216+ ...client . headers ,
217+ Authorization : `Bearer ${ token . token } ` ,
218+ } ,
219+ } ;
220+ }
221+
222+ async function createGitHubCopilotEmbeddingProvider (
223+ client : GitHubCopilotEmbeddingClient ,
224+ ) : Promise < { provider : MemoryEmbeddingProvider ; client : GitHubCopilotEmbeddingClient } > {
225+ const initialSession = await resolveGitHubCopilotEmbeddingSession ( client ) ;
226+
227+ const embed = async ( input : string [ ] ) : Promise < number [ ] [ ] > => {
228+ if ( input . length === 0 ) {
229+ return [ ] ;
230+ }
231+
232+ const session = await resolveGitHubCopilotEmbeddingSession ( client ) ;
233+ const url = `${ session . baseUrl . replace ( / \/ $ / , "" ) } /embeddings` ;
234+ return await withRemoteHttpResponse ( {
235+ url,
236+ fetchImpl : client . fetchImpl ,
237+ ssrfPolicy : buildRemoteBaseUrlPolicy ( session . baseUrl ) ,
238+ init : {
239+ method : "POST" ,
240+ headers : session . headers ,
241+ body : JSON . stringify ( { model : client . model , input } ) ,
242+ } ,
243+ onResponse : async ( response ) => {
244+ if ( ! response . ok ) {
245+ throw new Error (
246+ `GitHub Copilot embeddings HTTP ${ response . status } : ${ await response . text ( ) } ` ,
247+ ) ;
248+ }
249+
250+ let payload : unknown ;
251+ try {
252+ payload = await response . json ( ) ;
253+ } catch {
254+ throw new Error ( "GitHub Copilot embeddings returned invalid JSON" ) ;
255+ }
256+ return parseGitHubCopilotEmbeddingPayload ( payload , input . length ) ;
257+ } ,
258+ } ) ;
259+ } ;
260+
261+ return {
262+ provider : {
263+ id : COPILOT_EMBEDDING_PROVIDER_ID ,
264+ model : client . model ,
265+ embedQuery : async ( text ) => {
266+ const [ vector ] = await embed ( [ text ] ) ;
267+ return vector ?? [ ] ;
268+ } ,
269+ embedBatch : embed ,
270+ } ,
271+ client : {
272+ ...client ,
273+ baseUrl : initialSession . baseUrl ,
274+ } ,
275+ } ;
276+ }
277+
150278export const githubCopilotMemoryEmbeddingProviderAdapter : MemoryEmbeddingProviderAdapter = {
151279 id : COPILOT_EMBEDDING_PROVIDER_ID ,
152280 transport : "remote" ,
281+ authProviderId : COPILOT_EMBEDDING_PROVIDER_ID ,
153282 autoSelectPriority : 15 ,
154283 allowExplicitWhenConfiguredAuto : true ,
155284 shouldContinueAutoSelection : ( err : unknown ) => isCopilotSetupError ( err ) ,
0 commit comments