@@ -5,6 +5,8 @@ import { tmpdir } from "node:os";
55import { dirname , join } from "node:path" ;
66import { fileURLToPath } from "node:url" ;
77import { INTERNAL_DEADLINE_MS , SPAWN_BUDGET_MS } from "./helpers/test-budget" ;
8+ import { isModelTextOnly } from "../src/vision" ;
9+ import type { OcxProviderConfig } from "../src/types" ;
810
911const repoRoot = dirname ( fileURLToPath ( new URL ( "../package.json" , import . meta. url ) ) ) ;
1012const cliPath = join ( repoRoot , "src" , "cli" , "index.ts" ) ;
@@ -165,6 +167,100 @@ describe("ocx models richer metadata", () => {
165167 }
166168 } ) ;
167169
170+ test ( "a family entry classifies its tagged siblings, as the runtime does" , ( ) => {
171+ // isModelTextOnly matches noVisionModels with modelInList and reads
172+ // modelInputModalities with modelRecordValue, so a `gpt-oss` entry covers
173+ // `gpt-oss:120b`. This command must not report a different answer.
174+ const dir = mkdtempSync ( join ( tmpdir ( ) , "ocx-models-family-" ) ) ;
175+ const provider = {
176+ adapter : "openai-chat" ,
177+ baseUrl : "http://localhost:8080/v1" ,
178+ allowPrivateNetwork : true ,
179+ defaultModel : "gpt-oss:120b" ,
180+ models : [ "gpt-oss:120b" ] ,
181+ modelContextWindows : { "gpt-oss" : 131000 } ,
182+ noVisionModels : [ "gpt-oss" ] ,
183+ modelReasoningEfforts : { "gpt-oss" : [ "low" , "high" ] } ,
184+ } ;
185+ writeFileSync (
186+ join ( dir , "config.json" ) ,
187+ JSON . stringify ( { port : 10121 , providers : { test : provider } , defaultProvider : "test" } ) ,
188+ "utf8" ,
189+ ) ;
190+ try {
191+ // Ground truth first: what the proxy itself will do with this config.
192+ expect ( isModelTextOnly ( provider as unknown as OcxProviderConfig , "gpt-oss:120b" ) ) . toBe ( true ) ;
193+
194+ const result = runCli ( [ "models" , "--json" ] , { OPENCODEX_HOME : dir } ) ;
195+ expect ( result . status ) . toBe ( 0 ) ;
196+ const row = JSON . parse ( result . stdout ) . models
197+ . find ( ( m : { model : string } ) => m . model === "gpt-oss:120b" ) ;
198+ expect ( row . inputModalities ) . toEqual ( [ "text" ] ) ;
199+ expect ( row . contextWindow ) . toBe ( 131000 ) ;
200+ expect ( row . reasoningEfforts ) . toEqual ( [ "low" , "high" ] ) ;
201+ } finally {
202+ rmSync ( dir , { recursive : true , force : true } ) ;
203+ }
204+ } ) ;
205+
206+ test ( "a noVision family entry beats an exact modality entry, as the runtime does" , ( ) => {
207+ // isModelTextOnly returns true on the noVisionModels match before it ever reads
208+ // modelInputModalities, so an exact entry listing "image" does not grant vision.
209+ // Reporting ["text", "image"] here would advertise support the proxy then rejects.
210+ const dir = mkdtempSync ( join ( tmpdir ( ) , "ocx-models-novision-" ) ) ;
211+ const provider = {
212+ adapter : "openai-chat" ,
213+ baseUrl : "http://localhost:8080/v1" ,
214+ allowPrivateNetwork : true ,
215+ defaultModel : "gpt-oss:120b" ,
216+ models : [ "gpt-oss:120b" ] ,
217+ noVisionModels : [ "gpt-oss" ] ,
218+ modelInputModalities : { "gpt-oss:120b" : [ "text" , "image" ] } ,
219+ } ;
220+ writeFileSync (
221+ join ( dir , "config.json" ) ,
222+ JSON . stringify ( { port : 10123 , providers : { test : provider } , defaultProvider : "test" } ) ,
223+ "utf8" ,
224+ ) ;
225+ try {
226+ // Ground truth first: the proxy treats this model as text-only.
227+ expect ( isModelTextOnly ( provider as unknown as OcxProviderConfig , "gpt-oss:120b" ) ) . toBe ( true ) ;
228+
229+ const result = runCli ( [ "models" , "--json" ] , { OPENCODEX_HOME : dir } ) ;
230+ expect ( result . status ) . toBe ( 0 ) ;
231+ const row = JSON . parse ( result . stdout ) . models
232+ . find ( ( m : { model : string } ) => m . model === "gpt-oss:120b" ) ;
233+ expect ( row . inputModalities ) . toEqual ( [ "text" ] ) ;
234+ } finally {
235+ rmSync ( dir , { recursive : true , force : true } ) ;
236+ }
237+ } ) ;
238+
239+ test ( "an exact entry still wins over the family entry" , ( ) => {
240+ const dir = mkdtempSync ( join ( tmpdir ( ) , "ocx-models-exact-" ) ) ;
241+ const provider = {
242+ adapter : "openai-chat" ,
243+ baseUrl : "http://localhost:8080/v1" ,
244+ allowPrivateNetwork : true ,
245+ defaultModel : "gpt-oss:20b" ,
246+ models : [ "gpt-oss:20b" ] ,
247+ modelContextWindows : { "gpt-oss" : 131000 , "gpt-oss:20b" : 32000 } ,
248+ } ;
249+ writeFileSync (
250+ join ( dir , "config.json" ) ,
251+ JSON . stringify ( { port : 10122 , providers : { test : provider } , defaultProvider : "test" } ) ,
252+ "utf8" ,
253+ ) ;
254+ try {
255+ const result = runCli ( [ "models" , "--json" ] , { OPENCODEX_HOME : dir } ) ;
256+ const row = JSON . parse ( result . stdout ) . models
257+ . find ( ( m : { model : string } ) => m . model === "gpt-oss:20b" ) ;
258+ expect ( row . contextWindow ) . toBe ( 32000 ) ;
259+ } finally {
260+ rmSync ( dir , { recursive : true , force : true } ) ;
261+ }
262+ } ) ;
263+
168264 test ( "models rejects unknown flags" , ( ) => {
169265 const { dir } = freshConfig ( ) ;
170266 try {
0 commit comments