@@ -1934,6 +1934,181 @@ describe("api", () => {
19341934 }
19351935 } ) ;
19361936
1937+ test ( "/v1/chat/completions hybrid prefers keyed provider over credits-backed provider for gemini-2.5-flash-lite" , async ( ) => {
1938+ await harness . setProjectMode ( "hybrid" ) ;
1939+ await harness . setRoutingMetrics (
1940+ "gemini-2.5-flash-lite" ,
1941+ "google-ai-studio" ,
1942+ {
1943+ uptime : 90 ,
1944+ latency : 1200 ,
1945+ throughput : 5 ,
1946+ } ,
1947+ ) ;
1948+ await harness . setRoutingMetrics ( "gemini-2.5-flash-lite" , "google-vertex" , {
1949+ uptime : 100 ,
1950+ latency : 10 ,
1951+ throughput : 500 ,
1952+ } ) ;
1953+
1954+ await db . insert ( tables . apiKey ) . values ( {
1955+ id : "token-id" ,
1956+ token : "real-token" ,
1957+ projectId : "project-id" ,
1958+ description : "Test API Key" ,
1959+ createdBy : "user-id" ,
1960+ } ) ;
1961+
1962+ await db . insert ( tables . providerKey ) . values ( {
1963+ id : "provider-key-id" ,
1964+ token : "studio-db-key" ,
1965+ provider : "google-ai-studio" ,
1966+ organizationId : "org-id" ,
1967+ } ) ;
1968+
1969+ const previousVertexKey = process . env . LLM_GOOGLE_VERTEX_API_KEY ;
1970+ const previousGoogleCloudProject = process . env . LLM_GOOGLE_CLOUD_PROJECT ;
1971+ const originalFetch = globalThis . fetch ;
1972+ let selectedProvider : "google-ai-studio" | "google-vertex" | undefined ;
1973+ const fetchSpy = vi
1974+ . spyOn ( globalThis , "fetch" )
1975+ . mockImplementation ( async ( input , init ) => {
1976+ const url =
1977+ typeof input === "string"
1978+ ? input
1979+ : input instanceof URL
1980+ ? input . toString ( )
1981+ : input . url ;
1982+
1983+ if (
1984+ url . startsWith (
1985+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent" ,
1986+ )
1987+ ) {
1988+ selectedProvider = "google-ai-studio" ;
1989+ expect ( new URL ( url ) . searchParams . get ( "key" ) ) . toBe ( "studio-db-key" ) ;
1990+
1991+ const body = JSON . parse ( String ( init ?. body ?? "{}" ) ) ;
1992+ const userMessage =
1993+ body . contents ?. find ?.( ( content : { role ?: string } ) => {
1994+ return content . role === "user" ;
1995+ } ) ?. parts ?. [ 0 ] ?. text ?? "" ;
1996+
1997+ return new Response (
1998+ JSON . stringify ( {
1999+ candidates : [
2000+ {
2001+ content : {
2002+ parts : [
2003+ {
2004+ text : `studio response: ${ userMessage } ` ,
2005+ } ,
2006+ ] ,
2007+ role : "model" ,
2008+ } ,
2009+ finishReason : "STOP" ,
2010+ index : 0 ,
2011+ } ,
2012+ ] ,
2013+ usageMetadata : {
2014+ promptTokenCount : 10 ,
2015+ candidatesTokenCount : 20 ,
2016+ totalTokenCount : 30 ,
2017+ } ,
2018+ } ) ,
2019+ {
2020+ status : 200 ,
2021+ headers : {
2022+ "Content-Type" : "application/json" ,
2023+ } ,
2024+ } ,
2025+ ) ;
2026+ }
2027+
2028+ if (
2029+ url . startsWith (
2030+ "https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/publishers/google/models/gemini-2.5-flash-lite:generateContent" ,
2031+ )
2032+ ) {
2033+ selectedProvider = "google-vertex" ;
2034+ expect ( new URL ( url ) . searchParams . get ( "key" ) ) . toBe ( "vertex-env-key" ) ;
2035+
2036+ return new Response (
2037+ JSON . stringify ( {
2038+ candidates : [
2039+ {
2040+ content : {
2041+ parts : [
2042+ {
2043+ text : "vertex response" ,
2044+ } ,
2045+ ] ,
2046+ role : "model" ,
2047+ } ,
2048+ finishReason : "STOP" ,
2049+ index : 0 ,
2050+ } ,
2051+ ] ,
2052+ usageMetadata : {
2053+ promptTokenCount : 10 ,
2054+ candidatesTokenCount : 20 ,
2055+ totalTokenCount : 30 ,
2056+ } ,
2057+ } ) ,
2058+ {
2059+ status : 200 ,
2060+ headers : {
2061+ "Content-Type" : "application/json" ,
2062+ } ,
2063+ } ,
2064+ ) ;
2065+ }
2066+
2067+ return await originalFetch ( input as RequestInfo | URL , init ) ;
2068+ } ) ;
2069+
2070+ try {
2071+ process . env . LLM_GOOGLE_VERTEX_API_KEY = "vertex-env-key" ;
2072+ process . env . LLM_GOOGLE_CLOUD_PROJECT = "vertex-project" ;
2073+
2074+ const res = await app . request ( "/v1/chat/completions" , {
2075+ method : "POST" ,
2076+ headers : {
2077+ "Content-Type" : "application/json" ,
2078+ Authorization : "Bearer real-token" ,
2079+ } ,
2080+ body : JSON . stringify ( {
2081+ model : "gemini-2.5-flash-lite" ,
2082+ messages : [
2083+ {
2084+ role : "user" ,
2085+ content : "Hello from hybrid provider routing!" ,
2086+ } ,
2087+ ] ,
2088+ } ) ,
2089+ } ) ;
2090+
2091+ expect ( res . status ) . toBe ( 200 ) ;
2092+ expect ( selectedProvider ) . toBe ( "google-ai-studio" ) ;
2093+
2094+ const json = await res . json ( ) ;
2095+ expect ( json . metadata . used_provider ) . toBe ( "google-ai-studio" ) ;
2096+ expect ( json . choices [ 0 ] . message . content ) . toContain ( "studio response" ) ;
2097+ } finally {
2098+ fetchSpy . mockRestore ( ) ;
2099+ if ( previousVertexKey === undefined ) {
2100+ delete process . env . LLM_GOOGLE_VERTEX_API_KEY ;
2101+ } else {
2102+ process . env . LLM_GOOGLE_VERTEX_API_KEY = previousVertexKey ;
2103+ }
2104+ if ( previousGoogleCloudProject === undefined ) {
2105+ delete process . env . LLM_GOOGLE_CLOUD_PROJECT ;
2106+ } else {
2107+ process . env . LLM_GOOGLE_CLOUD_PROJECT = previousGoogleCloudProject ;
2108+ }
2109+ }
2110+ } ) ;
2111+
19372112 // test for model with multiple providers (llama-3.3-70b-instruct)
19382113 test . skip ( "/v1/chat/completions with model that has multiple providers" , async ( ) => {
19392114 await db . insert ( tables . apiKey ) . values ( {
0 commit comments