@@ -1875,6 +1875,97 @@ program
18751875 console . log ( '' ) ;
18761876 } ) ;
18771877
1878+ // =============================================================================
1879+ // Tiny Dancer - cost-optimal FastGRNN model router (train + route)
1880+ // =============================================================================
1881+
1882+ const tinyDancer = program
1883+ . command ( 'tiny-dancer' )
1884+ . alias ( 'td' )
1885+ . description ( 'Cost-optimal FastGRNN model router — train from a DRACO dataset and route with it (requires @ruvector/tiny-dancer)' ) ;
1886+
1887+ function loadTinyDancer ( ) {
1888+ try {
1889+ return require ( '@ruvector/tiny-dancer' ) ;
1890+ } catch ( e ) {
1891+ console . error ( chalk . red ( '\n This command requires @ruvector/tiny-dancer' ) ) ;
1892+ console . error ( chalk . yellow ( ' Install it: npm install @ruvector/tiny-dancer' ) ) ;
1893+ console . error ( chalk . dim ( ' (native router; ships for linux/macos/windows incl. musl + arm64)\n' ) ) ;
1894+ process . exit ( 1 ) ;
1895+ }
1896+ }
1897+
1898+ tinyDancer
1899+ . command ( 'train <draco>' )
1900+ . description ( 'Train a FastGRNN router from a DRACO dataset (rows of {embedding, scores}) into a .safetensors model' )
1901+ . requiredOption ( '--out <path>' , 'Output .safetensors model path' )
1902+ . option ( '--input-dim <n>' , 'Embedding/feature dimension (default: inferred from the first row)' )
1903+ . option ( '--prices <json>' , 'Price table as JSON or @file, e.g. \'{"haiku":1,"opus":15}\'' )
1904+ . option ( '--epochs <n>' , 'Training epochs' , '40' )
1905+ . option ( '--lr <n>' , 'Learning rate' , '0.05' )
1906+ . option ( '--hidden <n>' , 'Hidden dimension' , '12' )
1907+ . option ( '--tolerance <n>' , 'Cheap-model "good enough" tolerance' , '0.05' )
1908+ . action ( async ( draco , options ) => {
1909+ const td = loadTinyDancer ( ) ;
1910+ const parsed = JSON . parse ( fs . readFileSync ( draco , 'utf8' ) ) ;
1911+ const rows = Array . isArray ( parsed ) ? parsed : parsed . rows ;
1912+ const prices = options . prices
1913+ ? JSON . parse ( options . prices . startsWith ( '@' ) ? fs . readFileSync ( options . prices . slice ( 1 ) , 'utf8' ) : options . prices )
1914+ : ( parsed . prices || { } ) ;
1915+ if ( ! Array . isArray ( rows ) || rows . length === 0 ) {
1916+ console . error ( chalk . red ( ' DRACO file must contain rows of { embedding, scores }' ) ) ; process . exit ( 1 ) ;
1917+ }
1918+ if ( ! prices || Object . keys ( prices ) . length === 0 ) {
1919+ console . error ( chalk . red ( ' Provide a price table via --prices or a "prices" field in the file' ) ) ; process . exit ( 1 ) ;
1920+ }
1921+ const inputDim = options . inputDim ? parseInt ( options . inputDim , 10 ) : ( rows [ 0 ] . embedding || [ ] ) . length ;
1922+ console . log ( chalk . cyan ( `\n Training FastGRNN router: ${ rows . length } rows, dim ${ inputDim } ` ) ) ;
1923+ const res = await td . trainRouter ( rows , prices , {
1924+ outputPath : options . out ,
1925+ inputDim,
1926+ hiddenDim : parseInt ( options . hidden , 10 ) ,
1927+ epochs : parseInt ( options . epochs , 10 ) ,
1928+ learningRate : parseFloat ( options . lr ) ,
1929+ tolerance : parseFloat ( options . tolerance ) ,
1930+ } ) ;
1931+ console . log ( chalk . green ( ` ✓ trained: acc=${ res . trainAccuracy . toFixed ( 3 ) } val=${ res . valAccuracy . toFixed ( 3 ) } loss=${ res . trainLoss . toFixed ( 4 ) } ` ) ) ;
1932+ console . log ( chalk . white ( ` ✓ saved: ${ res . modelPath } (${ res . modelBytes } bytes, ${ res . epochsRun } epochs)` ) ) ;
1933+ console . log ( chalk . gray ( ` Load it: new Router({ modelPath: '${ res . modelPath } ' })\n` ) ) ;
1934+ } ) ;
1935+
1936+ tinyDancer
1937+ . command ( 'route <model>' )
1938+ . description ( 'Route a query through a trained model. --query: JSON embedding array; --candidates: JSON file of [{id, embedding}]' )
1939+ . requiredOption ( '--query <json>' , 'Query embedding as a JSON array or @file' )
1940+ . requiredOption ( '--candidates <file>' , 'Candidates JSON file: [{ id, embedding }]' )
1941+ . option ( '--threshold <n>' , 'Confidence threshold' , '0.85' )
1942+ . action ( async ( model , options ) => {
1943+ const td = loadTinyDancer ( ) ;
1944+ const queryEmbedding = JSON . parse ( options . query . startsWith ( '@' ) ? fs . readFileSync ( options . query . slice ( 1 ) , 'utf8' ) : options . query ) ;
1945+ const candidates = JSON . parse ( fs . readFileSync ( options . candidates , 'utf8' ) ) ;
1946+ const router = new td . Router ( { modelPath : model , confidenceThreshold : parseFloat ( options . threshold ) } ) ;
1947+ const resp = await router . route ( { queryEmbedding, candidates } ) ;
1948+ console . log ( chalk . cyan ( '\n Routing decisions (best first):' ) ) ;
1949+ for ( const d of resp . decisions ) {
1950+ console . log ( ` ${ chalk . white ( d . candidateId ) } conf=${ d . confidence . toFixed ( 3 ) } light=${ d . useLightweight } unc=${ d . uncertainty . toFixed ( 3 ) } ` ) ;
1951+ }
1952+ console . log ( chalk . gray ( ` inference ${ resp . inferenceTimeUs } µs over ${ resp . candidatesProcessed } candidates\n` ) ) ;
1953+ } ) ;
1954+
1955+ tinyDancer
1956+ . command ( 'info' )
1957+ . description ( 'Show tiny-dancer availability and version' )
1958+ . action ( ( ) => {
1959+ try {
1960+ const td = require ( '@ruvector/tiny-dancer' ) ;
1961+ console . log ( chalk . green ( `\n @ruvector/tiny-dancer ${ td . version ( ) } — ${ td . hello ( ) } ` ) ) ;
1962+ console . log ( chalk . gray ( ' train: npx ruvector tiny-dancer train <draco.json> --out model.safetensors' ) ) ;
1963+ console . log ( chalk . gray ( ' route: npx ruvector tiny-dancer route <model.safetensors> --query <emb> --candidates <file>\n' ) ) ;
1964+ } catch {
1965+ console . log ( chalk . yellow ( '\n @ruvector/tiny-dancer not installed. npm install @ruvector/tiny-dancer\n' ) ) ;
1966+ }
1967+ } ) ;
1968+
18781969// =============================================================================
18791970// Server Commands - HTTP/gRPC server
18801971// =============================================================================
0 commit comments