n8n-nodes-github-copilot 4.1.2 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +4 -3
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +15 -0
- package/dist/shared/utils/provider-injection.d.ts +15 -0
- package/dist/shared/utils/provider-injection.js +177 -0
- package/dist/shared/utils/version-detection.d.ts +11 -0
- package/dist/shared/utils/version-detection.js +78 -0
- package/package.json +4 -3
- package/shared/icons/copilot.svg +34 -0
- package/shared/index.ts +27 -0
- package/shared/models/DynamicModelLoader.ts +124 -0
- package/shared/models/GitHubCopilotModels.ts +420 -0
- package/shared/models/ModelVersionRequirements.ts +165 -0
- package/shared/properties/ModelProperties.ts +52 -0
- package/shared/properties/ModelSelectionProperty.ts +68 -0
- package/shared/utils/DynamicModelsManager.ts +355 -0
- package/shared/utils/EmbeddingsApiUtils.ts +135 -0
- package/shared/utils/FileChunkingApiUtils.ts +176 -0
- package/shared/utils/FileOptimizationUtils.ts +210 -0
- package/shared/utils/GitHubCopilotApiUtils.ts +407 -0
- package/shared/utils/GitHubCopilotEndpoints.ts +212 -0
- package/shared/utils/GitHubDeviceFlowHandler.ts +276 -0
- package/shared/utils/OAuthTokenManager.ts +196 -0
- package/shared/utils/provider-injection.ts +277 -0
- package/shared/utils/version-detection.ts +145 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Models Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages GitHub Copilot models dynamically based on user authentication.
|
|
5
|
+
* Fetches and caches available models per user token.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { GITHUB_COPILOT_API } from "./GitHubCopilotEndpoints";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Model information from GitHub Copilot API
|
|
12
|
+
*/
|
|
13
|
+
interface CopilotModel {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
display_name?: string;
|
|
17
|
+
model_picker_enabled?: boolean;
|
|
18
|
+
model_picker_category?: "lightweight" | "versatile" | "powerful" | string;
|
|
19
|
+
capabilities?: any;
|
|
20
|
+
vendor?: string;
|
|
21
|
+
version?: string;
|
|
22
|
+
preview?: boolean;
|
|
23
|
+
/** Billing information - only available with X-GitHub-Api-Version: 2025-05-01 header */
|
|
24
|
+
billing?: {
|
|
25
|
+
is_premium: boolean;
|
|
26
|
+
multiplier: number;
|
|
27
|
+
restricted_to?: string[];
|
|
28
|
+
};
|
|
29
|
+
is_chat_default?: boolean;
|
|
30
|
+
is_chat_fallback?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* API response format
|
|
35
|
+
*/
|
|
36
|
+
interface ModelsResponse {
|
|
37
|
+
data: CopilotModel[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Cached models with metadata
|
|
42
|
+
*/
|
|
43
|
+
interface ModelCache {
|
|
44
|
+
models: CopilotModel[];
|
|
45
|
+
fetchedAt: number;
|
|
46
|
+
expiresAt: number;
|
|
47
|
+
tokenHash: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Dynamic Models Manager
|
|
52
|
+
* Fetches and caches available models per authenticated user
|
|
53
|
+
*/
|
|
54
|
+
export class DynamicModelsManager {
|
|
55
|
+
private static cache: Map<string, ModelCache> = new Map();
|
|
56
|
+
private static readonly CACHE_DURATION_MS = 60 * 60 * 1000; // 1 hour
|
|
57
|
+
private static readonly MIN_REFRESH_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate a hash for the token (for cache key)
|
|
61
|
+
*/
|
|
62
|
+
private static hashToken(token: string): string {
|
|
63
|
+
// Simple hash for cache key (not cryptographic)
|
|
64
|
+
let hash = 0;
|
|
65
|
+
for (let i = 0; i < token.length; i++) {
|
|
66
|
+
const char = token.charCodeAt(i);
|
|
67
|
+
hash = (hash << 5) - hash + char;
|
|
68
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
69
|
+
}
|
|
70
|
+
return `models_${Math.abs(hash).toString(36)}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fetch models from GitHub Copilot API
|
|
75
|
+
*/
|
|
76
|
+
private static async fetchModelsFromAPI(oauthToken: string): Promise<CopilotModel[]> {
|
|
77
|
+
const url = `${GITHUB_COPILOT_API.BASE_URL}${GITHUB_COPILOT_API.ENDPOINTS.MODELS}`;
|
|
78
|
+
|
|
79
|
+
console.log("🔄 Fetching available models from GitHub Copilot API...");
|
|
80
|
+
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
method: "GET",
|
|
83
|
+
headers: {
|
|
84
|
+
Authorization: `Bearer ${oauthToken}`,
|
|
85
|
+
Accept: "application/json",
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
88
|
+
"Editor-Version": "vscode/1.96.0",
|
|
89
|
+
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
90
|
+
// CRITICAL: This API version returns billing.multiplier field
|
|
91
|
+
// Source: microsoft/vscode-copilot-chat networking.ts
|
|
92
|
+
"X-GitHub-Api-Version": "2025-05-01",
|
|
93
|
+
"X-Interaction-Type": "model-access",
|
|
94
|
+
"OpenAI-Intent": "model-access",
|
|
95
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const errorText = await response.text();
|
|
101
|
+
console.error(`❌ Failed to fetch models: ${response.status} ${response.statusText}`);
|
|
102
|
+
console.error(`❌ Error details: ${errorText}`);
|
|
103
|
+
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = (await response.json()) as ModelsResponse;
|
|
107
|
+
|
|
108
|
+
// Return ALL models (no filtering by model_picker_enabled)
|
|
109
|
+
console.log(`✅ Fetched ${data.data.length} models from API`);
|
|
110
|
+
|
|
111
|
+
return data.data;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get models for authenticated user (with caching)
|
|
116
|
+
*/
|
|
117
|
+
public static async getAvailableModels(oauthToken: string): Promise<CopilotModel[]> {
|
|
118
|
+
const tokenHash = this.hashToken(oauthToken);
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
|
|
121
|
+
// Check cache
|
|
122
|
+
const cached = this.cache.get(tokenHash);
|
|
123
|
+
if (cached && cached.expiresAt > now) {
|
|
124
|
+
const remainingMinutes = Math.round((cached.expiresAt - now) / 60000);
|
|
125
|
+
console.log(`✅ Using cached models (expires in ${remainingMinutes} minutes)`);
|
|
126
|
+
return cached.models;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check if we should wait before refreshing (avoid spam)
|
|
130
|
+
if (cached && now - cached.fetchedAt < this.MIN_REFRESH_INTERVAL_MS) {
|
|
131
|
+
const waitSeconds = Math.round((this.MIN_REFRESH_INTERVAL_MS - (now - cached.fetchedAt)) / 1000);
|
|
132
|
+
console.log(`⏰ Models fetched recently, using cache (min refresh interval: ${waitSeconds}s)`);
|
|
133
|
+
return cached.models;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Fetch from API
|
|
137
|
+
try {
|
|
138
|
+
const models = await this.fetchModelsFromAPI(oauthToken);
|
|
139
|
+
|
|
140
|
+
// Cache the result
|
|
141
|
+
this.cache.set(tokenHash, {
|
|
142
|
+
models,
|
|
143
|
+
fetchedAt: now,
|
|
144
|
+
expiresAt: now + this.CACHE_DURATION_MS,
|
|
145
|
+
tokenHash,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return models;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error("❌ Failed to fetch models from API:", error);
|
|
151
|
+
|
|
152
|
+
// Return cached models if available (even if expired)
|
|
153
|
+
if (cached) {
|
|
154
|
+
console.log("⚠️ Using expired cache as fallback");
|
|
155
|
+
return cached.models;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// No cache available, throw error
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Filter models by type (chat, embeddings, etc.)
|
|
165
|
+
*/
|
|
166
|
+
public static filterModelsByType(models: CopilotModel[], type: string): CopilotModel[] {
|
|
167
|
+
return models.filter((model) => {
|
|
168
|
+
const modelType = (model.capabilities as any)?.type;
|
|
169
|
+
return modelType === type;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get cost multiplier from API billing data or fallback to estimation
|
|
175
|
+
*
|
|
176
|
+
* With X-GitHub-Api-Version: 2025-05-01, the API returns:
|
|
177
|
+
* - billing.multiplier: 0, 0.33, 1, 3, or 10
|
|
178
|
+
* - billing.is_premium: boolean
|
|
179
|
+
*
|
|
180
|
+
* Display format: "0x", "0.33x", "1x", "3x", "10x"
|
|
181
|
+
*/
|
|
182
|
+
private static getCostMultiplier(model: CopilotModel): string {
|
|
183
|
+
// BEST: Use API billing data if available (requires 2025-05-01 header)
|
|
184
|
+
if (model.billing?.multiplier !== undefined) {
|
|
185
|
+
return `${model.billing.multiplier}x`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// FALLBACK: Estimate based on model ID patterns
|
|
189
|
+
// This is used when API doesn't return billing data
|
|
190
|
+
const id = model.id.toLowerCase();
|
|
191
|
+
|
|
192
|
+
// === 0x FREE TIER ===
|
|
193
|
+
// GPT-4 series (legacy, included in subscription)
|
|
194
|
+
if (id === 'gpt-4.1' || id.startsWith('gpt-4.1-')) return '0x';
|
|
195
|
+
if (id === 'gpt-4o' || id.startsWith('gpt-4o-')) return '0x';
|
|
196
|
+
if (id === 'gpt-4' || id === 'gpt-4-0613') return '0x';
|
|
197
|
+
// Mini models
|
|
198
|
+
if (id === 'gpt-5-mini') return '0x';
|
|
199
|
+
if (id === 'gpt-4o-mini' || id.startsWith('gpt-4o-mini-')) return '0x';
|
|
200
|
+
// Grok fast models
|
|
201
|
+
if (id.includes('grok') && id.includes('fast')) return '0x';
|
|
202
|
+
// Raptor mini (ID: oswe-vscode-prime)
|
|
203
|
+
if (id === 'oswe-vscode-prime' || id.includes('oswe-vscode')) return '0x';
|
|
204
|
+
|
|
205
|
+
// === 0.33x ECONOMY TIER ===
|
|
206
|
+
// Claude Haiku (economy)
|
|
207
|
+
if (id.includes('haiku')) return '0.33x';
|
|
208
|
+
// Gemini Flash models
|
|
209
|
+
if (id.includes('flash')) return '0.33x';
|
|
210
|
+
// Codex-Mini models
|
|
211
|
+
if (id.includes('codex-mini')) return '0.33x';
|
|
212
|
+
|
|
213
|
+
// === 10x ULTRA PREMIUM ===
|
|
214
|
+
// Claude Opus 4.1 specifically
|
|
215
|
+
if (id === 'claude-opus-41' || id === 'claude-opus-4.1') return '10x';
|
|
216
|
+
|
|
217
|
+
// === 3x PREMIUM TIER ===
|
|
218
|
+
// Claude Opus 4.5
|
|
219
|
+
if (id.includes('opus')) return '3x';
|
|
220
|
+
|
|
221
|
+
// === 1x STANDARD TIER (default for most models) ===
|
|
222
|
+
// GPT-5 series (including Codex variants - they are 1x, not 3x!)
|
|
223
|
+
// Claude Sonnet
|
|
224
|
+
// Gemini Pro
|
|
225
|
+
// Everything else
|
|
226
|
+
return '1x';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Convert models to n8n options format with capability badges
|
|
231
|
+
*/
|
|
232
|
+
public static modelsToN8nOptions(models: CopilotModel[]): Array<{
|
|
233
|
+
name: string;
|
|
234
|
+
value: string;
|
|
235
|
+
description?: string;
|
|
236
|
+
}> {
|
|
237
|
+
// First pass: count how many models share the same display name
|
|
238
|
+
const nameCount = new Map<string, number>();
|
|
239
|
+
models.forEach((model) => {
|
|
240
|
+
const displayName = model.display_name || model.name || model.id;
|
|
241
|
+
nameCount.set(displayName, (nameCount.get(displayName) || 0) + 1);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return models.map((model) => {
|
|
245
|
+
// Build capability badges/chips
|
|
246
|
+
const badges: string[] = [];
|
|
247
|
+
|
|
248
|
+
if (model.capabilities) {
|
|
249
|
+
const supports = (model.capabilities as any).supports || {};
|
|
250
|
+
|
|
251
|
+
// Check each capability and add corresponding badge
|
|
252
|
+
if (supports.streaming) badges.push("🔄 Streaming");
|
|
253
|
+
if (supports.tool_calls) badges.push("🔧 Tools");
|
|
254
|
+
if (supports.vision) badges.push("👁️ Vision");
|
|
255
|
+
if (supports.structured_outputs) badges.push("📋 Structured");
|
|
256
|
+
if (supports.parallel_tool_calls) badges.push("⚡ Parallel");
|
|
257
|
+
|
|
258
|
+
// Check for thinking capabilities (reasoning models)
|
|
259
|
+
if (supports.max_thinking_budget) badges.push("🧠 Reasoning");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Build display name with badges and cost multiplier (VS Code style: "Model Name • 1x")
|
|
263
|
+
const displayName = model.display_name || model.name || model.id;
|
|
264
|
+
const costMultiplier = this.getCostMultiplier(model);
|
|
265
|
+
const badgesText = badges.length > 0 ? ` [${badges.join(" • ")}]` : "";
|
|
266
|
+
|
|
267
|
+
// Check if this display name has duplicates
|
|
268
|
+
const hasDuplicates = (nameCount.get(displayName) || 0) > 1;
|
|
269
|
+
|
|
270
|
+
// Get category label (capitalize first letter)
|
|
271
|
+
const category = model.model_picker_category || "";
|
|
272
|
+
const categoryLabel = category ? ` - ${category.charAt(0).toUpperCase() + category.slice(1)}` : "";
|
|
273
|
+
|
|
274
|
+
// Format: "Model Name • 0x - Lightweight [badges]"
|
|
275
|
+
const multiplierDisplay = ` • ${costMultiplier}${categoryLabel}`;
|
|
276
|
+
|
|
277
|
+
// Build description with more details
|
|
278
|
+
let description = "";
|
|
279
|
+
if (model.capabilities) {
|
|
280
|
+
const limits = (model.capabilities as any).limits || {};
|
|
281
|
+
const parts: string[] = [];
|
|
282
|
+
|
|
283
|
+
// If duplicates exist, add model ID
|
|
284
|
+
if (hasDuplicates) {
|
|
285
|
+
parts.push(`ID: ${model.id}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (limits.max_context_window_tokens) {
|
|
289
|
+
parts.push(`Context: ${(limits.max_context_window_tokens / 1000).toFixed(0)}k`);
|
|
290
|
+
}
|
|
291
|
+
if (limits.max_output_tokens) {
|
|
292
|
+
parts.push(`Output: ${(limits.max_output_tokens / 1000).toFixed(0)}k`);
|
|
293
|
+
}
|
|
294
|
+
if (model.vendor) {
|
|
295
|
+
parts.push(`Provider: ${model.vendor}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
description = parts.join(" • ");
|
|
299
|
+
} else {
|
|
300
|
+
// No capabilities, just show ID if duplicates
|
|
301
|
+
if (hasDuplicates) {
|
|
302
|
+
description = `ID: ${model.id}`;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
name: `${displayName}${multiplierDisplay}${badgesText}`,
|
|
308
|
+
value: model.id,
|
|
309
|
+
description: description || undefined,
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Clear cache for specific token
|
|
316
|
+
*/
|
|
317
|
+
public static clearCache(oauthToken: string): void {
|
|
318
|
+
const tokenHash = this.hashToken(oauthToken);
|
|
319
|
+
this.cache.delete(tokenHash);
|
|
320
|
+
console.log("🗑️ Cleared models cache");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Clear all cached models
|
|
325
|
+
*/
|
|
326
|
+
public static clearAllCache(): void {
|
|
327
|
+
this.cache.clear();
|
|
328
|
+
console.log("🗑️ Cleared all models cache");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get cache info for debugging
|
|
333
|
+
*/
|
|
334
|
+
public static getCacheInfo(oauthToken: string): {
|
|
335
|
+
cached: boolean;
|
|
336
|
+
modelsCount: number;
|
|
337
|
+
expiresIn: number;
|
|
338
|
+
fetchedAt: string;
|
|
339
|
+
} | null {
|
|
340
|
+
const tokenHash = this.hashToken(oauthToken);
|
|
341
|
+
const cached = this.cache.get(tokenHash);
|
|
342
|
+
|
|
343
|
+
if (!cached) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
return {
|
|
349
|
+
cached: true,
|
|
350
|
+
modelsCount: cached.models.length,
|
|
351
|
+
expiresIn: Math.max(0, cached.expiresAt - now),
|
|
352
|
+
fetchedAt: new Date(cached.fetchedAt).toISOString(),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embeddings API Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared functions for making embeddings API requests.
|
|
5
|
+
* Used by both GitHubCopilotEmbeddings node and GitHubCopilotTest node.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { GitHubCopilotEndpoints, GITHUB_COPILOT_API } from "./GitHubCopilotEndpoints";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Embedding Response from GitHub Copilot API
|
|
12
|
+
*/
|
|
13
|
+
export interface EmbeddingResponse {
|
|
14
|
+
object: string;
|
|
15
|
+
data: Array<{
|
|
16
|
+
object: string;
|
|
17
|
+
index: number;
|
|
18
|
+
embedding: number[];
|
|
19
|
+
}>;
|
|
20
|
+
model: string;
|
|
21
|
+
usage: {
|
|
22
|
+
prompt_tokens: number;
|
|
23
|
+
total_tokens: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Embedding Request Body
|
|
29
|
+
*/
|
|
30
|
+
export interface EmbeddingRequest {
|
|
31
|
+
model: string;
|
|
32
|
+
input: string[];
|
|
33
|
+
dimensions?: number;
|
|
34
|
+
encoding_format?: "float" | "base64";
|
|
35
|
+
user?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Execute embeddings request with retry logic
|
|
40
|
+
*
|
|
41
|
+
* @param oauthToken - OAuth token for authentication
|
|
42
|
+
* @param requestBody - Embeddings request body
|
|
43
|
+
* @param enableRetry - Whether to enable retry on TPM quota errors
|
|
44
|
+
* @param maxRetries - Maximum number of retry attempts
|
|
45
|
+
* @returns Embedding response
|
|
46
|
+
*/
|
|
47
|
+
export async function executeEmbeddingsRequest(
|
|
48
|
+
oauthToken: string,
|
|
49
|
+
requestBody: EmbeddingRequest,
|
|
50
|
+
enableRetry = true,
|
|
51
|
+
maxRetries = 3,
|
|
52
|
+
): Promise<EmbeddingResponse> {
|
|
53
|
+
let lastError: Error | null = null;
|
|
54
|
+
|
|
55
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
56
|
+
try {
|
|
57
|
+
const response = await fetch(
|
|
58
|
+
GitHubCopilotEndpoints.getEmbeddingsUrl(),
|
|
59
|
+
{
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: GitHubCopilotEndpoints.getEmbeddingsHeaders(oauthToken),
|
|
62
|
+
body: JSON.stringify(requestBody),
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const errorText = await response.text();
|
|
68
|
+
|
|
69
|
+
// Check if it's a retryable error (TPM quota)
|
|
70
|
+
if (
|
|
71
|
+
GitHubCopilotEndpoints.isTpmQuotaError(response.status) &&
|
|
72
|
+
enableRetry &&
|
|
73
|
+
attempt < maxRetries
|
|
74
|
+
) {
|
|
75
|
+
const delay = GitHubCopilotEndpoints.getRetryDelay(attempt + 1);
|
|
76
|
+
console.log(
|
|
77
|
+
`Embeddings attempt ${attempt + 1} failed with ${response.status}, retrying in ${delay}ms...`,
|
|
78
|
+
);
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Embeddings API Error ${response.status}: ${errorText}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const data = (await response.json()) as EmbeddingResponse;
|
|
89
|
+
return data;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
92
|
+
|
|
93
|
+
if (attempt < maxRetries && enableRetry) {
|
|
94
|
+
const delay = GitHubCopilotEndpoints.getRetryDelay(attempt + 1);
|
|
95
|
+
console.log(`Embeddings attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
|
|
96
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
throw lastError;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw lastError || new Error("Maximum retry attempts exceeded for embeddings request");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Simple embeddings request without retry logic
|
|
109
|
+
*
|
|
110
|
+
* @param oauthToken - OAuth token for authentication
|
|
111
|
+
* @param requestBody - Embeddings request body
|
|
112
|
+
* @returns Embedding response
|
|
113
|
+
*/
|
|
114
|
+
export async function executeEmbeddingsRequestSimple(
|
|
115
|
+
oauthToken: string,
|
|
116
|
+
requestBody: EmbeddingRequest,
|
|
117
|
+
): Promise<EmbeddingResponse> {
|
|
118
|
+
const response = await fetch(
|
|
119
|
+
GitHubCopilotEndpoints.getEmbeddingsUrl(),
|
|
120
|
+
{
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: GitHubCopilotEndpoints.getEmbeddingsHeaders(oauthToken),
|
|
123
|
+
body: JSON.stringify(requestBody),
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
const errorText = await response.text();
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Embeddings API Error ${response.status}: ${errorText}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (await response.json()) as EmbeddingResponse;
|
|
135
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { GitHubCopilotEndpoints } from './GitHubCopilotEndpoints';
|
|
2
|
+
|
|
3
|
+
export interface ChunkRequest {
|
|
4
|
+
content: string;
|
|
5
|
+
embed: boolean;
|
|
6
|
+
qos: 'Batch' | 'Online';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Chunk {
|
|
10
|
+
content: string;
|
|
11
|
+
embedding?: number[];
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ChunkResponse {
|
|
18
|
+
chunks: Chunk[];
|
|
19
|
+
total: number;
|
|
20
|
+
contentLength: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Chunk a file using GitHub Copilot API
|
|
25
|
+
*/
|
|
26
|
+
export async function chunkFile(
|
|
27
|
+
token: string,
|
|
28
|
+
fileContent: string,
|
|
29
|
+
embeddings = true,
|
|
30
|
+
qos: 'Batch' | 'Online' = 'Online',
|
|
31
|
+
): Promise<ChunkResponse> {
|
|
32
|
+
const url = 'https://api.githubcopilot.com/chunks';
|
|
33
|
+
const headers = GitHubCopilotEndpoints.getAuthHeaders(token, true);
|
|
34
|
+
|
|
35
|
+
const requestBody: ChunkRequest = {
|
|
36
|
+
content: fileContent,
|
|
37
|
+
embed: embeddings,
|
|
38
|
+
qos,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
console.log(`🔪 Chunking file (${fileContent.length} chars, embed=${embeddings}, qos=${qos})`);
|
|
42
|
+
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers,
|
|
46
|
+
body: JSON.stringify(requestBody),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const errorText = await response.text();
|
|
51
|
+
throw new Error(`Chunking API error: ${response.status} ${response.statusText}. ${errorText}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const data = (await response.json()) as ChunkResponse;
|
|
55
|
+
console.log(`✅ Chunked into ${data.chunks.length} chunks`);
|
|
56
|
+
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Calculate cosine similarity between two vectors
|
|
62
|
+
*/
|
|
63
|
+
function cosineSimilarity(a: number[], b: number[]): number {
|
|
64
|
+
if (a.length !== b.length) {
|
|
65
|
+
throw new Error('Vectors must have same length');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let dotProduct = 0;
|
|
69
|
+
let normA = 0;
|
|
70
|
+
let normB = 0;
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < a.length; i++) {
|
|
73
|
+
dotProduct += a[i] * b[i];
|
|
74
|
+
normA += a[i] * a[i];
|
|
75
|
+
normB += b[i] * b[i];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Select the most relevant chunks based on query embedding
|
|
83
|
+
*/
|
|
84
|
+
export function selectRelevantChunks(
|
|
85
|
+
chunks: Chunk[],
|
|
86
|
+
queryEmbedding: number[],
|
|
87
|
+
maxTokens = 10000,
|
|
88
|
+
minRelevance = 0.5,
|
|
89
|
+
): string {
|
|
90
|
+
if (!chunks.length) {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const rankedChunks = chunks
|
|
95
|
+
.filter((chunk) => chunk.embedding)
|
|
96
|
+
.map((chunk) => ({
|
|
97
|
+
chunk,
|
|
98
|
+
relevance: cosineSimilarity(chunk.embedding!, queryEmbedding),
|
|
99
|
+
}))
|
|
100
|
+
.filter((item) => item.relevance >= minRelevance)
|
|
101
|
+
.sort((a, b) => b.relevance - a.relevance);
|
|
102
|
+
|
|
103
|
+
const selectedChunks: string[] = [];
|
|
104
|
+
let totalTokens = 0;
|
|
105
|
+
|
|
106
|
+
for (const item of rankedChunks) {
|
|
107
|
+
const chunkTokens = Math.ceil(item.chunk.content.length / 4);
|
|
108
|
+
if (totalTokens + chunkTokens > maxTokens) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
selectedChunks.push(item.chunk.content);
|
|
112
|
+
totalTokens += chunkTokens;
|
|
113
|
+
console.log(
|
|
114
|
+
` ✓ Selected chunk (relevance: ${item.relevance.toFixed(3)}, tokens: ~${chunkTokens})`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(
|
|
119
|
+
`📊 Selected ${selectedChunks.length}/${rankedChunks.length} chunks (~${totalTokens} tokens)`,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return selectedChunks.join('\n\n---\n\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Select top chunks by order (without relevance ranking)
|
|
127
|
+
*/
|
|
128
|
+
export function selectTopChunks(chunks: Chunk[], maxTokens = 10000): string {
|
|
129
|
+
const selectedChunks: string[] = [];
|
|
130
|
+
let totalTokens = 0;
|
|
131
|
+
|
|
132
|
+
for (const chunk of chunks) {
|
|
133
|
+
const chunkTokens = Math.ceil(chunk.content.length / 4);
|
|
134
|
+
if (totalTokens + chunkTokens > maxTokens) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
selectedChunks.push(chunk.content);
|
|
138
|
+
totalTokens += chunkTokens;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log(`📊 Selected ${selectedChunks.length}/${chunks.length} chunks (~${totalTokens} tokens)`);
|
|
142
|
+
|
|
143
|
+
return selectedChunks.join('\n\n---\n\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Estimate token count for text
|
|
148
|
+
*/
|
|
149
|
+
export function estimateTokens(text: string): number {
|
|
150
|
+
return Math.ceil(text.length / 4);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get embedding for a query string
|
|
155
|
+
*/
|
|
156
|
+
export async function getQueryEmbedding(token: string, query: string): Promise<number[]> {
|
|
157
|
+
const url = 'https://api.githubcopilot.com/embeddings';
|
|
158
|
+
const headers = GitHubCopilotEndpoints.getEmbeddingsHeaders(token);
|
|
159
|
+
|
|
160
|
+
const response = await fetch(url, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers,
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
input: [query],
|
|
165
|
+
model: 'text-embedding-3-small',
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
const errorText = await response.text();
|
|
171
|
+
throw new Error(`Embeddings API error: ${response.status} ${response.statusText}. ${errorText}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const data = (await response.json()) as { data: Array<{ embedding: number[] }> };
|
|
175
|
+
return data.data[0].embedding;
|
|
176
|
+
}
|