@@ -141,6 +141,12 @@ const ALLOWED_MODELS_PATHS = new Set([
141141 URL_PATHS . MODELS ,
142142 `/v1${ URL_PATHS . MODELS } ` ,
143143] ) ;
144+ const ALLOWED_THREAD_GOAL_PATHS = new Set ( [
145+ "/thread/goal/get" ,
146+ "/thread/goal/set" ,
147+ "/codex/thread/goal/get" ,
148+ "/codex/thread/goal/set" ,
149+ ] ) ;
144150
145151function isResponsesPath ( pathname : string ) : boolean {
146152 return ALLOWED_RESPONSES_PATHS . has ( pathname ) ;
@@ -150,6 +156,14 @@ function isModelsPath(pathname: string): boolean {
150156 return ALLOWED_MODELS_PATHS . has ( pathname ) ;
151157}
152158
159+ function isThreadGoalPath ( pathname : string ) : boolean {
160+ return ALLOWED_THREAD_GOAL_PATHS . has ( pathname ) ;
161+ }
162+
163+ function normalizeThreadGoalUpstreamPath ( pathname : string ) : string {
164+ return pathname . startsWith ( "/codex/" ) ? pathname : `/codex${ pathname } ` ;
165+ }
166+
153167function headersFromIncoming ( req : IncomingMessage ) : Headers {
154168 const headers = new Headers ( ) ;
155169 for ( const [ key , value ] of Object . entries ( req . headers ) ) {
@@ -383,6 +397,30 @@ function buildModelsRequestContext(req: IncomingMessage): RequestContext {
383397 } ;
384398}
385399
400+ function buildThreadGoalRequestContext (
401+ req : IncomingMessage ,
402+ body : Buffer ,
403+ pathname : string ,
404+ ) : RequestContext {
405+ const headers = headersFromIncoming ( req ) ;
406+ const parsedBody = parseRequestBody ( body ) ;
407+ const sessionKey = parsedBody
408+ ? ( readStringRecordValue ( parsedBody , "thread_id" ) ??
409+ readStringRecordValue ( parsedBody , "threadId" ) ??
410+ resolveSessionKey ( headers , parsedBody ) )
411+ : resolveSessionKey ( headers , null ) ;
412+ return {
413+ body,
414+ headers,
415+ method : req . method === "GET" ? "GET" : "POST" ,
416+ upstreamPath : normalizeThreadGoalUpstreamPath ( pathname ) ,
417+ model : null ,
418+ family : "codex" ,
419+ stream : false ,
420+ sessionKey,
421+ } ;
422+ }
423+
386424function buildUpstreamUrl (
387425 req : IncomingMessage ,
388426 upstreamBaseUrl : string ,
@@ -668,7 +706,7 @@ function writeMethodOrPathError(res: ServerResponse): void {
668706 writeJson ( res , 404 , {
669707 error : {
670708 message :
671- "Runtime rotation proxy only accepts Responses API and model discovery requests." ,
709+ "Runtime rotation proxy only accepts Responses API, model discovery, and Codex thread goal requests." ,
672710 code : "runtime_rotation_proxy_not_found" ,
673711 } ,
674712 } ) ;
@@ -832,6 +870,7 @@ export async function startRuntimeRotationProxy(
832870 lastAccountId : null ,
833871 lastAccountUpdatedAt : null ,
834872 } ;
873+ const threadGoalFallbacks = new Map < string , string | null > ( ) ;
835874
836875 const handleRequest = async (
837876 req : IncomingMessage ,
@@ -844,7 +883,10 @@ export async function startRuntimeRotationProxy(
844883 req . method === "POST" && isResponsesPath ( incomingUrl . pathname ) ;
845884 const isModelsRequest =
846885 req . method === "GET" && isModelsPath ( incomingUrl . pathname ) ;
847- if ( ! isResponsesRequest && ! isModelsRequest ) {
886+ const isThreadGoalRequest =
887+ ( req . method === "GET" || req . method === "POST" ) &&
888+ isThreadGoalPath ( incomingUrl . pathname ) ;
889+ if ( ! isResponsesRequest && ! isModelsRequest && ! isThreadGoalRequest ) {
848890 writeMethodOrPathError ( res ) ;
849891 return ;
850892 }
@@ -856,12 +898,15 @@ export async function startRuntimeRotationProxy(
856898 }
857899
858900 status . totalRequests += 1 ;
901+ const requestBody =
902+ isResponsesRequest || ( isThreadGoalRequest && req . method === "POST" )
903+ ? await readRequestBody ( req , maxRequestBodyBytes )
904+ : Buffer . alloc ( 0 ) ;
859905 const context = isModelsRequest
860906 ? buildModelsRequestContext ( req )
861- : buildResponsesRequestContext (
862- req ,
863- await readRequestBody ( req , maxRequestBodyBytes ) ,
864- ) ;
907+ : isThreadGoalRequest
908+ ? buildThreadGoalRequestContext ( req , requestBody , incomingUrl . pathname )
909+ : buildResponsesRequestContext ( req , requestBody ) ;
865910 const requestStartedAt = now ( ) ;
866911 let policyDecision : RuntimePolicyDecision | null = null ;
867912 let projectKey : string | null = null ;
@@ -881,7 +926,11 @@ export async function startRuntimeRotationProxy(
881926 }
882927 usageRecorder = createRuntimeUsageRecorder ( {
883928 source : "runtime-proxy" ,
884- operation : isModelsRequest ? "models" : "responses" ,
929+ operation : isModelsRequest
930+ ? "models"
931+ : isThreadGoalRequest
932+ ? "diagnostic"
933+ : "responses" ,
885934 model : context . model ,
886935 projectKey,
887936 requestId : context . sessionKey ,
@@ -1078,6 +1127,34 @@ export async function startRuntimeRotationProxy(
10781127 continue ;
10791128 }
10801129
1130+ if ( isThreadGoalRequest && upstream . status >= 400 ) {
1131+ await readErrorBody ( upstream ) ;
1132+ const parsedGoalBody = parseRequestBody ( context . body ) ;
1133+ const fallbackKey = context . sessionKey ?? "default" ;
1134+ const goal =
1135+ typeof parsedGoalBody ?. goal === "string" ? parsedGoalBody . goal : null ;
1136+ accountManager . recordSuccess ( refreshed . account , context . family , context . model ) ;
1137+ await persistRuntimeActiveAccount (
1138+ accountManager ,
1139+ refreshed . account ,
1140+ context . family ,
1141+ ) ;
1142+ await usageRecorder . record ( {
1143+ outcome : "success" ,
1144+ statusCode : HTTP_STATUS . OK ,
1145+ account : refreshed . account ,
1146+ } ) ;
1147+ if ( context . upstreamPath . endsWith ( "/set" ) ) {
1148+ threadGoalFallbacks . set ( fallbackKey , goal ) ;
1149+ writeJson ( res , HTTP_STATUS . OK , { ok : true , goal } ) ;
1150+ return ;
1151+ }
1152+ writeJson ( res , HTTP_STATUS . OK , {
1153+ goal : threadGoalFallbacks . get ( fallbackKey ) ?? null ,
1154+ } ) ;
1155+ return ;
1156+ }
1157+
10811158 accountManager . recordSuccess ( refreshed . account , context . family , context . model ) ;
10821159 const nearExhaustionWaitMs = getQuotaNearExhaustionWaitMs (
10831160 upstream . headers ,
0 commit comments