@@ -252,6 +252,77 @@ describe('ProxyService endpoint failover', () => {
252252 expect ( localCreate ) . toHaveBeenCalledTimes ( 0 ) ;
253253 } ) ;
254254
255+ it ( 'treats claude-sonnet-4-5 as desktop-vision for endpoint ordering' , async ( ) => {
256+ const eventEmitter = { emit : jest . fn ( ) } ;
257+ const llmResilienceService = makeResilience ( eventEmitter ) ;
258+
259+ const configService = {
260+ get : jest . fn ( ( key : string ) => {
261+ const map : Record < string , string > = {
262+ BYTEBOT_LLM_PROXY_URL : 'http://local-proxy:4000' ,
263+ BYTEBOT_LLM_PROXY_ENDPOINTS :
264+ 'http://local-proxy:4000,http://global-proxy:4000' ,
265+ // Desktop-vision ordering is global-first
266+ BYTEBOT_LLM_PROXY_DESKTOP_VISION_ENDPOINTS :
267+ 'http://global-proxy:4000,http://local-proxy:4000' ,
268+ BYTEBOT_LLM_PROXY_API_KEY : 'dummy' ,
269+ BYTEBOT_LLM_PROXY_ENDPOINT_PREFLIGHT_ENABLED : 'false' ,
270+ } ;
271+ return map [ key ] ?? '' ;
272+ } ) ,
273+ } as any ;
274+
275+ const localCreate = jest . fn ( async ( ) => {
276+ const error = new Error ( 'connect ECONNREFUSED 10.0.0.1:4000' ) ;
277+ ( error as any ) . code = 'ECONNREFUSED' ;
278+ throw error ;
279+ } ) ;
280+ const globalCreate = jest . fn ( async ( ) => {
281+ return {
282+ model : 'claude-sonnet-4-5' ,
283+ choices : [ { message : { content : 'ok' } } ] ,
284+ usage : { prompt_tokens : 1 , completion_tokens : 1 , total_tokens : 2 } ,
285+ } ;
286+ } ) ;
287+
288+ class TestProxyService extends ProxyService {
289+ protected override createOpenAIClient ( baseURL : string ) : any {
290+ if ( baseURL . includes ( 'local-proxy' ) ) {
291+ return { chat : { completions : { create : localCreate } } } ;
292+ }
293+ return { chat : { completions : { create : globalCreate } } } ;
294+ }
295+ }
296+
297+ const service = new TestProxyService (
298+ configService ,
299+ llmResilienceService ,
300+ eventEmitter as any ,
301+ ) ;
302+
303+ const messages = [
304+ {
305+ id : 'm1' ,
306+ createdAt : new Date ( ) ,
307+ updatedAt : new Date ( ) ,
308+ taskId : 't1' ,
309+ summaryId : null ,
310+ role : Role . USER ,
311+ content : [ { type : MessageContentType . Text , text : 'hello' } ] ,
312+ } ,
313+ ] as any ;
314+
315+ await service . generateMessage (
316+ 'system' ,
317+ messages ,
318+ 'claude-sonnet-4-5' ,
319+ { useTools : false } ,
320+ ) ;
321+
322+ expect ( globalCreate ) . toHaveBeenCalledTimes ( 1 ) ;
323+ expect ( localCreate ) . toHaveBeenCalledTimes ( 0 ) ;
324+ } ) ;
325+
255326 it ( 'disables LiteLLM caching for desktop-vision model requests' , async ( ) => {
256327 const eventEmitter = { emit : jest . fn ( ) } ;
257328 const llmResilienceService = makeResilience ( eventEmitter ) ;
@@ -310,6 +381,64 @@ describe('ProxyService endpoint failover', () => {
310381 expect ( create . mock . calls [ 0 ] [ 0 ] . cache ) . toEqual ( { 'no-cache' : true } ) ;
311382 } ) ;
312383
384+ it ( 'disables LiteLLM caching for claude-sonnet-4-5 desktop-vision requests' , async ( ) => {
385+ const eventEmitter = { emit : jest . fn ( ) } ;
386+ const llmResilienceService = makeResilience ( eventEmitter ) ;
387+
388+ const configService = {
389+ get : jest . fn ( ( key : string ) => {
390+ const map : Record < string , string > = {
391+ BYTEBOT_LLM_PROXY_URL : 'http://proxy:4000' ,
392+ BYTEBOT_LLM_PROXY_ENDPOINTS : 'http://proxy:4000' ,
393+ BYTEBOT_LLM_PROXY_DESKTOP_VISION_ENDPOINTS : 'http://proxy:4000' ,
394+ BYTEBOT_LLM_PROXY_API_KEY : 'dummy' ,
395+ BYTEBOT_LLM_PROXY_ENDPOINT_PREFLIGHT_ENABLED : 'false' ,
396+ } ;
397+ return map [ key ] ?? '' ;
398+ } ) ,
399+ } as any ;
400+
401+ const create = jest . fn ( async ( request : any ) => {
402+ return {
403+ model : 'claude-sonnet-4-5' ,
404+ choices : [ { message : { content : 'ok' } } ] ,
405+ usage : { prompt_tokens : 1 , completion_tokens : 1 , total_tokens : 2 } ,
406+ __request : request ,
407+ } ;
408+ } ) ;
409+
410+ class TestProxyService extends ProxyService {
411+ protected override createOpenAIClient ( ) : any {
412+ return { chat : { completions : { create } } } ;
413+ }
414+ }
415+
416+ const service = new TestProxyService (
417+ configService ,
418+ llmResilienceService ,
419+ eventEmitter as any ,
420+ ) ;
421+
422+ const messages = [
423+ {
424+ id : 'm1' ,
425+ createdAt : new Date ( ) ,
426+ updatedAt : new Date ( ) ,
427+ taskId : 't1' ,
428+ summaryId : null ,
429+ role : Role . USER ,
430+ content : [ { type : MessageContentType . Text , text : 'hello' } ] ,
431+ } ,
432+ ] as any ;
433+
434+ await service . generateMessage ( 'system' , messages , 'claude-sonnet-4-5' , {
435+ useTools : false ,
436+ } ) ;
437+
438+ expect ( create ) . toHaveBeenCalledTimes ( 1 ) ;
439+ expect ( create . mock . calls [ 0 ] [ 0 ] . cache ) . toEqual ( { 'no-cache' : true } ) ;
440+ } ) ;
441+
313442 it ( 'does not replay Thinking blocks into Chat Completions history' , async ( ) => {
314443 const eventEmitter = { emit : jest . fn ( ) } ;
315444 const llmResilienceService = makeResilience ( eventEmitter ) ;
0 commit comments