@@ -1491,6 +1491,46 @@ test('ShimToolSearch predict mode includes web tools for current web requests',
14911491 expect ( toolNames ) . toContain ( 'WebFetch' )
14921492} )
14931493
1494+ test ( 'ShimToolSearch predict mode includes forced tool_choice tool for conversational prompts' , async ( ) => {
1495+ let requestBody : Record < string , unknown > | undefined
1496+ process . env . OPENAI_SHIM_TOOL_MODE = 'predict'
1497+
1498+ globalThis . fetch = ( async ( _input , init ) => {
1499+ requestBody = JSON . parse ( String ( init ?. body ) )
1500+
1501+ return new Response (
1502+ JSON . stringify ( {
1503+ id : 'chatcmpl-shim-predict-forced-tool' ,
1504+ model : 'fake-model' ,
1505+ choices : [ { message : { role : 'assistant' , content : 'ok' } , finish_reason : 'stop' } ] ,
1506+ usage : { prompt_tokens : 1 , completion_tokens : 1 , total_tokens : 2 } ,
1507+ } ) ,
1508+ { headers : { 'Content-Type' : 'application/json' } } ,
1509+ )
1510+ } ) as FetchType
1511+
1512+ const client = createOpenAIShimClient ( { } ) as OpenAIShimClient
1513+
1514+ await client . beta . messages . create ( {
1515+ model : 'fake-model' ,
1516+ system : 'test system' ,
1517+ messages : [ { role : 'user' , content : 'What is 2+2?' } ] ,
1518+ tools : makeShimToolFixtures ( ) ,
1519+ tool_choice : { type : 'tool' , name : 'WebSearch' } ,
1520+ max_tokens : 32 ,
1521+ stream : false ,
1522+ } )
1523+
1524+ const toolNames = ( ( requestBody ?. tools ?? [ ] ) as Array < { function : { name : string } } > )
1525+ . map ( tool => tool . function . name )
1526+ expect ( toolNames . length ) . toBeGreaterThan ( 0 )
1527+ expect ( toolNames ) . toContain ( 'WebSearch' )
1528+ expect ( requestBody ?. tool_choice ) . toEqual ( {
1529+ type : 'function' ,
1530+ function : { name : 'WebSearch' } ,
1531+ } )
1532+ } )
1533+
14941534test ( 'ShimToolSearch predict mode reduces tools for responses transport' , async ( ) => {
14951535 let requestBody : Record < string , unknown > | undefined
14961536 process . env . OPENAI_API_FORMAT = 'responses'
@@ -1536,6 +1576,212 @@ test('ShimToolSearch predict mode reduces tools for responses transport', async
15361576 expect ( webSearchParameters ?. properties ?. query ?. description ) . toBeUndefined ( )
15371577} )
15381578
1579+ test ( 'ShimToolSearch predict mode does not re-expand empty predicted tools for responses transport' , async ( ) => {
1580+ let requestBody : Record < string , unknown > | undefined
1581+ process . env . OPENAI_API_FORMAT = 'responses'
1582+ process . env . OPENAI_SHIM_TOOL_MODE = 'predict'
1583+
1584+ globalThis . fetch = ( async ( _input , init ) => {
1585+ requestBody = JSON . parse ( String ( init ?. body ) )
1586+
1587+ return new Response (
1588+ JSON . stringify ( {
1589+ id : 'resp-shim-predict-empty' ,
1590+ model : 'fake-model' ,
1591+ output : [
1592+ {
1593+ type : 'message' ,
1594+ role : 'assistant' ,
1595+ content : [ { type : 'output_text' , text : 'ok' } ] ,
1596+ } ,
1597+ ] ,
1598+ usage : { input_tokens : 1 , output_tokens : 1 , total_tokens : 2 } ,
1599+ } ) ,
1600+ { headers : { 'Content-Type' : 'application/json' } } ,
1601+ )
1602+ } ) as FetchType
1603+
1604+ const client = createOpenAIShimClient ( { } ) as OpenAIShimClient
1605+
1606+ await client . beta . messages . create ( {
1607+ model : 'fake-model' ,
1608+ system : 'test system' ,
1609+ messages : [ { role : 'user' , content : 'What is 2+2?' } ] ,
1610+ tools : makeShimToolFixtures ( ) ,
1611+ max_tokens : 32 ,
1612+ stream : false ,
1613+ } )
1614+
1615+ expect ( requestBody ?. tools ) . toBeUndefined ( )
1616+ expect ( requestBody ?. tool_choice ) . toBeUndefined ( )
1617+ } )
1618+
1619+ test ( 'ShimToolSearch predict mode includes forced tool_choice tool for responses transport' , async ( ) => {
1620+ let requestBody : Record < string , unknown > | undefined
1621+ process . env . OPENAI_API_FORMAT = 'responses'
1622+ process . env . OPENAI_SHIM_TOOL_MODE = 'predict'
1623+
1624+ globalThis . fetch = ( async ( _input , init ) => {
1625+ requestBody = JSON . parse ( String ( init ?. body ) )
1626+
1627+ return new Response (
1628+ JSON . stringify ( {
1629+ id : 'resp-shim-predict-forced-tool' ,
1630+ model : 'fake-model' ,
1631+ output : [
1632+ {
1633+ type : 'message' ,
1634+ role : 'assistant' ,
1635+ content : [ { type : 'output_text' , text : 'ok' } ] ,
1636+ } ,
1637+ ] ,
1638+ usage : { input_tokens : 1 , output_tokens : 1 , total_tokens : 2 } ,
1639+ } ) ,
1640+ { headers : { 'Content-Type' : 'application/json' } } ,
1641+ )
1642+ } ) as FetchType
1643+
1644+ const client = createOpenAIShimClient ( { } ) as OpenAIShimClient
1645+
1646+ await client . beta . messages . create ( {
1647+ model : 'fake-model' ,
1648+ system : 'test system' ,
1649+ messages : [ { role : 'user' , content : 'What is 2+2?' } ] ,
1650+ tools : makeShimToolFixtures ( ) ,
1651+ tool_choice : { type : 'tool' , name : 'WebSearch' } ,
1652+ max_tokens : 32 ,
1653+ stream : false ,
1654+ } )
1655+
1656+ const toolNames = ( ( requestBody ?. tools ?? [ ] ) as Array < { name : string } > )
1657+ . map ( tool => tool . name )
1658+ expect ( toolNames ) . toContain ( 'WebSearch' )
1659+ expect ( requestBody ?. tool_choice ) . toEqual ( {
1660+ type : 'function' ,
1661+ name : 'WebSearch' ,
1662+ } )
1663+ } )
1664+
1665+ test ( 'OpenAI-compatible chat transport drops forced tool_choice when selected tools lack that schema' , async ( ) => {
1666+ let requestBody : Record < string , unknown > | undefined
1667+ process . env . OPENAI_SHIM_TOOL_MODE = 'predict'
1668+
1669+ globalThis . fetch = ( async ( _input , init ) => {
1670+ requestBody = JSON . parse ( String ( init ?. body ) )
1671+
1672+ return new Response (
1673+ JSON . stringify ( {
1674+ id : 'chatcmpl-shim-missing-forced-tool' ,
1675+ model : 'fake-model' ,
1676+ choices : [ { message : { role : 'assistant' , content : 'ok' } , finish_reason : 'stop' } ] ,
1677+ usage : { prompt_tokens : 1 , completion_tokens : 1 , total_tokens : 2 } ,
1678+ } ) ,
1679+ { headers : { 'Content-Type' : 'application/json' } } ,
1680+ )
1681+ } ) as FetchType
1682+
1683+ const client = createOpenAIShimClient ( { } ) as OpenAIShimClient
1684+
1685+ await client . beta . messages . create ( {
1686+ model : 'fake-model' ,
1687+ system : 'test system' ,
1688+ messages : [ { role : 'user' , content : 'What is 2+2?' } ] ,
1689+ tools : makeShimToolFixtures ( ) . filter ( tool => tool . name !== 'WebSearch' ) ,
1690+ tool_choice : { type : 'tool' , name : 'WebSearch' } ,
1691+ max_tokens : 32 ,
1692+ stream : false ,
1693+ } )
1694+
1695+ const toolNames = ( ( requestBody ?. tools ?? [ ] ) as Array < { function : { name : string } } > )
1696+ . map ( tool => tool . function . name )
1697+ expect ( toolNames ) . not . toContain ( 'WebSearch' )
1698+ expect ( requestBody ?. tool_choice ) . toBeUndefined ( )
1699+ } )
1700+
1701+ test ( 'OpenAI-compatible responses transport drops forced tool_choice when selected tools lack that schema' , async ( ) => {
1702+ let requestBody : Record < string , unknown > | undefined
1703+ process . env . OPENAI_API_FORMAT = 'responses'
1704+ process . env . OPENAI_SHIM_TOOL_MODE = 'predict'
1705+
1706+ globalThis . fetch = ( async ( _input , init ) => {
1707+ requestBody = JSON . parse ( String ( init ?. body ) )
1708+
1709+ return new Response (
1710+ JSON . stringify ( {
1711+ id : 'resp-shim-missing-forced-tool' ,
1712+ model : 'fake-model' ,
1713+ output : [
1714+ {
1715+ type : 'message' ,
1716+ role : 'assistant' ,
1717+ content : [ { type : 'output_text' , text : 'ok' } ] ,
1718+ } ,
1719+ ] ,
1720+ usage : { input_tokens : 1 , output_tokens : 1 , total_tokens : 2 } ,
1721+ } ) ,
1722+ { headers : { 'Content-Type' : 'application/json' } } ,
1723+ )
1724+ } ) as FetchType
1725+
1726+ const client = createOpenAIShimClient ( { } ) as OpenAIShimClient
1727+
1728+ await client . beta . messages . create ( {
1729+ model : 'fake-model' ,
1730+ system : 'test system' ,
1731+ messages : [ { role : 'user' , content : 'What is 2+2?' } ] ,
1732+ tools : makeShimToolFixtures ( ) . filter ( tool => tool . name !== 'WebSearch' ) ,
1733+ tool_choice : { type : 'tool' , name : 'WebSearch' } ,
1734+ max_tokens : 32 ,
1735+ stream : false ,
1736+ } )
1737+
1738+ const toolNames = ( ( requestBody ?. tools ?? [ ] ) as Array < { name : string } > )
1739+ . map ( tool => tool . name )
1740+ expect ( toolNames ) . not . toContain ( 'WebSearch' )
1741+ expect ( requestBody ?. tool_choice ) . toBeUndefined ( )
1742+ } )
1743+
1744+ test ( 'OpenAI-compatible responses transport drops scalar any when predict selects no tools' , async ( ) => {
1745+ let requestBody : Record < string , unknown > | undefined
1746+ process . env . OPENAI_API_FORMAT = 'responses'
1747+ process . env . OPENAI_SHIM_TOOL_MODE = 'predict'
1748+
1749+ globalThis . fetch = ( async ( _input , init ) => {
1750+ requestBody = JSON . parse ( String ( init ?. body ) )
1751+
1752+ return new Response (
1753+ JSON . stringify ( {
1754+ id : 'resp-shim-any-empty-tools' ,
1755+ model : 'fake-model' ,
1756+ output : [
1757+ {
1758+ type : 'message' ,
1759+ role : 'assistant' ,
1760+ content : [ { type : 'output_text' , text : 'ok' } ] ,
1761+ } ,
1762+ ] ,
1763+ usage : { input_tokens : 1 , output_tokens : 1 , total_tokens : 2 } ,
1764+ } ) ,
1765+ { headers : { 'Content-Type' : 'application/json' } } ,
1766+ )
1767+ } ) as FetchType
1768+
1769+ const client = createOpenAIShimClient ( { } ) as OpenAIShimClient
1770+
1771+ await client . beta . messages . create ( {
1772+ model : 'fake-model' ,
1773+ system : 'test system' ,
1774+ messages : [ { role : 'user' , content : 'What is 2+2?' } ] ,
1775+ tools : makeShimToolFixtures ( ) ,
1776+ tool_choice : { type : 'any' } ,
1777+ max_tokens : 32 ,
1778+ stream : false ,
1779+ } )
1780+
1781+ expect ( requestBody ?. tools ) . toBeUndefined ( )
1782+ expect ( requestBody ?. tool_choice ) . toBeUndefined ( )
1783+ } )
1784+
15391785test ( 'OpenAI-compatible responses transport preserves tool_choice' , async ( ) => {
15401786 let requestBody : Record < string , unknown > | undefined
15411787 process . env . OPENAI_API_FORMAT = 'responses'
@@ -1674,6 +1920,47 @@ test('ShimToolSearch lazy phase 1 sends a valid request_tools schema through sys
16741920 expect ( result . request_id ) . toMatch ( / ^ m s g _ / )
16751921} )
16761922
1923+ test ( 'ShimToolSearch lazy mode skips phase 1 for forced non-request_tools tool_choice' , async ( ) => {
1924+ const requestBodies : Array < Record < string , unknown > > = [ ]
1925+ process . env . OPENAI_SHIM_TOOL_MODE = 'lazy'
1926+
1927+ globalThis . fetch = ( async ( _input , init ) => {
1928+ requestBodies . push ( JSON . parse ( String ( init ?. body ) ) )
1929+
1930+ return new Response (
1931+ JSON . stringify ( {
1932+ id : 'chatcmpl-shim-lazy-forced-tool' ,
1933+ model : 'fake-model' ,
1934+ choices : [ { message : { role : 'assistant' , content : 'ok' } , finish_reason : 'stop' } ] ,
1935+ usage : { prompt_tokens : 1 , completion_tokens : 1 , total_tokens : 2 } ,
1936+ } ) ,
1937+ { headers : { 'Content-Type' : 'application/json' } } ,
1938+ )
1939+ } ) as FetchType
1940+
1941+ const client = createOpenAIShimClient ( { } ) as OpenAIShimClient
1942+
1943+ await client . beta . messages . create ( {
1944+ model : 'fake-model' ,
1945+ system : 'test system' ,
1946+ messages : [ { role : 'user' , content : 'What is 2+2?' } ] ,
1947+ tools : makeShimToolFixtures ( ) ,
1948+ tool_choice : { type : 'tool' , name : 'WebSearch' } ,
1949+ max_tokens : 32 ,
1950+ stream : false ,
1951+ } )
1952+
1953+ expect ( requestBodies ) . toHaveLength ( 1 )
1954+ const toolNames = ( ( requestBodies [ 0 ] . tools ?? [ ] ) as Array < { function : { name : string } } > )
1955+ . map ( tool => tool . function . name )
1956+ expect ( toolNames ) . toContain ( 'WebSearch' )
1957+ expect ( toolNames ) . not . toContain ( 'request_tools' )
1958+ expect ( requestBodies [ 0 ] . tool_choice ) . toEqual ( {
1959+ type : 'function' ,
1960+ function : { name : 'WebSearch' } ,
1961+ } )
1962+ } )
1963+
16771964test ( 'ShimToolSearch lazy phase 2 falls back to all tools on malformed request_tools JSON' , async ( ) => {
16781965 const requestBodies : Array < Record < string , unknown > > = [ ]
16791966 process . env . OPENAI_SHIM_TOOL_MODE = 'lazy'
0 commit comments