@@ -85,6 +85,77 @@ const limiter = rateLimiter.create();
8585/** When false, token-budget warnings are never injected into request bodies. */
8686const isSteeringEnabled = ( ) => process . env . AWF_ENABLE_TOKEN_STEERING === 'true' ;
8787
88+ const anthropicDeprecatedBetaValues = new Set ( ) ;
89+ const ANTHROPIC_DEPRECATED_BETA_PATTERN = / U n e x p e c t e d v a l u e \( s \) \s + ` ( [ ^ ` ] + ) ` \s + f o r t h e ` a n t h r o p i c - b e t a ` h e a d e r / ;
90+
91+ function normalizeAnthropicBetaHeaderValue ( value ) {
92+ if ( ! value ) return '' ;
93+ return Array . isArray ( value ) ? value . join ( ',' ) : String ( value ) ;
94+ }
95+
96+ function splitAnthropicBetaHeaderValue ( value ) {
97+ return normalizeAnthropicBetaHeaderValue ( value ) . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( Boolean ) ;
98+ }
99+
100+ function updateAnthropicBetaHeader ( headers , values ) {
101+ if ( ! values . length ) {
102+ delete headers [ 'anthropic-beta' ] ;
103+ return ;
104+ }
105+ headers [ 'anthropic-beta' ] = values . join ( ',' ) ;
106+ }
107+
108+ function stripAnthropicBetaValuesFromHeaders ( headers , valuesToStrip ) {
109+ if ( ! headers [ 'anthropic-beta' ] || ! valuesToStrip . size ) return null ;
110+ const existingValues = splitAnthropicBetaHeaderValue ( headers [ 'anthropic-beta' ] ) ;
111+ if ( ! existingValues . length ) {
112+ delete headers [ 'anthropic-beta' ] ;
113+ return { removed : [ ] , remaining : [ ] } ;
114+ }
115+ const remaining = existingValues . filter ( value => ! valuesToStrip . has ( value ) ) ;
116+ const removed = existingValues . filter ( value => valuesToStrip . has ( value ) ) ;
117+ if ( ! removed . length ) return null ;
118+ updateAnthropicBetaHeader ( headers , remaining ) ;
119+ return { removed, remaining } ;
120+ }
121+
122+ function maybeStripLearnedAnthropicBetaHeaders ( headers , requestId ) {
123+ const stripped = stripAnthropicBetaValuesFromHeaders ( headers , anthropicDeprecatedBetaValues ) ;
124+ if ( ! stripped ) return ;
125+ logRequest ( 'warn' , 'anthropic_beta_stripped' , {
126+ request_id : requestId ,
127+ provider : 'anthropic' ,
128+ mode : 'cached' ,
129+ removed_values : stripped . removed ,
130+ remaining_values : stripped . remaining ,
131+ message : 'Removed deprecated anthropic-beta values learned from prior upstream 400 responses' ,
132+ } ) ;
133+ }
134+
135+ function getAnthropicDeprecatedBetaValueFromBody ( body ) {
136+ const match = body . toString ( 'utf8' ) . match ( ANTHROPIC_DEPRECATED_BETA_PATTERN ) ;
137+ return match ? match [ 1 ] . trim ( ) : null ;
138+ }
139+
140+ function learnAndStripAnthropicBetaHeader ( headers , deprecatedValue , requestId ) {
141+ anthropicDeprecatedBetaValues . add ( deprecatedValue ) ;
142+ if ( anthropicDeprecatedBetaValues . size > 200 ) {
143+ const oldest = anthropicDeprecatedBetaValues . values ( ) . next ( ) . value ;
144+ if ( oldest !== undefined ) anthropicDeprecatedBetaValues . delete ( oldest ) ;
145+ }
146+ const stripped = stripAnthropicBetaValuesFromHeaders ( headers , new Set ( [ deprecatedValue ] ) ) ;
147+ if ( ! stripped ) return null ;
148+ logRequest ( 'warn' , 'anthropic_beta_stripped' , {
149+ request_id : requestId ,
150+ provider : 'anthropic' ,
151+ mode : 'retry' ,
152+ removed_values : stripped . removed ,
153+ remaining_values : stripped . remaining ,
154+ message : `Removed deprecated anthropic-beta value rejected by Anthropic: ${ deprecatedValue } ` ,
155+ } ) ;
156+ return stripped ;
157+ }
158+
88159function getUrlPathForSpan ( requestUrl ) {
89160 if ( typeof requestUrl !== 'string' || ! requestUrl ) return '/' ;
90161 try {
@@ -364,6 +435,10 @@ function proxyRequest(req, res, targetHost, injectHeaders, provider, basePath =
364435 headers [ 'x-request-id' ] = requestId ;
365436 Object . assign ( headers , injectHeaders ) ;
366437
438+ if ( provider === 'anthropic' ) {
439+ maybeStripLearnedAnthropicBetaHeaders ( headers , requestId ) ;
440+ }
441+
367442 const isCopilotHost =
368443 targetHost === 'githubcopilot.com' ||
369444 targetHost . endsWith ( '.githubcopilot.com' ) ;
@@ -426,17 +501,135 @@ function proxyRequest(req, res, targetHost, injectHeaders, provider, basePath =
426501 return ;
427502 }
428503
429- const options = {
430- hostname : targetHost , port : 443 , path : upstreamPath ,
431- method : req . method , headers,
432- agent : proxyAgent ,
504+ const logRequestCompletion = ( statusCode , responseBytes , initiatorSent , billingInfo ) => {
505+ const duration = Date . now ( ) - startTime ;
506+ const sc = metrics . statusClass ( statusCode ) ;
507+ metrics . gaugeDec ( 'active_requests' , { provider } ) ;
508+ metrics . increment ( 'requests_total' , { provider, method : req . method , status_class : sc } ) ;
509+ metrics . increment ( 'response_bytes_total' , { provider } , responseBytes ) ;
510+ metrics . observe ( 'request_duration_ms' , duration , { provider } ) ;
511+ if ( statusCode >= 200 && statusCode < 300 ) {
512+ applyMaxRunsInvocation ( ) ;
513+ }
514+ const logFields = {
515+ request_id : requestId , provider, method : req . method ,
516+ path : sanitizeForLog ( req . url ) , status : statusCode ,
517+ duration_ms : duration , request_bytes : requestBytes ,
518+ response_bytes : responseBytes , upstream_host : targetHost ,
519+ } ;
520+ if ( initiatorSent ) logFields . x_initiator = initiatorSent ;
521+ if ( billingInfo ) logFields . billing = billingInfo ;
522+ logRequest ( 'info' , 'request_complete' , logFields ) ;
523+ } ;
524+
525+ const logUpstreamAuthError = ( statusCode ) => {
526+ if ( statusCode === 400 || statusCode === 401 || statusCode === 403 ) {
527+ logRequest ( 'warn' , 'upstream_auth_error' , {
528+ request_id : requestId , provider, status : statusCode ,
529+ upstream_host : targetHost , path : sanitizeForLog ( req . url ) ,
530+ message : `Upstream returned ${ statusCode } — check that the API key is valid and correctly formatted` ,
531+ } ) ;
532+ }
433533 } ;
434534
435- const proxyReq = https . request ( options , ( proxyRes ) => {
436- let responseBytes = 0 ;
437- proxyRes . on ( 'data' , ( chunk ) => { responseBytes += chunk . length ; } ) ;
535+ const sendUpstreamRequest = ( requestHeaders , hasRetried = false ) => {
536+ const options = {
537+ hostname : targetHost , port : 443 , path : upstreamPath ,
538+ method : req . method , headers : requestHeaders ,
539+ agent : proxyAgent ,
540+ } ;
541+
542+ const proxyReq = https . request ( options , ( proxyRes ) => {
543+ let responseBytes = 0 ;
544+ const billingInfo = extractBillingHeaders ( proxyRes . headers ) ;
545+ const initiatorSent = requestHeaders [ 'x-initiator' ] || null ;
546+ const shouldBufferAnthropic400 =
547+ provider === 'anthropic' &&
548+ ! hasRetried &&
549+ proxyRes . statusCode === 400 &&
550+ ! ! requestHeaders [ 'anthropic-beta' ] ;
551+
552+ proxyRes . on ( 'error' , ( err ) => {
553+ otel . endSpanError ( span , err , 502 ) ;
554+ handleRequestError ( err , {
555+ res,
556+ requestId,
557+ provider,
558+ req,
559+ targetHost,
560+ startTime,
561+ statusCode : 502 ,
562+ clientMessage : 'Response stream error' ,
563+ onHeadersSent : ( ) => {
564+ if ( typeof res . destroy === 'function' ) res . destroy ( err ) ;
565+ } ,
566+ } ) ;
567+ } ) ;
568+
569+ if ( shouldBufferAnthropic400 ) {
570+ const bufferedChunks = [ ] ;
571+ proxyRes . on ( 'data' , ( chunk ) => {
572+ responseBytes += chunk . length ;
573+ bufferedChunks . push ( chunk ) ;
574+ } ) ;
575+ proxyRes . on ( 'end' , ( ) => {
576+ const responseBody = Buffer . concat ( bufferedChunks ) ;
577+ const deprecatedValue = getAnthropicDeprecatedBetaValueFromBody ( responseBody ) ;
578+ if ( deprecatedValue ) {
579+ const retryHeaders = { ...requestHeaders } ;
580+ const stripped = learnAndStripAnthropicBetaHeader ( retryHeaders , deprecatedValue , requestId ) ;
581+ if ( stripped ) {
582+ sendUpstreamRequest ( retryHeaders , true ) ;
583+ return ;
584+ }
585+ }
586+
587+ logRequestCompletion ( proxyRes . statusCode , responseBytes , initiatorSent , billingInfo ) ;
588+ logUpstreamAuthError ( proxyRes . statusCode ) ;
589+
590+ const resHeaders = {
591+ ...proxyRes . headers ,
592+ 'x-request-id' : requestId ,
593+ 'content-length' : String ( responseBody . length ) ,
594+ } ;
595+ delete resHeaders [ 'transfer-encoding' ] ;
596+ res . writeHead ( proxyRes . statusCode , resHeaders ) ;
597+ res . end ( responseBody ) ;
598+ otel . endSpan ( span , proxyRes . statusCode ) ;
599+ } ) ;
600+ return ;
601+ }
602+
603+ proxyRes . on ( 'data' , ( chunk ) => { responseBytes += chunk . length ; } ) ;
604+ proxyRes . on ( 'end' , ( ) => {
605+ logRequestCompletion ( proxyRes . statusCode , responseBytes , initiatorSent , billingInfo ) ;
606+ } ) ;
607+
608+ const resHeaders = { ...proxyRes . headers , 'x-request-id' : requestId } ;
609+ logUpstreamAuthError ( proxyRes . statusCode ) ;
610+ res . writeHead ( proxyRes . statusCode , resHeaders ) ;
611+ proxyRes . pipe ( res ) ;
612+
613+ const isStreaming = ( proxyRes . headers [ 'content-type' ] || '' ) . includes ( 'text/event-stream' ) ;
614+ trackTokenUsage ( proxyRes , {
615+ requestId,
616+ provider,
617+ path : sanitizeForLog ( req . url ) ,
618+ startTime,
619+ metrics,
620+ billingInfo,
621+ initiatorSent,
622+ onUsage : ( normalizedUsage , model ) => {
623+ otel . setTokenAttributes ( span , { provider, model, normalizedUsage, streaming : isStreaming } ) ;
624+ applyEffectiveTokenUsage ( normalizedUsage , model ) ;
625+ } ,
626+ onSpanEnd : ( statusCode ) => {
627+ otel . endSpan ( span , statusCode ) ;
628+ } ,
629+ } ) ;
630+ } ) ;
438631
439- proxyRes . on ( 'error' , ( err ) => {
632+ proxyReq . on ( 'error' , ( err ) => {
440633 otel . endSpanError ( span , err , 502 ) ;
441634 handleRequestError ( err , {
442635 res,
@@ -446,89 +639,19 @@ function proxyRequest(req, res, targetHost, injectHeaders, provider, basePath =
446639 targetHost,
447640 startTime,
448641 statusCode : 502 ,
449- clientMessage : 'Response stream error' ,
450- onHeadersSent : ( ) => {
451- if ( typeof res . destroy === 'function' ) res . destroy ( err ) ;
642+ clientMessage : 'Proxy error' ,
643+ extraMetrics : ( duration ) => {
644+ metrics . increment ( 'requests_total' , { provider, method : req . method , status_class : '5xx' } ) ;
645+ metrics . observe ( 'request_duration_ms' , duration , { provider } ) ;
452646 } ,
453647 } ) ;
454648 } ) ;
455649
456- const billingInfo = extractBillingHeaders ( proxyRes . headers ) ;
457- const initiatorSent = headers [ 'x-initiator' ] || null ;
458-
459- proxyRes . on ( 'end' , ( ) => {
460- const duration = Date . now ( ) - startTime ;
461- const sc = metrics . statusClass ( proxyRes . statusCode ) ;
462- metrics . gaugeDec ( 'active_requests' , { provider } ) ;
463- metrics . increment ( 'requests_total' , { provider, method : req . method , status_class : sc } ) ;
464- metrics . increment ( 'response_bytes_total' , { provider } , responseBytes ) ;
465- metrics . observe ( 'request_duration_ms' , duration , { provider } ) ;
466- if ( proxyRes . statusCode >= 200 && proxyRes . statusCode < 300 ) {
467- applyMaxRunsInvocation ( ) ;
468- }
469- const logFields = {
470- request_id : requestId , provider, method : req . method ,
471- path : sanitizeForLog ( req . url ) , status : proxyRes . statusCode ,
472- duration_ms : duration , request_bytes : requestBytes ,
473- response_bytes : responseBytes , upstream_host : targetHost ,
474- } ;
475- if ( initiatorSent ) logFields . x_initiator = initiatorSent ;
476- if ( billingInfo ) logFields . billing = billingInfo ;
477- logRequest ( 'info' , 'request_complete' , logFields ) ;
478- } ) ;
479-
480- const resHeaders = { ...proxyRes . headers , 'x-request-id' : requestId } ;
481-
482- if ( proxyRes . statusCode === 400 || proxyRes . statusCode === 401 || proxyRes . statusCode === 403 ) {
483- logRequest ( 'warn' , 'upstream_auth_error' , {
484- request_id : requestId , provider, status : proxyRes . statusCode ,
485- upstream_host : targetHost , path : sanitizeForLog ( req . url ) ,
486- message : `Upstream returned ${ proxyRes . statusCode } — check that the API key is valid and correctly formatted` ,
487- } ) ;
488- }
489-
490- res . writeHead ( proxyRes . statusCode , resHeaders ) ;
491- proxyRes . pipe ( res ) ;
492-
493- const isStreaming = ( proxyRes . headers [ 'content-type' ] || '' ) . includes ( 'text/event-stream' ) ;
494- trackTokenUsage ( proxyRes , {
495- requestId,
496- provider,
497- path : sanitizeForLog ( req . url ) ,
498- startTime,
499- metrics,
500- billingInfo,
501- initiatorSent,
502- onUsage : ( normalizedUsage , model ) => {
503- otel . setTokenAttributes ( span , { provider, model, normalizedUsage, streaming : isStreaming } ) ;
504- applyEffectiveTokenUsage ( normalizedUsage , model ) ;
505- } ,
506- onSpanEnd : ( statusCode ) => {
507- otel . endSpan ( span , statusCode ) ;
508- } ,
509- } ) ;
510- } ) ;
511-
512- proxyReq . on ( 'error' , ( err ) => {
513- otel . endSpanError ( span , err , 502 ) ;
514- handleRequestError ( err , {
515- res,
516- requestId,
517- provider,
518- req,
519- targetHost,
520- startTime,
521- statusCode : 502 ,
522- clientMessage : 'Proxy error' ,
523- extraMetrics : ( duration ) => {
524- metrics . increment ( 'requests_total' , { provider, method : req . method , status_class : '5xx' } ) ;
525- metrics . observe ( 'request_duration_ms' , duration , { provider } ) ;
526- } ,
527- } ) ;
528- } ) ;
650+ if ( body . length > 0 ) proxyReq . write ( body ) ;
651+ proxyReq . end ( ) ;
652+ } ;
529653
530- if ( body . length > 0 ) proxyReq . write ( body ) ;
531- proxyReq . end ( ) ;
654+ sendUpstreamRequest ( headers ) ;
532655 } ) ;
533656}
534657
@@ -546,6 +669,7 @@ module.exports = {
546669 resetEffectiveTokenGuardForTests,
547670 resetMaxRunsGuardForTests,
548671 resetTimeoutSteeringForTests,
672+ resetAnthropicDeprecatedBetaHeadersForTests : ( ) => anthropicDeprecatedBetaValues . clear ( ) ,
549673 getAndClearPendingSteeringMessage,
550674 getAndClearPendingTimeoutSteeringMessage,
551675 injectSteeringMessage,
0 commit comments