latency-proxy.mjs 4.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. import http from 'node:http';
  2. import { appendFileSync,writeFileSync } from 'node:fs';
  3. const directory='/tmp/bot-latency-acceptance';
  4. async function seed(url){
  5. const login=await fetch(url+'/auth/password-login',{method:'POST',headers:{'content-type':'application/json',origin:url},body:JSON.stringify({username:'fixture',password:'fixture-pass'})});
  6. let cookie=login.headers.getSetCookie().map(v=>v.split(';')[0]).join('; ');
  7. const response=await fetch(url+'/api/app/bootstrap',{headers:{cookie}}),bootstrap=await response.json();
  8. cookie+='; '+response.headers.getSetCookie().map(v=>v.split(';')[0]).join('; ');
  9. const headers={cookie,origin:url,'content-type':'application/json','X-CSRF-Token':bootstrap.csrfToken};
  10. const created=await fetch(url+'/api/app/agents',{method:'POST',headers,body:JSON.stringify({name:'LatencyBot',profile:'default'})});
  11. if(!created.ok&&created.status!==409)throw new Error(await created.text());
  12. const {conversations}=await(await fetch(url+'/api/app/conversations',{headers})).json();
  13. return {conversationId:conversations.find(v=>v.name==='LatencyBot').id};
  14. }
  15. const baseline=await seed('http://127.0.0.1:19351'),optimized=await seed('http://127.0.0.1:19361');
  16. let delay=160,run='setup',id=0;
  17. const log=value=>appendFileSync(directory+'/evidence/network.jsonl',JSON.stringify({time:Date.now()/1000,run,...value})+'\n');
  18. function proxy(mode,port,targetPort){
  19. const target=new URL('http://127.0.0.1:'+targetPort);
  20. http.createServer((req,res)=>{
  21. const requestId=++id,half=delay/2,start=Date.now();const parts=[];
  22. req.on('data',p=>parts.push(p));req.on('end',()=>{
  23. log({mode,id:requestId,event:'request',method:req.method,path:req.url});
  24. setTimeout(()=>{
  25. const headers={...req.headers,host:target.host};if(headers.origin)headers.origin=target.origin;
  26. const up=http.request(new URL(req.url,target),{method:req.method,headers},response=>{
  27. const streaming=String(response.headers['content-type']??'').includes('text/event-stream');let buffer='',bytes=0;const lengths=new Map();
  28. setTimeout(()=>{if(!res.destroyed){res.writeHead(response.statusCode,response.headers);res.flushHeaders()}},half);
  29. response.on('data',chunk=>{
  30. bytes+=chunk.length;
  31. if(streaming){
  32. log({mode,id:requestId,event:'sse_bytes',bytes:chunk.length});buffer+=chunk.toString();let cut;
  33. while((cut=buffer.indexOf('\n\n'))>=0){
  34. const frame=buffer.slice(0,cut);buffer=buffer.slice(cut+2);
  35. const line=frame.split('\n').find(x=>x.startsWith('data:'));
  36. if(line)try{
  37. const v=JSON.parse(line.slice(5));const entry={mode,id:requestId,event:'sse',type:v.type??'ready',seq:v.seq};
  38. if(v.type==='message.changed'&&v.data?.role==='assistant'){
  39. lengths.set(v.data.id,v.data.content.length);Object.assign(entry,{chars:v.data.content.length,status:v.data.status,messageId:v.data.id});
  40. }else if(v.type==='message.patch'){
  41. const n=(lengths.get(v.data.id)??0)+v.data.contentAppend.length;lengths.set(v.data.id,n);Object.assign(entry,{chars:n,status:'streaming',messageId:v.data.id});
  42. }log(entry);
  43. }catch{}
  44. }
  45. }
  46. setTimeout(()=>{if(!res.destroyed)res.write(chunk)},half);
  47. });
  48. response.on('end',()=>setTimeout(()=>{if(!res.destroyed)res.end();log({mode,id:requestId,event:'response_end',path:req.url,status:response.statusCode,bytes,elapsedMs:Date.now()-start})},half));
  49. });up.on('error',e=>{if(!res.headersSent)res.writeHead(502);res.end();log({mode,id:requestId,event:'error',message:e.message})});res.on('close',()=>up.destroy());up.end(Buffer.concat(parts));
  50. },half);
  51. });
  52. }).listen(port,'127.0.0.1');
  53. }
  54. proxy('baseline',19371,19351);proxy('optimized',19372,19361);
  55. const config={baseline:{url:'http://127.0.0.1:19371',...baseline},optimized:{url:'http://127.0.0.1:19372',...optimized}};
  56. writeFileSync(directory+'/connections.json',JSON.stringify(config,null,2));
  57. http.createServer((req,res)=>{
  58. const url=new URL(req.url,'http://127.0.0.1:19359');res.setHeader('content-type','application/json');
  59. if(url.pathname==='/scenario'){delay=Number(url.searchParams.get('delay')??160);run=url.searchParams.get('run')??'setup';log({event:'scenario',delay});res.end('{"ok":true}');return;}
  60. if(url.pathname==='/clock'){res.end(JSON.stringify({time:Date.now()/1000}));return;}
  61. res.end(JSON.stringify(config));
  62. }).listen(19359,'127.0.0.1');console.log('Isolated latency proxy ready');