Skip to content

Commit 4e8c3ce

Browse files
committed
fix: prioritize keyed hybrid routes
1 parent 9cc8ccd commit 4e8c3ce

2 files changed

Lines changed: 343 additions & 78 deletions

File tree

apps/gateway/src/api.spec.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)