Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/node/shared/copilotApiService.test.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import type Anthropic from '@anthropic-ai/sdk';
8
import { Iterable } from '../../../../../base/common/iterator.js';
9
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
10
import { CopilotApiService, type FetchFunction } from '../../../node/shared/copilotApiService.js';
11
import { NullLogService } from '../../../../log/common/log.js';
12
import { IProductService } from '../../../../product/common/productService.js';
13
import product from '../../../../product/common/product.js';
14
15
// #region Test Helpers
16
17
const testProductService: IProductService = { _serviceBrand: undefined, ...product };
18
19
function sseLines(...lines: string[]): Uint8Array {
20
return new TextEncoder().encode(lines.join('\n') + '\n');
21
}
22
23
function makeSseBody(chunks: Uint8Array[]): ReadableStream<Uint8Array> {
24
let index = 0;
25
return new ReadableStream({
26
pull(controller) {
27
if (index < chunks.length) {
28
controller.enqueue(chunks[index++]);
29
} else {
30
controller.close();
31
}
32
}
33
});
34
}
35
36
const collect = Iterable.asyncToArray;
37
38
function getUrl(input: string | URL | Request): string {
39
if (typeof input === 'string') {
40
return input;
41
}
42
return input instanceof URL ? input.href : input.url;
43
}
44
45
function getText(msg: Anthropic.Message): string {
46
return msg.content
47
.filter((b): b is Anthropic.TextBlock => b.type === 'text')
48
.map(b => b.text)
49
.join('');
50
}
51
52
function tokenResponse(overrides?: Record<string, unknown>): Response {
53
return new Response(JSON.stringify({
54
token: 'copilot-tok-abc',
55
expires_at: Date.now() / 1000 + 3600,
56
refresh_in: 1800,
57
...overrides,
58
}), { status: 200 });
59
}
60
61
function anthropicResponse(content: Array<{ type: string; text?: string }>, stopReason = 'end_turn'): Response {
62
return new Response(JSON.stringify({
63
id: 'msg_test',
64
type: 'message',
65
role: 'assistant',
66
content,
67
model: 'claude-sonnet-4-5-20250514',
68
stop_reason: stopReason,
69
usage: { input_tokens: 10, output_tokens: 50 },
70
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
71
}
72
73
function sseResponse(chunks: Uint8Array[]): Response {
74
return new Response(makeSseBody(chunks), {
75
status: 200,
76
headers: { 'Content-Type': 'text/event-stream' },
77
});
78
}
79
80
function modelsResponse(models: object[]): Response {
81
return new Response(JSON.stringify({ data: models }), {
82
status: 200,
83
headers: { 'Content-Type': 'application/json' },
84
});
85
}
86
87
function createService(fetchImpl: FetchFunction): CopilotApiService {
88
return new CopilotApiService(fetchImpl, new NullLogService(), testProductService);
89
}
90
91
type CapturedRequest = { url: string; init: RequestInit | undefined };
92
93
function routingFetch(
94
messageResponse: (captured: CapturedRequest) => Response,
95
tokenOverrides?: Record<string, unknown>,
96
): { fetch: FetchFunction; captured: () => CapturedRequest } {
97
let lastCapture: CapturedRequest = { url: '', init: undefined };
98
const impl: FetchFunction = async (input, init) => {
99
const url = getUrl(input);
100
if (url.includes('/token') || url.includes('/copilot_internal')) {
101
return tokenResponse(tokenOverrides);
102
}
103
lastCapture = { url, init };
104
return messageResponse(lastCapture);
105
};
106
return { fetch: impl, captured: () => lastCapture };
107
}
108
109
const userMsg: Anthropic.MessageParam[] = [{ role: 'user', content: 'hello' }];
110
const baseRequest = {
111
model: 'claude-sonnet-4-5',
112
messages: userMsg,
113
max_tokens: 8192,
114
stream: false as const,
115
};
116
117
function streamService(chunks: Uint8Array[], tokenOverrides?: Record<string, unknown>): CopilotApiService {
118
const { fetch: fetchFn } = routingFetch(() => sseResponse(chunks), tokenOverrides);
119
return createService(fetchFn);
120
}
121
122
// #endregion
123
124
suite('CopilotApiService', () => {
125
126
ensureNoDisposablesAreLeakedInTestSuite();
127
128
// #region Token Minting
129
130
suite('Token Minting', () => {
131
132
test('mints a token on first request', async () => {
133
let mintCount = 0;
134
const service = createService(async (input) => {
135
const url = getUrl(input);
136
if (url.includes('/copilot_internal')) {
137
mintCount++;
138
return tokenResponse();
139
}
140
return anthropicResponse([{ type: 'text', text: 'hi' }]);
141
});
142
143
await service.messages('gh-tok', baseRequest);
144
assert.strictEqual(mintCount, 1);
145
});
146
147
test('reuses cached token for consecutive calls with same github token', async () => {
148
let mintCount = 0;
149
const service = createService(async (input) => {
150
const url = getUrl(input);
151
if (url.includes('/copilot_internal')) {
152
mintCount++;
153
return tokenResponse();
154
}
155
return anthropicResponse([{ type: 'text', text: 'hi' }]);
156
});
157
158
await service.messages('gh-tok', baseRequest);
159
await service.messages('gh-tok', baseRequest);
160
await service.messages('gh-tok', baseRequest);
161
assert.strictEqual(mintCount, 1);
162
});
163
164
test('re-mints when the github token changes', async () => {
165
let mintCount = 0;
166
const service = createService(async (input) => {
167
const url = getUrl(input);
168
if (url.includes('/copilot_internal')) {
169
mintCount++;
170
return tokenResponse();
171
}
172
return anthropicResponse([{ type: 'text', text: 'hi' }]);
173
});
174
175
await service.messages('gh-tok-A', baseRequest);
176
await service.messages('gh-tok-B', baseRequest);
177
assert.strictEqual(mintCount, 2);
178
});
179
180
test('re-mints when the copilot token is within 5 minutes of expiry', async () => {
181
let mintCount = 0;
182
const service = createService(async (input) => {
183
const url = getUrl(input);
184
if (url.includes('/copilot_internal')) {
185
mintCount++;
186
// Both expires_at AND refresh_in must point to a soon-expiring token,
187
// because cache validity prefers refresh_in over expires_at.
188
return tokenResponse({ expires_at: Date.now() / 1000 + 120, refresh_in: 0 });
189
}
190
return anthropicResponse([{ type: 'text', text: 'hi' }]);
191
});
192
193
await service.messages('gh-tok', baseRequest);
194
await service.messages('gh-tok', baseRequest);
195
assert.strictEqual(mintCount, 2);
196
});
197
198
test('uses refresh_in (not expires_at) for cache validity to tolerate clock skew', async () => {
199
// Server says expires_at is in the past (simulating client clock ahead of server),
200
// but refresh_in is comfortably long. Cache must still be valid.
201
let mintCount = 0;
202
const service = createService(async (input) => {
203
const url = getUrl(input);
204
if (url.includes('/copilot_internal')) {
205
mintCount++;
206
return tokenResponse({ expires_at: Date.now() / 1000 - 999, refresh_in: 1800 });
207
}
208
return anthropicResponse([{ type: 'text', text: 'hi' }]);
209
});
210
211
await service.messages('gh-tok', baseRequest);
212
await service.messages('gh-tok', baseRequest);
213
assert.strictEqual(mintCount, 1);
214
});
215
216
test('invalidates cached token on 401 from messages so the next call re-mints', async () => {
217
let mintCount = 0;
218
let messageCallCount = 0;
219
const service = createService(async (input) => {
220
const url = getUrl(input);
221
if (url.includes('/copilot_internal')) {
222
mintCount++;
223
return tokenResponse();
224
}
225
messageCallCount++;
226
if (messageCallCount === 1) {
227
return new Response('unauthorized', { status: 401, statusText: 'Unauthorized' });
228
}
229
return anthropicResponse([{ type: 'text', text: 'ok' }]);
230
});
231
232
await assert.rejects(() => service.messages('gh-tok', baseRequest));
233
await service.messages('gh-tok', baseRequest);
234
assert.strictEqual(mintCount, 2);
235
});
236
237
test('invalidates cached token on 403 from models so the next call re-mints', async () => {
238
let mintCount = 0;
239
let modelsCallCount = 0;
240
const service = createService(async (input) => {
241
const url = getUrl(input);
242
if (url.includes('/copilot_internal')) {
243
mintCount++;
244
return tokenResponse();
245
}
246
modelsCallCount++;
247
if (modelsCallCount === 1) {
248
return new Response('forbidden', { status: 403, statusText: 'Forbidden' });
249
}
250
return modelsResponse([]);
251
});
252
253
await assert.rejects(() => service.models('gh-tok'));
254
await service.models('gh-tok');
255
assert.strictEqual(mintCount, 2);
256
});
257
258
test('does not re-mint when copilot token has plenty of time left', async () => {
259
let mintCount = 0;
260
const service = createService(async (input) => {
261
const url = getUrl(input);
262
if (url.includes('/copilot_internal')) {
263
mintCount++;
264
return tokenResponse({ expires_at: Date.now() / 1000 + 7200 });
265
}
266
return anthropicResponse([{ type: 'text', text: 'hi' }]);
267
});
268
269
await service.messages('gh-tok', baseRequest);
270
await service.messages('gh-tok', baseRequest);
271
assert.strictEqual(mintCount, 1);
272
});
273
274
test('uses endpoints.api from the token envelope as the CAPI base', async () => {
275
const { fetch: fetchFn, captured } = routingFetch(
276
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
277
{ endpoints: { api: 'https://custom.copilot.example.com' } },
278
);
279
const service = createService(fetchFn);
280
281
await service.messages('gh-tok', baseRequest);
282
assert.strictEqual(captured().url, 'https://custom.copilot.example.com/v1/messages');
283
});
284
285
test('falls back to default API base when endpoints.api is missing', async () => {
286
const { fetch: fetchFn, captured } = routingFetch(
287
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
288
);
289
const service = createService(fetchFn);
290
291
await service.messages('gh-tok', baseRequest);
292
assert.strictEqual(captured().url, 'https://api.githubcopilot.com/v1/messages');
293
});
294
295
test('sends the github token as Authorization header to the mint endpoint', async () => {
296
let capturedAuthHeader: string | undefined;
297
const service = createService(async (input, init) => {
298
const url = getUrl(input);
299
if (url.includes('/copilot_internal')) {
300
const headers = init?.headers as Record<string, string>;
301
capturedAuthHeader = headers?.['Authorization'];
302
return tokenResponse();
303
}
304
return anthropicResponse([{ type: 'text', text: 'ok' }]);
305
});
306
307
await service.messages('my-secret-gh-token', baseRequest);
308
assert.strictEqual(capturedAuthHeader, 'token my-secret-gh-token');
309
});
310
311
test('throws on 403 from token mint', async () => {
312
const service = createService(async () => new Response('{"message":"Not authorized"}', { status: 403, statusText: 'Forbidden' }));
313
await assert.rejects(
314
() => service.messages('bad-tok', baseRequest),
315
(err: Error) => err.message.includes('Copilot token minting failed: 403'),
316
);
317
});
318
319
test('throws on 500 from token mint', async () => {
320
const service = createService(async () => new Response('internal error', { status: 500, statusText: 'Internal Server Error' }));
321
await assert.rejects(
322
() => service.messages('gh-tok', baseRequest),
323
(err: Error) => err.message.includes('Copilot token minting failed: 500'),
324
);
325
});
326
327
test('does not double-mint when concurrent requests race on first call', async () => {
328
let mintCount = 0;
329
const service = createService(async (input) => {
330
const url = getUrl(input);
331
if (url.includes('/copilot_internal')) {
332
mintCount++;
333
await new Promise(r => setTimeout(r, 10)); // ensure overlap
334
return tokenResponse();
335
}
336
return anthropicResponse([{ type: 'text', text: 'ok' }]);
337
});
338
339
await Promise.all([
340
service.messages('gh-tok', baseRequest),
341
service.messages('gh-tok', baseRequest),
342
]);
343
assert.strictEqual(mintCount, 1);
344
});
345
346
test('in-flight mint dedup spans concurrent messages + models calls', async () => {
347
let mintCount = 0;
348
const service = createService(async (input) => {
349
const url = getUrl(input);
350
if (url.includes('/copilot_internal')) {
351
mintCount++;
352
await new Promise(r => setTimeout(r, 10));
353
return tokenResponse();
354
}
355
if (url.includes('/models')) {
356
return modelsResponse([]);
357
}
358
return anthropicResponse([{ type: 'text', text: 'ok' }]);
359
});
360
361
await Promise.all([
362
service.messages('gh-tok', baseRequest),
363
service.models('gh-tok'),
364
]);
365
assert.strictEqual(mintCount, 1);
366
});
367
368
test('error from token mint does not include the github token', async () => {
369
const service = createService(async () => new Response('forbidden', { status: 403, statusText: 'Forbidden' }));
370
await assert.rejects(
371
() => service.messages('super-secret-gh-token-xyz', baseRequest),
372
(err: Error) => !err.message.includes('super-secret-gh-token-xyz'),
373
);
374
});
375
376
test('error from CAPI does not include the copilot or github token', async () => {
377
const service = createService(async (input) => {
378
const url = getUrl(input);
379
if (url.includes('/copilot_internal')) {
380
return tokenResponse({ token: 'super-secret-copilot-token-xyz' });
381
}
382
return new Response('rate limited', { status: 429, statusText: 'Too Many Requests' });
383
});
384
await assert.rejects(
385
() => service.messages('super-secret-gh-token-xyz', baseRequest),
386
(err: Error) => !err.message.includes('super-secret-copilot-token-xyz') && !err.message.includes('super-secret-gh-token-xyz'),
387
);
388
});
389
390
test('mints independently for concurrent requests with different github tokens', async () => {
391
const minted: string[] = [];
392
const service = createService(async (input, init) => {
393
const url = getUrl(input);
394
if (url.includes('/copilot_internal')) {
395
const auth = (init?.headers as Record<string, string>)?.['Authorization'] ?? '';
396
minted.push(auth);
397
await new Promise(r => setTimeout(r, 10)); // ensure overlap
398
return tokenResponse();
399
}
400
return anthropicResponse([{ type: 'text', text: 'ok' }]);
401
});
402
403
await Promise.all([
404
service.messages('gh-tok-A', baseRequest),
405
service.messages('gh-tok-B', baseRequest),
406
]);
407
assert.strictEqual(minted.length, 2);
408
assert.ok(minted.some(h => h.includes('gh-tok-A')));
409
assert.ok(minted.some(h => h.includes('gh-tok-B')));
410
});
411
});
412
413
// #endregion
414
415
// #region Request Format
416
417
suite('Request Format', () => {
418
419
test('sends system as a top-level text-block array', async () => {
420
const { fetch: fetchFn, captured } = routingFetch(
421
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
422
);
423
const service = createService(fetchFn);
424
425
await service.messages('gh-tok', { ...baseRequest, system: 'You are helpful.' });
426
const body = JSON.parse(captured().init?.body as string);
427
428
assert.deepStrictEqual(body.system, [{ type: 'text', text: 'You are helpful.' }]);
429
});
430
431
test('omits system field entirely when not provided', async () => {
432
const { fetch: fetchFn, captured } = routingFetch(
433
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
434
);
435
const service = createService(fetchFn);
436
437
await service.messages('gh-tok', baseRequest);
438
const body = JSON.parse(captured().init?.body as string);
439
440
assert.strictEqual(body.system, undefined);
441
});
442
443
test('sends max_tokens in the body', async () => {
444
const { fetch: fetchFn, captured } = routingFetch(
445
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
446
);
447
const service = createService(fetchFn);
448
449
await service.messages('gh-tok', { ...baseRequest, max_tokens: 8192 });
450
const body = JSON.parse(captured().init?.body as string);
451
452
assert.strictEqual(body.max_tokens, 8192);
453
});
454
455
test('non-streaming sends stream=false in the body', async () => {
456
const { fetch: fetchFn, captured } = routingFetch(
457
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
458
);
459
const service = createService(fetchFn);
460
461
await service.messages('gh-tok', baseRequest);
462
const body = JSON.parse(captured().init?.body as string);
463
464
assert.strictEqual(body.stream, false);
465
});
466
467
test('defaults to non-streaming when stream is omitted', async () => {
468
const { fetch: fetchFn, captured } = routingFetch(
469
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
470
);
471
const service = createService(fetchFn);
472
473
await service.messages('gh-tok', baseRequest);
474
const body = JSON.parse(captured().init?.body as string);
475
476
assert.strictEqual(body.stream, false);
477
});
478
479
test('streaming sends stream=true in the body', async () => {
480
const { fetch: fetchFn, captured } = routingFetch(
481
() => sseResponse([sseLines('data: {"type":"message_stop"}')]),
482
);
483
const service = createService(fetchFn);
484
485
await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
486
const body = JSON.parse(captured().init?.body as string);
487
488
assert.strictEqual(body.stream, true);
489
});
490
491
test('sends correct CAPI headers', async () => {
492
const { fetch: fetchFn, captured } = routingFetch(
493
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
494
);
495
const service = createService(fetchFn);
496
497
await service.messages('gh-tok', baseRequest);
498
const headers = captured().init?.headers as Record<string, string>;
499
500
assert.strictEqual(headers['Content-Type'], 'application/json');
501
assert.strictEqual(headers['Authorization'], 'Bearer copilot-tok-abc');
502
assert.strictEqual(headers['OpenAI-Intent'], 'conversation');
503
assert.ok(headers['X-Request-Id'], 'should have a request id');
504
assert.ok(headers['X-GitHub-Api-Version'], 'CAPIClient should inject API version');
505
assert.ok(headers['VScode-SessionId'], 'CAPIClient should inject session id');
506
});
507
508
test('passes messages through as-is', async () => {
509
const { fetch: fetchFn, captured } = routingFetch(
510
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
511
);
512
const service = createService(fetchFn);
513
514
const messages: Anthropic.MessageParam[] = [
515
{ role: 'user', content: 'What is 2+2?' },
516
{ role: 'assistant', content: '4' },
517
{ role: 'user', content: 'Thanks!' },
518
];
519
await service.messages('gh-tok', { ...baseRequest, messages });
520
const body = JSON.parse(captured().init?.body as string);
521
522
assert.deepStrictEqual(body.messages, messages);
523
});
524
525
test('sends model in the body', async () => {
526
const { fetch: fetchFn, captured } = routingFetch(
527
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
528
);
529
const service = createService(fetchFn);
530
531
await service.messages('gh-tok', { ...baseRequest, model: 'claude-opus-4-1-20250805' });
532
const body = JSON.parse(captured().init?.body as string);
533
534
assert.strictEqual(body.model, 'claude-opus-4-1-20250805');
535
});
536
537
test('merges caller-provided headers into the request', async () => {
538
const { fetch: fetchFn, captured } = routingFetch(
539
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
540
);
541
const service = createService(fetchFn);
542
543
await service.messages('gh-tok', baseRequest, {
544
headers: { 'X-Custom-Trace': 'abc-123', 'X-Session-Id': 'sess-456' },
545
});
546
const headers = captured().init?.headers as Record<string, string>;
547
548
assert.strictEqual(headers['X-Custom-Trace'], 'abc-123');
549
assert.strictEqual(headers['X-Session-Id'], 'sess-456');
550
assert.strictEqual(headers['Authorization'], 'Bearer copilot-tok-abc', 'standard headers should not be overridden');
551
});
552
553
test('caller-supplied headers cannot override security-sensitive standard headers', async () => {
554
// Documented invariant: Authorization, Content-Type, X-Request-Id, OpenAI-Intent
555
// must always reflect the values the service computes — never the caller's.
556
const { fetch: fetchFn, captured } = routingFetch(
557
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
558
);
559
const service = createService(fetchFn);
560
561
await service.messages('gh-tok', baseRequest, {
562
headers: {
563
'Authorization': 'Bearer attacker-token',
564
'Content-Type': 'text/plain',
565
'X-Request-Id': 'attacker-id',
566
'OpenAI-Intent': 'attacker-intent',
567
},
568
});
569
const headers = captured().init?.headers as Record<string, string>;
570
571
assert.strictEqual(headers['Authorization'], 'Bearer copilot-tok-abc');
572
assert.strictEqual(headers['Content-Type'], 'application/json');
573
assert.notStrictEqual(headers['X-Request-Id'], 'attacker-id');
574
assert.strictEqual(headers['OpenAI-Intent'], 'conversation');
575
});
576
});
577
578
// #endregion
579
580
// #region Non-Streaming Responses
581
582
suite('Non-Streaming Responses', () => {
583
584
test('returns text content from a single text block', async () => {
585
const { fetch: fetchFn } = routingFetch(
586
() => anthropicResponse([{ type: 'text', text: 'The answer is 42.' }]),
587
);
588
const service = createService(fetchFn);
589
590
const result = await service.messages('gh-tok', baseRequest);
591
assert.strictEqual(getText(result), 'The answer is 42.');
592
});
593
594
test('concatenates multiple text blocks', async () => {
595
const { fetch: fetchFn } = routingFetch(
596
() => anthropicResponse([
597
{ type: 'text', text: 'First part. ' },
598
{ type: 'text', text: 'Second part.' },
599
]),
600
);
601
const service = createService(fetchFn);
602
603
const result = await service.messages('gh-tok', baseRequest);
604
assert.strictEqual(getText(result), 'First part. Second part.');
605
});
606
607
test('skips non-text content blocks (tool_use, thinking)', async () => {
608
const { fetch: fetchFn } = routingFetch(
609
() => anthropicResponse([
610
{ type: 'thinking', text: 'let me think...' },
611
{ type: 'text', text: 'the answer' },
612
{ type: 'tool_use' },
613
]),
614
);
615
const service = createService(fetchFn);
616
617
const result = await service.messages('gh-tok', baseRequest);
618
assert.strictEqual(getText(result), 'the answer');
619
});
620
621
test('returns empty string when no text blocks are present', async () => {
622
const { fetch: fetchFn } = routingFetch(
623
() => anthropicResponse([{ type: 'tool_use' }]),
624
);
625
const service = createService(fetchFn);
626
627
const result = await service.messages('gh-tok', baseRequest);
628
assert.strictEqual(getText(result), '');
629
});
630
631
test('returns the stop reason', async () => {
632
const { fetch: fetchFn } = routingFetch(
633
() => anthropicResponse([{ type: 'text', text: 'ok' }], 'max_tokens'),
634
);
635
const service = createService(fetchFn);
636
637
const result = await service.messages('gh-tok', baseRequest);
638
assert.strictEqual(result.stop_reason, 'max_tokens');
639
});
640
641
test('stop_reason is null when missing from server response', async () => {
642
const { fetch: fetchFn } = routingFetch(() => {
643
return new Response(JSON.stringify({
644
content: [{ type: 'text', text: 'ok' }],
645
}), { status: 200 });
646
});
647
const service = createService(fetchFn);
648
649
const result = await service.messages('gh-tok', baseRequest);
650
assert.strictEqual(result.stop_reason ?? null, null);
651
});
652
653
test('throws on 429 rate limit', async () => {
654
const { fetch: fetchFn } = routingFetch(
655
() => new Response('{"error":"rate_limited"}', { status: 429, statusText: 'Too Many Requests' }),
656
);
657
const service = createService(fetchFn);
658
659
await assert.rejects(
660
() => service.messages('gh-tok', baseRequest),
661
(err: Error) => err.message.includes('CAPI request failed: 429'),
662
);
663
});
664
665
test('throws on 500 server error', async () => {
666
const { fetch: fetchFn } = routingFetch(
667
() => new Response('internal server error', { status: 500, statusText: 'Internal Server Error' }),
668
);
669
const service = createService(fetchFn);
670
671
await assert.rejects(
672
() => service.messages('gh-tok', baseRequest),
673
(err: Error) => err.message.includes('CAPI request failed: 500'),
674
);
675
});
676
});
677
678
// #endregion
679
680
// #region Streaming Responses
681
682
suite('Streaming Responses', () => {
683
684
function collectTextDeltas(events: Anthropic.MessageStreamEvent[]): string[] {
685
return events
686
.filter((e): e is Anthropic.RawContentBlockDeltaEvent =>
687
e.type === 'content_block_delta' && e.delta.type === 'text_delta')
688
.map(e => (e.delta as Anthropic.TextDelta).text);
689
}
690
691
test('yields text deltas from content_block_delta events', async () => {
692
const service = streamService([
693
sseLines(
694
'event: content_block_delta',
695
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}',
696
'',
697
'event: content_block_delta',
698
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" world"}}',
699
),
700
sseLines(
701
'event: message_stop',
702
'data: {"type":"message_stop"}',
703
),
704
]);
705
706
const events = await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
707
assert.deepStrictEqual(collectTextDeltas(events), ['Hello', ' world']);
708
});
709
710
test('handles data split across multiple network chunks', async () => {
711
const encoder = new TextEncoder();
712
const service = streamService([
713
encoder.encode('event: content_block_delta\ndata: {"type":"content_bl'),
714
encoder.encode('ock_delta","index":0,"delta":{"type":"text_delta","text":"split"}}\n'),
715
sseLines(
716
'event: message_stop',
717
'data: {"type":"message_stop"}',
718
),
719
]);
720
721
const events = await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
722
assert.deepStrictEqual(collectTextDeltas(events), ['split']);
723
});
724
725
test('handles a data line split right at the newline boundary', async () => {
726
const encoder = new TextEncoder();
727
const service = streamService([
728
encoder.encode('data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"chunk1"}}'),
729
encoder.encode('\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"chunk2"}}\n'),
730
sseLines('data: {"type":"message_stop"}'),
731
]);
732
733
const events = await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
734
assert.deepStrictEqual(collectTextDeltas(events), ['chunk1', 'chunk2']);
735
});
736
737
test('skips event: lines, comment lines, and blank lines', async () => {
738
const service = streamService([
739
sseLines(
740
': keep-alive comment',
741
'event: content_block_delta',
742
'',
743
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}',
744
'',
745
'event: message_stop',
746
'data: {"type":"message_stop"}',
747
),
748
]);
749
750
const events = await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
751
assert.deepStrictEqual(collectTextDeltas(events), ['ok']);
752
});
753
754
test('handles many small deltas', async () => {
755
const deltas = Array.from({ length: 100 }, (_, i) =>
756
`data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"w${i}"}}`
757
);
758
const service = streamService([
759
sseLines(...deltas),
760
sseLines('data: {"type":"message_stop"}'),
761
]);
762
763
const texts = collectTextDeltas(await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const })));
764
assert.strictEqual(texts.length, 100);
765
assert.strictEqual(texts[0], 'w0');
766
assert.strictEqual(texts[99], 'w99');
767
});
768
769
test('throws on error event with message', async () => {
770
const service = streamService([
771
sseLines(
772
'event: error',
773
'data: {"type":"error","error":{"message":"overloaded"}}',
774
),
775
]);
776
777
await assert.rejects(
778
() => collect(service.messages('gh-tok', { ...baseRequest, stream: true as const })),
779
(err: Error) => err.message === 'overloaded',
780
);
781
});
782
783
test('throws on error event without message', async () => {
784
const service = streamService([
785
sseLines(
786
'event: error',
787
'data: {"type":"error","error":{}}',
788
),
789
]);
790
791
await assert.rejects(
792
() => collect(service.messages('gh-tok', { ...baseRequest, stream: true as const })),
793
(err: Error) => err.message === 'Unknown streaming error',
794
);
795
});
796
797
test('throws on non-200 CAPI response', async () => {
798
const { fetch: fetchFn } = routingFetch(
799
() => new Response('overloaded', { status: 529, statusText: 'Overloaded' }),
800
);
801
const service = createService(fetchFn);
802
803
await assert.rejects(
804
() => collect(service.messages('gh-tok', { ...baseRequest, stream: true as const })),
805
(err: Error) => err.message.includes('CAPI request failed: 529'),
806
);
807
});
808
809
test('throws when response has no body', async () => {
810
const { fetch: fetchFn } = routingFetch(
811
() => new Response(null, { status: 200 }),
812
);
813
const service = createService(fetchFn);
814
815
await assert.rejects(
816
() => collect(service.messages('gh-tok', { ...baseRequest, stream: true as const })),
817
(err: Error) => err.message.includes('no body'),
818
);
819
});
820
821
test('survives malformed JSON in the stream (skips the line)', async () => {
822
const service = streamService([
823
sseLines(
824
'data: not-valid-json',
825
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}',
826
'data: {"type":"message_stop"}',
827
),
828
]);
829
830
const events = await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
831
assert.deepStrictEqual(collectTextDeltas(events), ['ok']);
832
});
833
});
834
835
// #endregion
836
837
// #region Raw Event Stream (messages())
838
839
suite('Raw Event Stream (messages())', () => {
840
841
test('yields all six protocol event types in order', async () => {
842
const service = streamService([
843
sseLines(
844
'data: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","content":[],"model":"claude-sonnet-4-5","stop_reason":null,"usage":{"input_tokens":1,"output_tokens":1}}}',
845
'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}',
846
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}',
847
'data: {"type":"content_block_stop","index":0}',
848
'data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":1}}',
849
'data: {"type":"message_stop"}',
850
),
851
]);
852
853
const events = await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
854
assert.deepStrictEqual(events.map(e => e.type), [
855
'message_start',
856
'content_block_start',
857
'content_block_delta',
858
'content_block_stop',
859
'message_delta',
860
'message_stop',
861
]);
862
});
863
864
test('message_stop is the last yielded event', async () => {
865
const service = streamService([
866
sseLines(
867
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"a"}}',
868
'data: {"type":"message_stop"}',
869
),
870
]);
871
872
const events = await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
873
assert.strictEqual(events.length, 2);
874
assert.strictEqual(events[events.length - 1].type, 'message_stop');
875
});
876
877
test('stops after message_stop even if extra SSE data follows', async () => {
878
const service = streamService([
879
sseLines(
880
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"a"}}',
881
'data: {"type":"message_stop"}',
882
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"SHOULD_NOT_APPEAR"}}',
883
),
884
]);
885
886
const events = await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
887
const texts = events
888
.filter((e): e is Anthropic.RawContentBlockDeltaEvent => e.type === 'content_block_delta')
889
.map(e => e.delta.type === 'text_delta' ? e.delta.text : '');
890
assert.deepStrictEqual(texts, ['a']);
891
});
892
893
test('yields thinking_delta events (not filtered by messages())', async () => {
894
const service = streamService([
895
sseLines(
896
'data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"hmm"}}',
897
'data: {"type":"message_stop"}',
898
),
899
]);
900
901
const events = await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
902
const delta = events.find((e): e is Anthropic.RawContentBlockDeltaEvent => e.type === 'content_block_delta');
903
assert.ok(delta);
904
assert.strictEqual(delta.delta.type, 'thinking_delta');
905
});
906
907
test('yields input_json_delta events', async () => {
908
const service = streamService([
909
sseLines(
910
'data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\\"k\\":1}"}}',
911
'data: {"type":"message_stop"}',
912
),
913
]);
914
915
const events = await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
916
const delta = events.find((e): e is Anthropic.RawContentBlockDeltaEvent => e.type === 'content_block_delta');
917
assert.ok(delta);
918
assert.strictEqual(delta.delta.type, 'input_json_delta');
919
});
920
921
test('yields message_delta with stop_reason payload', async () => {
922
const service = streamService([
923
sseLines(
924
'data: {"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null},"usage":{"output_tokens":7}}',
925
'data: {"type":"message_stop"}',
926
),
927
]);
928
929
const events = await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
930
const msgDelta = events.find((e): e is Anthropic.RawMessageDeltaEvent => e.type === 'message_delta');
931
assert.ok(msgDelta);
932
assert.strictEqual(msgDelta.delta.stop_reason, 'max_tokens');
933
});
934
935
test('tool_use block events round-trip through messages()', async () => {
936
const service = streamService([
937
sseLines(
938
'data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"tu_1","name":"read_file","input":{}}}',
939
'data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\\"path\\":"}}',
940
'data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\\"/tmp/x\\"}"}}',
941
'data: {"type":"content_block_stop","index":0}',
942
'data: {"type":"message_stop"}',
943
),
944
]);
945
946
const events = await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
947
const blockStart = events.find((e): e is Anthropic.RawContentBlockStartEvent => e.type === 'content_block_start');
948
assert.ok(blockStart, 'expected content_block_start event');
949
assert.strictEqual(blockStart.content_block.type, 'tool_use');
950
assert.strictEqual((blockStart.content_block as Anthropic.ToolUseBlock).name, 'read_file');
951
952
const jsonDeltas = events.filter(
953
(e): e is Anthropic.RawContentBlockDeltaEvent =>
954
e.type === 'content_block_delta' && e.delta.type === 'input_json_delta',
955
);
956
assert.strictEqual(jsonDeltas.length, 2);
957
assert.strictEqual(events[events.length - 1].type, 'message_stop');
958
});
959
});
960
961
// #endregion
962
963
// #region countTokens
964
965
suite('countTokens', () => {
966
967
test('throws "countTokens not supported by CAPI"', async () => {
968
const service = createService(async () => new Response('{}', { status: 200 }));
969
await assert.rejects(
970
() => service.countTokens('gh-tok', { model: 'claude-sonnet-4-5', messages: [{ role: 'user', content: 'hi' }] }),
971
(err: Error) => err.message.includes('countTokens not supported by CAPI'),
972
);
973
});
974
975
test('does not mint a token before throwing', async () => {
976
let mintCount = 0;
977
const service = createService(async (input) => {
978
const url = getUrl(input);
979
if (url.includes('/copilot_internal')) {
980
mintCount++;
981
return tokenResponse();
982
}
983
return new Response('{}', { status: 200 });
984
});
985
986
await assert.rejects(
987
() => service.countTokens('gh-tok', { model: 'claude-sonnet-4-5', messages: [{ role: 'user', content: 'hi' }] }),
988
);
989
assert.strictEqual(mintCount, 0);
990
});
991
});
992
993
// #endregion
994
995
// #region Streaming + Non-Streaming Shared Behavior
996
997
suite('Shared Behavior', () => {
998
999
test('streaming and non-streaming hit the same /v1/messages endpoint', async () => {
1000
const urls: string[] = [];
1001
const service = createService(async (input) => {
1002
const url = getUrl(input);
1003
if (url.includes('/copilot_internal')) {
1004
return tokenResponse();
1005
}
1006
urls.push(url);
1007
if (urls.length === 1) {
1008
return anthropicResponse([{ type: 'text', text: 'ok' }]);
1009
}
1010
return sseResponse([sseLines('data: {"type":"message_stop"}')]);
1011
});
1012
1013
await service.messages('gh-tok', baseRequest);
1014
await collect(service.messages('gh-tok', { ...baseRequest, stream: true as const }));
1015
1016
assert.strictEqual(urls.length, 2);
1017
assert.ok(urls[0].endsWith('/v1/messages'));
1018
assert.ok(urls[1].endsWith('/v1/messages'));
1019
});
1020
1021
test('both modes share the same cached copilot token', async () => {
1022
let mintCount = 0;
1023
const service = createService(async (input) => {
1024
const url = getUrl(input);
1025
if (url.includes('/copilot_internal')) {
1026
mintCount++;
1027
return tokenResponse();
1028
}
1029
return anthropicResponse([{ type: 'text', text: 'ok' }]);
1030
});
1031
1032
await service.messages('gh-tok', baseRequest);
1033
await service.messages('gh-tok', baseRequest);
1034
assert.strictEqual(mintCount, 1);
1035
});
1036
});
1037
1038
// #endregion
1039
1040
// #region Cancellation
1041
1042
suite('Cancellation', () => {
1043
1044
test('forwards AbortSignal to fetch for messages', async () => {
1045
const controller = new AbortController();
1046
let capturedSignal: AbortSignal | undefined;
1047
const service = createService(async (input, init) => {
1048
const url = getUrl(input);
1049
if (url.includes('/copilot_internal')) {
1050
return tokenResponse();
1051
}
1052
capturedSignal = init?.signal as AbortSignal;
1053
return anthropicResponse([{ type: 'text', text: 'ok' }]);
1054
});
1055
1056
await service.messages('gh-tok', baseRequest, { signal: controller.signal });
1057
assert.strictEqual(capturedSignal, controller.signal);
1058
});
1059
1060
test('forwards AbortSignal to fetch for models', async () => {
1061
const controller = new AbortController();
1062
let capturedSignal: AbortSignal | undefined;
1063
const service = createService(async (input, init) => {
1064
const url = getUrl(input);
1065
if (url.includes('/copilot_internal')) {
1066
return tokenResponse();
1067
}
1068
capturedSignal = init?.signal as AbortSignal;
1069
return modelsResponse([]);
1070
});
1071
1072
await service.models('gh-tok', { signal: controller.signal });
1073
assert.strictEqual(capturedSignal, controller.signal);
1074
});
1075
1076
test('does not forward AbortSignal to the shared token mint fetch', async () => {
1077
const controller = new AbortController();
1078
let mintSignal: AbortSignal | undefined;
1079
const service = createService(async (input, init) => {
1080
const url = getUrl(input);
1081
if (url.includes('/copilot_internal')) {
1082
mintSignal = init?.signal as AbortSignal;
1083
return tokenResponse();
1084
}
1085
return anthropicResponse([{ type: 'text', text: 'ok' }]);
1086
});
1087
1088
await service.messages('gh-tok', baseRequest, { signal: controller.signal });
1089
assert.strictEqual(mintSignal, undefined);
1090
});
1091
1092
test('cancels the underlying SSE stream when the consumer breaks early', async () => {
1093
let cancelled = false;
1094
const body = new ReadableStream<Uint8Array>({
1095
pull(controller) {
1096
controller.enqueue(sseLines(
1097
'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Hello"}}',
1098
));
1099
},
1100
cancel() {
1101
cancelled = true;
1102
},
1103
});
1104
const service = createService(async (input) => {
1105
const url = getUrl(input);
1106
if (url.includes('/copilot_internal')) {
1107
return tokenResponse();
1108
}
1109
return new Response(body, { status: 200, headers: { 'Content-Type': 'text/event-stream' } });
1110
});
1111
1112
const iter = service.messages('gh-tok', { ...baseRequest, stream: true });
1113
for await (const _ of iter) {
1114
break; // abandon after first chunk
1115
}
1116
assert.strictEqual(cancelled, true);
1117
});
1118
1119
test('cancels the underlying SSE stream after message_stop terminates the generator', async () => {
1120
let cancelled = false;
1121
const body = new ReadableStream<Uint8Array>({
1122
start(controller) {
1123
controller.enqueue(sseLines(
1124
'data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Hello"}}',
1125
'data: {"type":"message_stop"}',
1126
));
1127
// Server is still alive — connection must be released by the client
1128
// even though the producer hasn't closed yet.
1129
},
1130
cancel() {
1131
cancelled = true;
1132
},
1133
});
1134
const service = createService(async (input) => {
1135
const url = getUrl(input);
1136
if (url.includes('/copilot_internal')) {
1137
return tokenResponse();
1138
}
1139
return new Response(body, { status: 200, headers: { 'Content-Type': 'text/event-stream' } });
1140
});
1141
1142
await collect(service.messages('gh-tok', { ...baseRequest, stream: true }));
1143
assert.strictEqual(cancelled, true);
1144
});
1145
1146
test('cancels the underlying SSE stream when the generator throws', async () => {
1147
let cancelled = false;
1148
const body = new ReadableStream<Uint8Array>({
1149
start(controller) {
1150
controller.enqueue(sseLines(
1151
'data: {"type":"error","error":{"message":"boom"}}',
1152
));
1153
},
1154
cancel() {
1155
cancelled = true;
1156
},
1157
});
1158
const service = createService(async (input) => {
1159
const url = getUrl(input);
1160
if (url.includes('/copilot_internal')) {
1161
return tokenResponse();
1162
}
1163
return new Response(body, { status: 200, headers: { 'Content-Type': 'text/event-stream' } });
1164
});
1165
1166
await assert.rejects(() => collect(service.messages('gh-tok', { ...baseRequest, stream: true })));
1167
assert.strictEqual(cancelled, true);
1168
});
1169
});
1170
1171
// #endregion
1172
1173
// #region Models
1174
1175
suite('Models', () => {
1176
1177
test('returns models from the data array', async () => {
1178
const fakeModels = [
1179
{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5', vendor: 'anthropic', supported_endpoints: ['chat/messages'] },
1180
{ id: 'claude-opus-4', name: 'Claude Opus 4', vendor: 'anthropic', supported_endpoints: ['chat/messages'] },
1181
];
1182
const service = createService(async (input) => {
1183
const url = getUrl(input);
1184
if (url.includes('/copilot_internal')) {
1185
return tokenResponse();
1186
}
1187
return modelsResponse(fakeModels);
1188
});
1189
1190
const result = await service.models('gh-tok');
1191
assert.deepStrictEqual(result, fakeModels);
1192
});
1193
1194
test('returns empty array when data is missing', async () => {
1195
const service = createService(async (input) => {
1196
const url = getUrl(input);
1197
if (url.includes('/copilot_internal')) {
1198
return tokenResponse();
1199
}
1200
return new Response(JSON.stringify({}), { status: 200 });
1201
});
1202
1203
const result = await service.models('gh-tok');
1204
assert.deepStrictEqual(result, []);
1205
});
1206
1207
test('sends Bearer token in Authorization header', async () => {
1208
let capturedAuthHeader: string | undefined;
1209
const service = createService(async (input, init) => {
1210
const url = getUrl(input);
1211
if (url.includes('/copilot_internal')) {
1212
return tokenResponse();
1213
}
1214
capturedAuthHeader = (init?.headers as Record<string, string>)?.['Authorization'];
1215
return modelsResponse([]);
1216
});
1217
1218
await service.models('gh-tok');
1219
assert.strictEqual(capturedAuthHeader, 'Bearer copilot-tok-abc');
1220
});
1221
1222
test('throws on non-200 response', async () => {
1223
const service = createService(async (input) => {
1224
const url = getUrl(input);
1225
if (url.includes('/copilot_internal')) {
1226
return tokenResponse();
1227
}
1228
return new Response('forbidden', { status: 403, statusText: 'Forbidden' });
1229
});
1230
1231
await assert.rejects(
1232
() => service.models('gh-tok'),
1233
(err: Error) => err.message.includes('CAPI models request failed: 403'),
1234
);
1235
});
1236
1237
test('reuses cached token across messages and models calls', async () => {
1238
let mintCount = 0;
1239
const service = createService(async (input) => {
1240
const url = getUrl(input);
1241
if (url.includes('/copilot_internal')) {
1242
mintCount++;
1243
return tokenResponse();
1244
}
1245
if (url.includes('/models')) {
1246
return modelsResponse([]);
1247
}
1248
return anthropicResponse([{ type: 'text', text: 'ok' }]);
1249
});
1250
1251
await service.messages('gh-tok', baseRequest);
1252
await service.models('gh-tok');
1253
assert.strictEqual(mintCount, 1);
1254
});
1255
1256
test('routes to the models endpoint URL', async () => {
1257
const { fetch: fetchFn, captured } = routingFetch(() => modelsResponse([]));
1258
const service = createService(fetchFn);
1259
1260
await service.models('gh-tok');
1261
assert.ok(captured().url.includes('/models'), `expected models URL, got: ${captured().url}`);
1262
});
1263
1264
test('caller-supplied headers cannot override Authorization in models()', async () => {
1265
let capturedHeaders: Record<string, string> | undefined;
1266
const service = createService(async (input, init) => {
1267
const url = getUrl(input);
1268
if (url.includes('/copilot_internal')) {
1269
return tokenResponse();
1270
}
1271
capturedHeaders = init?.headers as Record<string, string>;
1272
return modelsResponse([]);
1273
});
1274
1275
await service.models('gh-tok', {
1276
headers: { 'Authorization': 'Bearer attacker-token' },
1277
});
1278
assert.strictEqual(capturedHeaders?.['Authorization'], 'Bearer copilot-tok-abc');
1279
});
1280
});
1281
1282
// #endregion
1283
});
1284
1285