@@ -500,6 +500,17 @@ describe("gateway server chat", () => {
500500 const dispatchRelease = createDeferred < void > ( ) ;
501501 try {
502502 testState . sessionStorePath = path . join ( sessionDir , "sessions.json" ) ;
503+ testState . agentConfig = {
504+ model : { primary : "test-provider/vision-model" } ,
505+ models : {
506+ providers : {
507+ "test-provider" : {
508+ api : "openai-completions" ,
509+ models : [ { id : "vision-model" , name : "Vision Model" } ] ,
510+ } ,
511+ } ,
512+ } ,
513+ } ;
503514 await writeSessionStore ( {
504515 entries : {
505516 main : {
@@ -629,6 +640,7 @@ describe("gateway server chat", () => {
629640 dispatchRelease . resolve ( ) ;
630641 dispatchInboundMessageMock . mockReset ( ) ;
631642 testState . sessionStorePath = undefined ;
643+ testState . agentConfig = undefined ;
632644 clearConfigCache ( ) ;
633645 await fs . rm ( sessionDir , { recursive : true , force : true } ) ;
634646 }
@@ -640,6 +652,17 @@ describe("gateway server chat", () => {
640652 createDeferred < Awaited < ReturnType < GatewayRequestContext [ "loadGatewayModelCatalog" ] > > > ( ) ;
641653 try {
642654 testState . sessionStorePath = path . join ( sessionDir , "sessions.json" ) ;
655+ testState . agentConfig = {
656+ model : { primary : "test-provider/vision-model" } ,
657+ models : {
658+ providers : {
659+ "test-provider" : {
660+ api : "openai-completions" ,
661+ models : [ { id : "vision-model" , name : "Vision Model" } ] ,
662+ } ,
663+ } ,
664+ } ,
665+ } ;
643666 await writeSessionStore ( {
644667 entries : {
645668 main : {
@@ -819,11 +842,211 @@ describe("gateway server chat", () => {
819842 firstCatalog . resolve ( [ ] ) ;
820843 dispatchInboundMessageMock . mockReset ( ) ;
821844 testState . sessionStorePath = undefined ;
845+ testState . agentConfig = undefined ;
822846 clearConfigCache ( ) ;
823847 await fs . rm ( sessionDir , { recursive : true , force : true } ) ;
824848 }
825849 } ) ;
826850
851+ test ( "chat.send stop command bypasses session model catalog validation" , async ( ) => {
852+ const sessionDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-gw-" ) ) ;
853+ try {
854+ testState . sessionStorePath = path . join ( sessionDir , "sessions.json" ) ;
855+ testState . agentConfig = {
856+ model : { primary : "openai/gpt-5.4" } ,
857+ } ;
858+ await writeSessionStore ( {
859+ entries : {
860+ main : {
861+ sessionId : "sess-main" ,
862+ providerOverride : "custom-api-deepseek-com" ,
863+ modelOverride : "deepseek-v4-pro" ,
864+ modelProvider : "custom-api-deepseek-com" ,
865+ model : "deepseek-v4-pro" ,
866+ updatedAt : Date . now ( ) ,
867+ } ,
868+ } ,
869+ } ) ;
870+
871+ const context = {
872+ loadGatewayModelCatalog : vi . fn < GatewayRequestContext [ "loadGatewayModelCatalog" ] > (
873+ async ( ) => {
874+ throw new Error ( "stop command must not load model catalog" ) ;
875+ } ,
876+ ) ,
877+ logGateway : {
878+ info : vi . fn ( ) ,
879+ warn : vi . fn ( ) ,
880+ error : vi . fn ( ) ,
881+ debug : vi . fn ( ) ,
882+ } ,
883+ agentRunSeq : new Map < string , number > ( ) ,
884+ chatAbortControllers : new Map ( ) ,
885+ chatAbortedRuns : new Map ( ) ,
886+ chatRunBuffers : new Map ( ) ,
887+ chatDeltaSentAt : new Map ( ) ,
888+ chatDeltaLastBroadcastLen : new Map ( ) ,
889+ chatDeltaLastBroadcastText : new Map ( ) ,
890+ agentDeltaSentAt : new Map ( ) ,
891+ bufferedAgentEvents : new Map ( ) ,
892+ clearChatRunState : vi . fn ( ) ,
893+ addChatRun : vi . fn ( ) ,
894+ removeChatRun : vi . fn ( ) ,
895+ broadcast : vi . fn ( ) ,
896+ nodeSendToSession : vi . fn ( ) ,
897+ registerToolEventRecipient : vi . fn ( ) ,
898+ dedupe : new Map ( ) ,
899+ } as unknown as GatewayRequestContext ;
900+ const { chatHandlers } = await import ( "./server-methods/chat.js" ) ;
901+ const responses : Array < { ok : boolean ; payload ?: unknown ; error ?: unknown } > = [ ] ;
902+
903+ await chatHandlers [ "chat.send" ] ( {
904+ req : {
905+ type : "req" ,
906+ id : "stop-bypasses-model-catalog" ,
907+ method : "chat.send" ,
908+ params : {
909+ sessionKey : "main" ,
910+ message : "/stop" ,
911+ idempotencyKey : "idem-stop-bypass-model-catalog" ,
912+ } ,
913+ } ,
914+ params : {
915+ sessionKey : "main" ,
916+ message : "/stop" ,
917+ idempotencyKey : "idem-stop-bypass-model-catalog" ,
918+ } ,
919+ client : null ,
920+ isWebchatConnect : ( ) => false ,
921+ respond : ( ( ok , payload , error ) => {
922+ responses . push ( { ok, payload, error } ) ;
923+ } ) as RespondFn ,
924+ context,
925+ } ) ;
926+
927+ expect ( context . loadGatewayModelCatalog ) . not . toHaveBeenCalled ( ) ;
928+ expect ( responses ) . toEqual ( [
929+ {
930+ ok : true ,
931+ payload : { ok : true , aborted : false , runIds : [ ] } ,
932+ error : undefined ,
933+ } ,
934+ ] ) ;
935+ } finally {
936+ testState . sessionStorePath = undefined ;
937+ clearConfigCache ( ) ;
938+ await fs . rm ( sessionDir , { recursive : true , force : true , maxRetries : 5 , retryDelay : 50 } ) ;
939+ }
940+ } ) ;
941+
942+ test ( "chat.send fresh custom-provider session does not load model catalog" , async ( ) => {
943+ const sessionDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-gw-" ) ) ;
944+ try {
945+ testState . sessionStorePath = path . join ( sessionDir , "sessions.json" ) ;
946+ await writeGatewayConfig ( {
947+ agents : {
948+ defaults : {
949+ model : { primary : "custom-api-deepseek-com/deepseek-v4-pro" } ,
950+ } ,
951+ } ,
952+ models : {
953+ providers : {
954+ "custom-api-deepseek-com" : {
955+ baseUrl : "https://deepseek.example.com/v1" ,
956+ models : [ { id : "deepseek-v4-pro" , name : "DeepSeek V4 Pro" } ] ,
957+ } ,
958+ } ,
959+ } ,
960+ } ) ;
961+ await writeSessionStore ( {
962+ entries : {
963+ main : {
964+ sessionId : "sess-main" ,
965+ updatedAt : Date . now ( ) ,
966+ } ,
967+ } ,
968+ } ) ;
969+
970+ const context = {
971+ loadGatewayModelCatalog : vi . fn < GatewayRequestContext [ "loadGatewayModelCatalog" ] > (
972+ async ( ) => {
973+ throw new Error ( "fresh custom-provider session must not load model catalog" ) ;
974+ } ,
975+ ) ,
976+ logGateway : {
977+ info : vi . fn ( ) ,
978+ warn : vi . fn ( ) ,
979+ error : vi . fn ( ) ,
980+ debug : vi . fn ( ) ,
981+ } ,
982+ agentRunSeq : new Map < string , number > ( ) ,
983+ chatAbortControllers : new Map ( ) ,
984+ chatAbortedRuns : new Map ( ) ,
985+ chatRunBuffers : new Map ( ) ,
986+ chatDeltaSentAt : new Map ( ) ,
987+ chatDeltaLastBroadcastLen : new Map ( ) ,
988+ chatDeltaLastBroadcastText : new Map ( ) ,
989+ agentDeltaSentAt : new Map ( ) ,
990+ bufferedAgentEvents : new Map ( ) ,
991+ clearChatRunState : vi . fn ( ) ,
992+ addChatRun : vi . fn ( ) ,
993+ removeChatRun : vi . fn ( ) ,
994+ broadcast : vi . fn ( ) ,
995+ nodeSendToSession : vi . fn ( ) ,
996+ registerToolEventRecipient : vi . fn ( ) ,
997+ dedupe : new Map ( ) ,
998+ } as unknown as GatewayRequestContext ;
999+ dispatchInboundMessageMock . mockResolvedValueOnce ( undefined ) ;
1000+
1001+ const { chatHandlers } = await import ( "./server-methods/chat.js" ) ;
1002+ const responses : Array < { ok : boolean ; payload ?: unknown ; error ?: unknown } > = [ ] ;
1003+ await chatHandlers [ "chat.send" ] ( {
1004+ req : {
1005+ type : "req" ,
1006+ id : "fresh-custom-provider-no-catalog" ,
1007+ method : "chat.send" ,
1008+ params : {
1009+ sessionKey : "main" ,
1010+ message : "hello" ,
1011+ idempotencyKey : "idem-fresh-custom-provider-no-catalog" ,
1012+ } ,
1013+ } ,
1014+ params : {
1015+ sessionKey : "main" ,
1016+ message : "hello" ,
1017+ idempotencyKey : "idem-fresh-custom-provider-no-catalog" ,
1018+ } ,
1019+ client : null ,
1020+ isWebchatConnect : ( ) => false ,
1021+ respond : ( ( ok , payload , error ) => {
1022+ responses . push ( { ok, payload, error } ) ;
1023+ } ) as RespondFn ,
1024+ context,
1025+ } ) ;
1026+
1027+ expect ( context . loadGatewayModelCatalog ) . not . toHaveBeenCalled ( ) ;
1028+ expect ( responses ) . toEqual ( [
1029+ {
1030+ ok : true ,
1031+ payload : expect . objectContaining ( {
1032+ runId : "idem-fresh-custom-provider-no-catalog" ,
1033+ status : "started" ,
1034+ } ) ,
1035+ error : undefined ,
1036+ } ,
1037+ ] ) ;
1038+ await vi . waitFor ( ( ) => expect ( context . removeChatRun ) . toHaveBeenCalledTimes ( 1 ) ) ;
1039+ } finally {
1040+ dispatchInboundMessageMock . mockReset ( ) ;
1041+ testState . sessionStorePath = undefined ;
1042+ clearConfigCache ( ) ;
1043+ if ( process . env . OPENCLAW_CONFIG_PATH ) {
1044+ await fs . rm ( process . env . OPENCLAW_CONFIG_PATH , { force : true } ) ;
1045+ }
1046+ await fs . rm ( sessionDir , { recursive : true , force : true , maxRetries : 5 , retryDelay : 50 } ) ;
1047+ }
1048+ } ) ;
1049+
8271050 test . each ( configuredImageModelCases ) (
8281051 "chat.send preserves text-only image uploads as MediaPaths even with configured imageModel: $id" ,
8291052 async ( { id, imageModel } ) => {
0 commit comments