@@ -135,6 +135,13 @@ describe('/api/v1/chat/completions POST endpoint', () => {
135135 status : 'running' ,
136136 }
137137 }
138+ if ( runId === 'run-free' ) {
139+ return {
140+ // Real free-mode allowlisted agent (see FREE_MODE_AGENT_MODELS).
141+ agent_id : 'base2-free' ,
142+ status : 'running' ,
143+ }
144+ }
138145 if ( runId === 'run-completed' ) {
139146 return {
140147 agent_id : 'agent-123' ,
@@ -529,10 +536,10 @@ describe('/api/v1/chat/completions POST endpoint', () => {
529536 method : 'POST' ,
530537 headers : { Authorization : 'Bearer test-api-key-new-free' } ,
531538 body : JSON . stringify ( {
532- model : 'test/test-model ' ,
539+ model : 'z-ai/glm-5.1 ' ,
533540 stream : false ,
534541 codebuff_metadata : {
535- run_id : 'run-123 ' ,
542+ run_id : 'run-free ' ,
536543 client_id : 'test-client-id-123' ,
537544 cost_mode : 'free' ,
538545 } ,
@@ -562,10 +569,10 @@ describe('/api/v1/chat/completions POST endpoint', () => {
562569 method : 'POST' ,
563570 headers : { Authorization : 'Bearer test-api-key-no-credits' } ,
564571 body : JSON . stringify ( {
565- model : 'test/test-model ' ,
572+ model : 'z-ai/glm-5.1 ' ,
566573 stream : false ,
567574 codebuff_metadata : {
568- run_id : 'run-123 ' ,
575+ run_id : 'run-free ' ,
569576 client_id : 'test-client-id-123' ,
570577 cost_mode : 'free' ,
571578 } ,
@@ -587,6 +594,116 @@ describe('/api/v1/chat/completions POST endpoint', () => {
587594
588595 expect ( response . status ) . toBe ( 200 )
589596 } )
597+
598+ it ( 'rejects free-mode requests using a non-allowlisted model (e.g. Opus)' , async ( ) => {
599+ const req = new NextRequest (
600+ 'http://localhost:3000/api/v1/chat/completions' ,
601+ {
602+ method : 'POST' ,
603+ headers : { Authorization : 'Bearer test-api-key-new-free' } ,
604+ body : JSON . stringify ( {
605+ // Expensive model the attacker wants for free.
606+ model : 'anthropic/claude-4.7-opus' ,
607+ stream : true ,
608+ codebuff_metadata : {
609+ run_id : 'run-free' ,
610+ client_id : 'test-client-id-123' ,
611+ cost_mode : 'free' ,
612+ } ,
613+ } ) ,
614+ } ,
615+ )
616+
617+ const response = await postChatCompletions ( {
618+ req,
619+ getUserInfoFromApiKey : mockGetUserInfoFromApiKey ,
620+ logger : mockLogger ,
621+ trackEvent : mockTrackEvent ,
622+ getUserUsageData : mockGetUserUsageData ,
623+ getAgentRunFromId : mockGetAgentRunFromId ,
624+ fetch : mockFetch ,
625+ insertMessageBigquery : mockInsertMessageBigquery ,
626+ loggerWithContext : mockLoggerWithContext ,
627+ } )
628+
629+ expect ( response . status ) . toBe ( 403 )
630+ const body = await response . json ( )
631+ expect ( body . error ) . toBe ( 'free_mode_invalid_agent_model' )
632+ } )
633+
634+ it ( 'rejects free-mode requests with an allowlisted agent but a model outside its allowed set' , async ( ) => {
635+ // agent=base2-free is allowlisted, but Opus is not in its allowed
636+ // model set. This is the spoofing variant of the attack where the
637+ // caller picks a real free-mode agentId to try to sneak past the gate.
638+ const req = new NextRequest (
639+ 'http://localhost:3000/api/v1/chat/completions' ,
640+ {
641+ method : 'POST' ,
642+ headers : { Authorization : 'Bearer test-api-key-new-free' } ,
643+ body : JSON . stringify ( {
644+ model : 'anthropic/claude-4.7-opus' ,
645+ stream : true ,
646+ codebuff_metadata : {
647+ run_id : 'run-free' ,
648+ client_id : 'test-client-id-123' ,
649+ cost_mode : 'free' ,
650+ } ,
651+ } ) ,
652+ } ,
653+ )
654+
655+ const response = await postChatCompletions ( {
656+ req,
657+ getUserInfoFromApiKey : mockGetUserInfoFromApiKey ,
658+ logger : mockLogger ,
659+ trackEvent : mockTrackEvent ,
660+ getUserUsageData : mockGetUserUsageData ,
661+ getAgentRunFromId : mockGetAgentRunFromId ,
662+ fetch : mockFetch ,
663+ insertMessageBigquery : mockInsertMessageBigquery ,
664+ loggerWithContext : mockLoggerWithContext ,
665+ } )
666+
667+ expect ( response . status ) . toBe ( 403 )
668+ const body = await response . json ( )
669+ expect ( body . error ) . toBe ( 'free_mode_invalid_agent_model' )
670+ } )
671+
672+ it ( 'rejects free-mode requests where agentId is not in the allowlist at all' , async ( ) => {
673+ // run-123 points to agent-123, which is not a free-mode agent.
674+ const req = new NextRequest (
675+ 'http://localhost:3000/api/v1/chat/completions' ,
676+ {
677+ method : 'POST' ,
678+ headers : { Authorization : 'Bearer test-api-key-new-free' } ,
679+ body : JSON . stringify ( {
680+ model : 'z-ai/glm-5.1' ,
681+ stream : true ,
682+ codebuff_metadata : {
683+ run_id : 'run-123' ,
684+ client_id : 'test-client-id-123' ,
685+ cost_mode : 'free' ,
686+ } ,
687+ } ) ,
688+ } ,
689+ )
690+
691+ const response = await postChatCompletions ( {
692+ req,
693+ getUserInfoFromApiKey : mockGetUserInfoFromApiKey ,
694+ logger : mockLogger ,
695+ trackEvent : mockTrackEvent ,
696+ getUserUsageData : mockGetUserUsageData ,
697+ getAgentRunFromId : mockGetAgentRunFromId ,
698+ fetch : mockFetch ,
699+ insertMessageBigquery : mockInsertMessageBigquery ,
700+ loggerWithContext : mockLoggerWithContext ,
701+ } )
702+
703+ expect ( response . status ) . toBe ( 403 )
704+ const body = await response . json ( )
705+ expect ( body . error ) . toBe ( 'free_mode_invalid_agent_model' )
706+ } )
590707 } )
591708
592709 describe ( 'Successful responses' , ( ) => {
@@ -734,10 +851,10 @@ describe('/api/v1/chat/completions POST endpoint', () => {
734851 method : 'POST' ,
735852 headers : { Authorization : 'Bearer test-api-key-123' } ,
736853 body : JSON . stringify ( {
737- model : 'test/test-model ' ,
854+ model : 'z-ai/glm-5.1 ' ,
738855 stream : false ,
739856 codebuff_metadata : {
740- run_id : 'run-123 ' ,
857+ run_id : 'run-free ' ,
741858 client_id : 'test-client-id-123' ,
742859 cost_mode : 'free' ,
743860 } ,
0 commit comments