@@ -15,7 +15,7 @@ const { deriveCopilotApiTarget, deriveGitHubApiTarget, deriveGitHubApiBasePath,
1515const { resolveOpenCodeRoute } = require ( './providers/opencode' ) ;
1616
1717// Core proxy functions that remain in server.js
18- const { proxyWebSocket, httpProbe, validateApiKeys, keyValidationResults, resetKeyValidationState, fetchJson, extractModelIds, fetchStartupModels, reflectEndpoints, healthResponse, cachedModels, resetModelCacheState, makeModelBodyTransform, MODEL_ALIASES , buildModelsJson, writeModelsJson, createProviderServer } = require ( './server' ) ;
18+ const { proxyRequest , proxyWebSocket, httpProbe, validateApiKeys, keyValidationResults, resetKeyValidationState, fetchJson, extractModelIds, fetchStartupModels, reflectEndpoints, healthResponse, cachedModels, resetModelCacheState, makeModelBodyTransform, MODEL_ALIASES , buildModelsJson, writeModelsJson, createProviderServer } = require ( './server' ) ;
1919
2020describe ( 'normalizeApiTarget' , ( ) => {
2121 it ( 'should strip https:// prefix' , ( ) => {
@@ -2773,3 +2773,75 @@ describe('provider adapter alwaysBind', () => {
27732773 expect ( body . error ) . toMatch ( / n o c a n d i d a t e p r o v i d e r c r e d e n t i a l s / ) ;
27742774 } ) ;
27752775} ) ;
2776+
2777+ // ── proxyRequest X-Initiator billing header injection ─────────────────────────
2778+ //
2779+ // When forwarding to the Copilot API, the proxy must inject "X-Initiator: agent"
2780+ // when the header is absent so that autonomous turns are billed correctly instead
2781+ // of defaulting to the (more expensive) "user" rate.
2782+ //
2783+ describe ( 'proxyRequest X-Initiator injection' , ( ) => {
2784+ /** Minimal mock for http.IncomingMessage backed by EventEmitter. */
2785+ function makeReq ( headers = { } ) {
2786+ const req = new EventEmitter ( ) ;
2787+ req . url = '/v1/chat/completions' ;
2788+ req . method = 'POST' ;
2789+ req . headers = { 'content-type' : 'application/json' , ...headers } ;
2790+ return req ;
2791+ }
2792+
2793+ /** Minimal mock for http.ServerResponse. */
2794+ function makeRes ( ) {
2795+ return {
2796+ headersSent : false ,
2797+ setHeader : jest . fn ( ) ,
2798+ writeHead : jest . fn ( ) ,
2799+ end : jest . fn ( ) ,
2800+ } ;
2801+ }
2802+
2803+ afterEach ( ( ) => {
2804+ jest . restoreAllMocks ( ) ;
2805+ } ) ;
2806+
2807+ /**
2808+ * Mock https.request to capture the outgoing options (including headers)
2809+ * without making a real network connection.
2810+ */
2811+ function mockHttpsRequest ( ) {
2812+ let capturedOptions ;
2813+ jest . spyOn ( https , 'request' ) . mockImplementation ( ( options ) => {
2814+ capturedOptions = options ;
2815+ const proxyReq = new EventEmitter ( ) ;
2816+ proxyReq . end = jest . fn ( ) ;
2817+ proxyReq . write = jest . fn ( ) ;
2818+ proxyReq . destroy = jest . fn ( ) ;
2819+ return proxyReq ;
2820+ } ) ;
2821+ return { getCaptured : ( ) => capturedOptions } ;
2822+ }
2823+
2824+ it ( 'injects x-initiator: agent when absent on copilot requests' , ( ) => {
2825+ const { getCaptured } = mockHttpsRequest ( ) ;
2826+ const req = makeReq ( ) ;
2827+ proxyRequest ( req , makeRes ( ) , 'api.githubcopilot.com' , { 'Authorization' : 'Bearer token' } , 'copilot' ) ;
2828+ req . emit ( 'end' ) ;
2829+ expect ( getCaptured ( ) . headers [ 'x-initiator' ] ) . toBe ( 'agent' ) ;
2830+ } ) ;
2831+
2832+ it ( 'preserves a client-supplied x-initiator value on copilot requests' , ( ) => {
2833+ const { getCaptured } = mockHttpsRequest ( ) ;
2834+ const req = makeReq ( { 'x-initiator' : 'user' } ) ;
2835+ proxyRequest ( req , makeRes ( ) , 'api.githubcopilot.com' , { 'Authorization' : 'Bearer token' } , 'copilot' ) ;
2836+ req . emit ( 'end' ) ;
2837+ expect ( getCaptured ( ) . headers [ 'x-initiator' ] ) . toBe ( 'user' ) ;
2838+ } ) ;
2839+
2840+ it ( 'does not inject x-initiator for non-copilot providers' , ( ) => {
2841+ const { getCaptured } = mockHttpsRequest ( ) ;
2842+ const req = makeReq ( ) ;
2843+ proxyRequest ( req , makeRes ( ) , 'api.anthropic.com' , { 'x-api-key' : 'sk-ant-test' } , 'anthropic' ) ;
2844+ req . emit ( 'end' ) ;
2845+ expect ( getCaptured ( ) . headers [ 'x-initiator' ] ) . toBeUndefined ( ) ;
2846+ } ) ;
2847+ } ) ;
0 commit comments