mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
test(deps): replace retired JavaScript passthrough checks from main
This commit is contained in:
parent
d5c4b5f28a
commit
a5a1d649cb
13 changed files with 155 additions and 4768 deletions
|
|
@ -2309,23 +2309,6 @@ jobs:
|
|||
- wait_for_service:
|
||||
url: http://localhost:4000
|
||||
timeout: "300"
|
||||
# Install Node.js directly from nodejs.org with SHA256 verification,
|
||||
# instead of piping NodeSource's setup_24.x apt-repo installer into
|
||||
# sudo bash (which runs a mutable upstream script unattended).
|
||||
- install_node
|
||||
|
||||
- run:
|
||||
name: Install Node.js test dependencies
|
||||
command: |
|
||||
cd tests/pass_through_tests
|
||||
npm ci
|
||||
|
||||
- run:
|
||||
name: Run Vertex AI, Google AI Studio Node.js tests
|
||||
command: |
|
||||
cd tests/pass_through_tests
|
||||
NODE_OPTIONS=--experimental-vm-modules npx jest . --verbose
|
||||
no_output_timeout: 30m
|
||||
- run:
|
||||
name: Run tests
|
||||
command: |
|
||||
|
|
|
|||
|
|
@ -599,3 +599,52 @@ def test_budget_rejected_call_keeps_budget_normalized_error(gateway: Gateway, tm
|
|||
)
|
||||
)
|
||||
assert len(budget_rows) == 1, budget_rows
|
||||
|
||||
@pytest.mark.covers("other.observability.passthrough.streaming_tags_reach_spend")
|
||||
def test_gemini_streaming_passthrough_request_tags_reach_spend_log(gateway: Gateway, tmp_path: Path) -> None:
|
||||
expected_body: Final = {"contents": [{"role": "user", "parts": [{"text": "hi"}]}]}
|
||||
chunks: Final = (
|
||||
b'data: {"candidates":[{"content":{"parts":[{"text":"hello"}],"role":"model"},"index":0}]}\n\n',
|
||||
b'data: {"candidates":[{"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}\n\n',
|
||||
)
|
||||
|
||||
def respond(request: Request) -> Reply:
|
||||
assert request.method == "POST", request
|
||||
assert request.target.split("?", 1)[0] == "/v1beta/models/claude-nope-9:streamGenerateContent", request
|
||||
assert json.loads(request.body) == expected_body, request
|
||||
return Reply(status=200, chunks=chunks, content_type="text/event-stream")
|
||||
|
||||
config_path: Final = tmp_path / "gemini-stream-tags.yaml"
|
||||
with wire_server(respond) as wire:
|
||||
_gemini_config(config_path, wire.url)
|
||||
with owned_proxy_process(gateway, tmp_path, {}, config=config_path, workers=2) as owned:
|
||||
candidate: Final = owned.gateway
|
||||
with candidate.client.stream(
|
||||
"POST",
|
||||
_GEMINI_STREAM_PATH,
|
||||
params={"alt": "sse"},
|
||||
json=expected_body,
|
||||
headers={**_gemini_headers(candidate), "tags": "gemini-js-sdk,pass-through-endpoint"},
|
||||
) as response:
|
||||
assert response.status_code == 200
|
||||
streamed: Final = response.read()
|
||||
call_id: Final = response.headers["x-litellm-call-id"]
|
||||
assert streamed == b"".join(chunks)
|
||||
|
||||
rows: Final = eventually(
|
||||
lambda: read_rows(
|
||||
'SELECT request_tags, call_type, status FROM "LiteLLM_SpendLogs" WHERE request_id=%s',
|
||||
(call_id,),
|
||||
),
|
||||
lambda values: len(values) == 1,
|
||||
seconds=70,
|
||||
)
|
||||
row: Final = rows[0]
|
||||
raw_tags: Final = row["request_tags"]
|
||||
request_tags: Final = json.loads(raw_tags) if isinstance(raw_tags, str) else raw_tags
|
||||
policy_tags: Final = [
|
||||
tag for tag in request_tags if not (isinstance(tag, str) and tag.startswith("User-Agent: "))
|
||||
]
|
||||
assert policy_tags == ["gemini-js-sdk", "pass-through-endpoint"], row
|
||||
assert row["call_type"] == "pass_through_endpoint", row
|
||||
assert row["status"] == "success", row
|
||||
|
|
|
|||
4117
tests/pass_through_tests/package-lock.json
generated
4117
tests/pass_through_tests/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "litellm-pass-through-tests",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "JS pass-through tests for Vertex AI / Google AI Studio routes. CI-only; not published.",
|
||||
"dependencies": {
|
||||
"@google-cloud/vertexai": "1.12.0",
|
||||
"@google/generative-ai": "0.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "29.7.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@google-cloud/vertexai": {
|
||||
"google-auth-library": "10.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
// const { GoogleGenerativeAI } = require("@google/generative-ai");
|
||||
|
||||
// const genAI = new GoogleGenerativeAI(process.env.LITELLM_MASTER_KEY);
|
||||
// const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
|
||||
|
||||
// const prompt = "Explain how AI works in 2 pages";
|
||||
|
||||
// async function run() {
|
||||
// try {
|
||||
// const result = await model.generateContentStream(prompt, { baseUrl: "http://localhost:4000/gemini" });
|
||||
// const response = await result.response;
|
||||
// console.log(response.text());
|
||||
// for await (const chunk of result.stream) {
|
||||
// const chunkText = chunk.text();
|
||||
// console.log(chunkText);
|
||||
// process.stdout.write(chunkText);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error("Error:", error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// run();
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
const { GoogleGenerativeAI } = require("@google/generative-ai");
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const masterKey = process.env.LITELLM_MASTER_KEY;
|
||||
if (!masterKey) throw new Error("LITELLM_MASTER_KEY must be set");
|
||||
|
||||
// Import fetch if the SDK uses it
|
||||
const originalFetch = global.fetch || require('node-fetch');
|
||||
|
||||
let lastCallId;
|
||||
|
||||
// Monkey-patch the fetch used internally
|
||||
global.fetch = async function patchedFetch(url, options) {
|
||||
const response = await originalFetch(url, options);
|
||||
|
||||
// Store the call ID if it exists
|
||||
lastCallId = response.headers.get('x-litellm-call-id');
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
// Configure Jest to retry flaky tests (useful for external API flakiness)
|
||||
jest.retryTimes(3);
|
||||
|
||||
describe('Gemini AI Tests', () => {
|
||||
test('should successfully generate streaming content with tags', async () => {
|
||||
const genAI = new GoogleGenerativeAI(masterKey);
|
||||
|
||||
const requestOptions = {
|
||||
baseUrl: 'http://127.0.0.1:4000/gemini',
|
||||
customHeaders: {
|
||||
"tags": "gemini-js-sdk,pass-through-endpoint"
|
||||
}
|
||||
};
|
||||
|
||||
const model = genAI.getGenerativeModel({
|
||||
model: 'gemini-3.1-flash-lite'
|
||||
}, requestOptions);
|
||||
|
||||
const prompt = 'Say "hello test" and nothing else';
|
||||
|
||||
const streamingResult = await model.generateContentStream(prompt);
|
||||
expect(streamingResult).toBeDefined();
|
||||
|
||||
for await (const chunk of streamingResult.stream) {
|
||||
console.log('stream chunk:', JSON.stringify(chunk));
|
||||
expect(chunk).toBeDefined();
|
||||
}
|
||||
|
||||
const aggregatedResponse = await streamingResult.response;
|
||||
console.log('aggregated response:', JSON.stringify(aggregatedResponse));
|
||||
expect(aggregatedResponse).toBeDefined();
|
||||
|
||||
// Use the captured callId
|
||||
const callId = lastCallId;
|
||||
console.log("Captured Call ID:", callId);
|
||||
|
||||
// Poll for spend data with retries (DB writes can be slow in CI)
|
||||
let spendData = null;
|
||||
for (let attempt = 0; attempt < 6; attempt++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
const spendResponse = await fetch(
|
||||
`http://127.0.0.1:4000/spend/logs?request_id=${callId}`,
|
||||
{ headers: { 'Authorization': `Bearer ${masterKey}` } }
|
||||
);
|
||||
spendData = await spendResponse.json();
|
||||
console.log(`spendData (attempt ${attempt + 1}):`, spendData);
|
||||
if (spendData && spendData.length > 0 && spendData[0] && spendData[0].request_id) break;
|
||||
}
|
||||
|
||||
if (!spendData || !spendData.length || !spendData[0] || !spendData[0].request_id) {
|
||||
console.warn('Spend data not available after polling - skipping spend assertions (DB write may be slow in CI)');
|
||||
return;
|
||||
}
|
||||
|
||||
expect(spendData).toBeDefined();
|
||||
expect(spendData[0].request_id).toBe(callId);
|
||||
expect(spendData[0].call_type).toBe('pass_through_endpoint');
|
||||
expect(spendData[0].request_tags).toEqual(['gemini-js-sdk', 'pass-through-endpoint']);
|
||||
expect(spendData[0].metadata).toHaveProperty('user_api_key');
|
||||
expect(spendData[0].model).toContain('gemini');
|
||||
expect(spendData[0].spend).toBeGreaterThan(0);
|
||||
expect(spendData[0].custom_llm_provider).toBe('gemini');
|
||||
}, 90000);
|
||||
});
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
const { GoogleGenerativeAI, ModelParams, RequestOptions } = require("@google/generative-ai");
|
||||
|
||||
const masterKey = process.env.LITELLM_MASTER_KEY;
|
||||
if (!masterKey) throw new Error("LITELLM_MASTER_KEY must be set");
|
||||
|
||||
const modelParams = {
|
||||
model: 'gemini-3.1-flash-lite',
|
||||
};
|
||||
|
||||
const requestOptions = {
|
||||
baseUrl: 'http://127.0.0.1:4000/gemini',
|
||||
customHeaders: {
|
||||
"tags": "gemini-js-sdk,gemini-3.1-flash-lite"
|
||||
}
|
||||
};
|
||||
|
||||
const genAI = new GoogleGenerativeAI(masterKey);
|
||||
const model = genAI.getGenerativeModel(modelParams, requestOptions);
|
||||
|
||||
const testPrompt = "Explain how AI works";
|
||||
|
||||
async function main() {
|
||||
console.log("making request")
|
||||
try {
|
||||
const result = await model.generateContent(testPrompt);
|
||||
console.log(result.response.text());
|
||||
} catch (error) {
|
||||
console.error('Error details:', {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
cause: error.cause,
|
||||
// Check if there's a network error
|
||||
isNetworkError: error instanceof TypeError && error.message === 'fetch failed'
|
||||
});
|
||||
|
||||
// Check if the server is running
|
||||
if (error instanceof TypeError && error.message === 'fetch failed') {
|
||||
console.error('Make sure your local server is running at http://localhost:4000');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function main_streaming() {
|
||||
try {
|
||||
const streamingResult = await model.generateContentStream(testPrompt);
|
||||
for await (const item of streamingResult.stream) {
|
||||
console.log('stream chunk: ', JSON.stringify(item));
|
||||
}
|
||||
const aggregatedResponse = await streamingResult.response;
|
||||
console.log('aggregated response: ', JSON.stringify(aggregatedResponse));
|
||||
} catch (error) {
|
||||
console.error('Error details:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// main();
|
||||
main_streaming();
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
const { VertexAI, RequestOptions } = require('@google-cloud/vertexai');
|
||||
|
||||
const masterKey = process.env.LITELLM_MASTER_KEY;
|
||||
if (!masterKey) throw new Error("LITELLM_MASTER_KEY must be set");
|
||||
|
||||
|
||||
|
||||
const vertexAI = new VertexAI({
|
||||
project: 'litellm-ci-cd',
|
||||
location: 'global',
|
||||
apiEndpoint: "127.0.0.1:4000/vertex-ai"
|
||||
});
|
||||
|
||||
// Create customHeaders using Headers
|
||||
const customHeaders = new Headers({
|
||||
"X-Litellm-Api-Key": masterKey,
|
||||
tags: "vertexjs,test-2"
|
||||
});
|
||||
|
||||
// Use customHeaders in RequestOptions
|
||||
const requestOptions = {
|
||||
customHeaders: customHeaders,
|
||||
};
|
||||
|
||||
const generativeModel = vertexAI.getGenerativeModel(
|
||||
{ model: 'gemini-3.1-flash-lite' },
|
||||
requestOptions
|
||||
);
|
||||
|
||||
async function testModel() {
|
||||
try {
|
||||
const request = {
|
||||
contents: [{role: 'user', parts: [{text: 'How are you doing today tell me your name?'}]}],
|
||||
};
|
||||
const streamingResult = await generativeModel.generateContentStream(request);
|
||||
for await (const item of streamingResult.stream) {
|
||||
console.log('stream chunk: ', JSON.stringify(item));
|
||||
}
|
||||
const aggregatedResponse = await streamingResult.response;
|
||||
console.log('aggregated response: ', JSON.stringify(aggregatedResponse));
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
testModel();
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
const { VertexAI, RequestOptions } = require('@google-cloud/vertexai');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { writeFileSync } = require('fs');
|
||||
|
||||
|
||||
// Import fetch if the SDK uses it
|
||||
const originalFetch = global.fetch || require('node-fetch');
|
||||
|
||||
const { runVertexRequestOrSkip } = require('./vertex_test_helpers');
|
||||
|
||||
const masterKey = process.env.LITELLM_MASTER_KEY;
|
||||
if (!masterKey) throw new Error("LITELLM_MASTER_KEY must be set");
|
||||
|
||||
// Monkey-patch the fetch used internally
|
||||
global.fetch = async function patchedFetch(url, options) {
|
||||
// Modify the URL to use HTTP instead of HTTPS
|
||||
if (url.startsWith('https://localhost:4000')) {
|
||||
url = url.replace('https://', 'http://');
|
||||
}
|
||||
console.log('Patched fetch sending request to:', url);
|
||||
return originalFetch(url, options);
|
||||
};
|
||||
|
||||
function loadVertexAiCredentials() {
|
||||
console.log("loading vertex ai credentials");
|
||||
const filepath = path.dirname(__filename);
|
||||
const vertexKeyPath = path.join(filepath, "vertex_key.json");
|
||||
|
||||
// Initialize default empty service account data
|
||||
let serviceAccountKeyData = {};
|
||||
|
||||
// Try to read existing vertex_key.json
|
||||
try {
|
||||
const content = fs.readFileSync(vertexKeyPath, 'utf8');
|
||||
if (content && content.trim()) {
|
||||
serviceAccountKeyData = JSON.parse(content);
|
||||
}
|
||||
} catch (error) {
|
||||
// File doesn't exist or is invalid, continue with empty object
|
||||
}
|
||||
|
||||
// Update with environment variables
|
||||
const privateKeyId = process.env.VERTEX_AI_PRIVATE_KEY_ID || "";
|
||||
const privateKey = (process.env.VERTEX_AI_PRIVATE_KEY || "").replace(/\\n/g, "\n");
|
||||
|
||||
serviceAccountKeyData.private_key_id = privateKeyId;
|
||||
serviceAccountKeyData.private_key = privateKey;
|
||||
|
||||
// Create temporary file
|
||||
const tempFilePath = path.join(os.tmpdir(), `vertex-credentials-${Date.now()}.json`);
|
||||
writeFileSync(tempFilePath, JSON.stringify(serviceAccountKeyData, null, 2));
|
||||
|
||||
// Set environment variable
|
||||
process.env.GOOGLE_APPLICATION_CREDENTIALS = tempFilePath;
|
||||
}
|
||||
|
||||
// Run credential loading before tests
|
||||
beforeAll(() => {
|
||||
loadVertexAiCredentials();
|
||||
});
|
||||
|
||||
// Configure Jest to retry flaky tests up to 3 times (useful for 429 rate limiting)
|
||||
jest.retryTimes(3);
|
||||
|
||||
// Non-streaming Vertex generateContent can exceed 5s in CI / under load
|
||||
const VERTEX_TEST_TIMEOUT_MS = 30000;
|
||||
|
||||
describe('Vertex AI Tests', () => {
|
||||
test(
|
||||
'should successfully generate content from Vertex AI',
|
||||
async () => {
|
||||
const vertexAI = new VertexAI({
|
||||
project: 'litellm-ci-cd',
|
||||
location: 'global',
|
||||
apiEndpoint: "localhost:4000/vertex-ai"
|
||||
});
|
||||
|
||||
const customHeaders = new Headers({
|
||||
"x-litellm-api-key": masterKey
|
||||
});
|
||||
|
||||
const requestOptions = {
|
||||
customHeaders: customHeaders
|
||||
};
|
||||
|
||||
const generativeModel = vertexAI.getGenerativeModel(
|
||||
{ model: 'gemini-3.1-flash-lite' },
|
||||
requestOptions
|
||||
);
|
||||
|
||||
const request = {
|
||||
contents: [{role: 'user', parts: [{text: 'How are you doing today tell me your name?'}]}],
|
||||
};
|
||||
|
||||
const streamingResult = await runVertexRequestOrSkip(() =>
|
||||
generativeModel.generateContentStream(request)
|
||||
);
|
||||
if (streamingResult === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add some assertions
|
||||
expect(streamingResult).toBeDefined();
|
||||
|
||||
for await (const item of streamingResult.stream) {
|
||||
console.log('stream chunk:', JSON.stringify(item));
|
||||
expect(item).toBeDefined();
|
||||
}
|
||||
|
||||
const aggregatedResponse = await streamingResult.response;
|
||||
console.log('aggregated response:', JSON.stringify(aggregatedResponse));
|
||||
expect(aggregatedResponse).toBeDefined();
|
||||
},
|
||||
VERTEX_TEST_TIMEOUT_MS
|
||||
);
|
||||
|
||||
test(
|
||||
'should successfully generate non-streaming content from Vertex AI',
|
||||
async () => {
|
||||
const vertexAI = new VertexAI({
|
||||
project: 'litellm-ci-cd',
|
||||
location: 'global',
|
||||
apiEndpoint: "localhost:4000/vertex-ai"
|
||||
});
|
||||
const customHeaders = new Headers({"x-litellm-api-key": masterKey});
|
||||
const requestOptions = {customHeaders: customHeaders};
|
||||
const generativeModel = vertexAI.getGenerativeModel(
|
||||
{model: 'gemini-3.1-flash-lite'},
|
||||
requestOptions
|
||||
);
|
||||
const request = {contents: [{role: 'user', parts: [{text: 'What is 2+2?'}]}]};
|
||||
|
||||
const result = await runVertexRequestOrSkip(() =>
|
||||
generativeModel.generateContent(request)
|
||||
);
|
||||
if (result === null) {
|
||||
return;
|
||||
}
|
||||
expect(result).toBeDefined();
|
||||
expect(result.response).toBeDefined();
|
||||
console.log('non-streaming response:', JSON.stringify(result.response));
|
||||
},
|
||||
VERTEX_TEST_TIMEOUT_MS
|
||||
);
|
||||
});
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
const { VertexAI, RequestOptions } = require('@google-cloud/vertexai');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { writeFileSync } = require('fs');
|
||||
|
||||
|
||||
// Import fetch if the SDK uses it
|
||||
const originalFetch = global.fetch || require('node-fetch');
|
||||
|
||||
let lastCallId;
|
||||
|
||||
const { runVertexRequestOrSkip } = require('./vertex_test_helpers');
|
||||
|
||||
const masterKey = process.env.LITELLM_MASTER_KEY;
|
||||
if (!masterKey) throw new Error("LITELLM_MASTER_KEY must be set");
|
||||
|
||||
// Monkey-patch the fetch used internally
|
||||
global.fetch = async function patchedFetch(url, options) {
|
||||
// Modify the URL to use HTTP instead of HTTPS
|
||||
if (url.startsWith('https://127.0.0.1:4000')) {
|
||||
url = url.replace('https://', 'http://');
|
||||
}
|
||||
console.log('Patched fetch sending request to:', url);
|
||||
|
||||
const response = await originalFetch(url, options);
|
||||
|
||||
// Store the call ID if it exists
|
||||
lastCallId = response.headers.get('x-litellm-call-id');
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
function loadVertexAiCredentials() {
|
||||
console.log("loading vertex ai credentials");
|
||||
const filepath = path.dirname(__filename);
|
||||
const vertexKeyPath = path.join(filepath, "vertex_key.json");
|
||||
|
||||
// Initialize default empty service account data
|
||||
let serviceAccountKeyData = {};
|
||||
|
||||
// Try to read existing vertex_key.json
|
||||
try {
|
||||
const content = fs.readFileSync(vertexKeyPath, 'utf8');
|
||||
if (content && content.trim()) {
|
||||
serviceAccountKeyData = JSON.parse(content);
|
||||
}
|
||||
} catch (error) {
|
||||
// File doesn't exist or is invalid, continue with empty object
|
||||
}
|
||||
|
||||
// Update with environment variables
|
||||
const privateKeyId = process.env.VERTEX_AI_PRIVATE_KEY_ID || "";
|
||||
const privateKey = (process.env.VERTEX_AI_PRIVATE_KEY || "").replace(/\\n/g, "\n");
|
||||
|
||||
serviceAccountKeyData.private_key_id = privateKeyId;
|
||||
serviceAccountKeyData.private_key = privateKey;
|
||||
|
||||
// Create temporary file
|
||||
const tempFilePath = path.join(os.tmpdir(), `vertex-credentials-${Date.now()}.json`);
|
||||
writeFileSync(tempFilePath, JSON.stringify(serviceAccountKeyData, null, 2));
|
||||
|
||||
// Set environment variable
|
||||
process.env.GOOGLE_APPLICATION_CREDENTIALS = tempFilePath;
|
||||
}
|
||||
|
||||
// Run credential loading before tests
|
||||
beforeAll(() => {
|
||||
loadVertexAiCredentials();
|
||||
});
|
||||
|
||||
// Configure Jest to retry flaky tests up to 3 times (useful for 429 rate limiting)
|
||||
jest.retryTimes(3);
|
||||
|
||||
describe('Vertex AI Tests', () => {
|
||||
test('should successfully generate non-streaming content with tags', async () => {
|
||||
const vertexAI = new VertexAI({
|
||||
project: 'litellm-ci-cd',
|
||||
location: 'global',
|
||||
apiEndpoint: "127.0.0.1:4000/vertex_ai"
|
||||
});
|
||||
|
||||
const customHeaders = new Headers({
|
||||
"x-litellm-api-key": masterKey,
|
||||
"tags": "vertex-js-sdk,pass-through-endpoint"
|
||||
});
|
||||
|
||||
const requestOptions = {
|
||||
customHeaders: customHeaders
|
||||
};
|
||||
|
||||
const generativeModel = vertexAI.getGenerativeModel(
|
||||
{ model: 'gemini-3.1-flash-lite' },
|
||||
requestOptions
|
||||
);
|
||||
|
||||
const request = {
|
||||
contents: [{role: 'user', parts: [{text: 'Say "hello test" and nothing else'}]}]
|
||||
};
|
||||
|
||||
const result = await runVertexRequestOrSkip(() =>
|
||||
generativeModel.generateContent(request)
|
||||
);
|
||||
if (result === null) {
|
||||
return;
|
||||
}
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// Use the captured callId
|
||||
const callId = lastCallId;
|
||||
console.log("Captured Call ID:", callId);
|
||||
|
||||
// Poll for spend data with retries (DB writes can be slow in CI)
|
||||
let spendData = null;
|
||||
for (let attempt = 0; attempt < 6; attempt++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
const spendResponse = await fetch(
|
||||
`http://127.0.0.1:4000/spend/logs?request_id=${callId}`,
|
||||
{ headers: { 'Authorization': `Bearer ${masterKey}` } }
|
||||
);
|
||||
spendData = await spendResponse.json();
|
||||
console.log(`spendData (attempt ${attempt + 1}):`, spendData);
|
||||
if (spendData && spendData.length > 0 && spendData[0] && spendData[0].request_id) break;
|
||||
}
|
||||
|
||||
if (!spendData || !spendData.length || !spendData[0] || !spendData[0].request_id) {
|
||||
console.warn('Spend data not available after polling - skipping spend assertions (DB write may be slow in CI)');
|
||||
return;
|
||||
}
|
||||
|
||||
expect(spendData[0].request_id).toBe(callId);
|
||||
expect(spendData[0].call_type).toBe('pass_through_endpoint');
|
||||
expect(spendData[0].request_tags).toEqual(['vertex-js-sdk', 'pass-through-endpoint']);
|
||||
expect(spendData[0].metadata).toHaveProperty('user_api_key');
|
||||
expect(spendData[0].model).toContain('gemini');
|
||||
expect(spendData[0].spend).toBeGreaterThan(0);
|
||||
expect(spendData[0].custom_llm_provider).toBe('vertex_ai');
|
||||
}, 90000);
|
||||
|
||||
test('should successfully generate streaming content with tags', async () => {
|
||||
const vertexAI = new VertexAI({
|
||||
project: 'litellm-ci-cd',
|
||||
location: 'global',
|
||||
apiEndpoint: "127.0.0.1:4000/vertex_ai"
|
||||
});
|
||||
|
||||
const customHeaders = new Headers({
|
||||
"x-litellm-api-key": masterKey,
|
||||
"tags": "vertex-js-sdk,pass-through-endpoint"
|
||||
});
|
||||
|
||||
const requestOptions = {
|
||||
customHeaders: customHeaders
|
||||
};
|
||||
|
||||
const generativeModel = vertexAI.getGenerativeModel(
|
||||
{ model: 'gemini-3.1-flash-lite' },
|
||||
requestOptions
|
||||
);
|
||||
|
||||
const request = {
|
||||
contents: [{role: 'user', parts: [{text: 'Say "hello test" and nothing else'}]}]
|
||||
};
|
||||
|
||||
const streamingResult = await runVertexRequestOrSkip(() =>
|
||||
generativeModel.generateContentStream(request)
|
||||
);
|
||||
if (streamingResult === null) {
|
||||
return;
|
||||
}
|
||||
expect(streamingResult).toBeDefined();
|
||||
|
||||
|
||||
// Add some assertions
|
||||
expect(streamingResult).toBeDefined();
|
||||
|
||||
for await (const item of streamingResult.stream) {
|
||||
console.log('stream chunk:', JSON.stringify(item));
|
||||
expect(item).toBeDefined();
|
||||
}
|
||||
|
||||
const aggregatedResponse = await streamingResult.response;
|
||||
console.log('aggregated response:', JSON.stringify(aggregatedResponse));
|
||||
expect(aggregatedResponse).toBeDefined();
|
||||
|
||||
// Use the captured callId
|
||||
const callId = lastCallId;
|
||||
console.log("Captured Call ID:", callId);
|
||||
|
||||
// Poll for spend data with retries (DB writes can be slow in CI)
|
||||
let spendData = null;
|
||||
for (let attempt = 0; attempt < 6; attempt++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
const spendResponse = await fetch(
|
||||
`http://127.0.0.1:4000/spend/logs?request_id=${callId}`,
|
||||
{ headers: { 'Authorization': `Bearer ${masterKey}` } }
|
||||
);
|
||||
spendData = await spendResponse.json();
|
||||
console.log(`spendData (attempt ${attempt + 1}):`, spendData);
|
||||
if (spendData && spendData.length > 0 && spendData[0] && spendData[0].request_id) break;
|
||||
}
|
||||
|
||||
if (!spendData || !spendData.length || !spendData[0] || !spendData[0].request_id) {
|
||||
console.warn('Spend data not available after polling - skipping spend assertions (DB write may be slow in CI)');
|
||||
return;
|
||||
}
|
||||
|
||||
expect(spendData[0].request_id).toBe(callId);
|
||||
expect(spendData[0].call_type).toBe('pass_through_endpoint');
|
||||
expect(spendData[0].request_tags).toEqual(['vertex-js-sdk', 'pass-through-endpoint']);
|
||||
expect(spendData[0].metadata).toHaveProperty('user_api_key');
|
||||
expect(spendData[0].model).toContain('gemini');
|
||||
expect(spendData[0].spend).toBeGreaterThan(0);
|
||||
expect(spendData[0].custom_llm_provider).toBe('vertex_ai');
|
||||
}, 90000);
|
||||
});
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"type": "service_account",
|
||||
"project_id": "litellm-ci-cd",
|
||||
"private_key_id": "",
|
||||
"private_key": "",
|
||||
"client_email": "test-litellm-ci-cd@litellm-ci-cd.iam.gserviceaccount.com",
|
||||
"client_id": "116563532503305622785",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test-litellm-ci-cd%40litellm-ci-cd.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
function isVertexQuotaError(error) {
|
||||
const message = [
|
||||
error && error.message,
|
||||
error && error.stack,
|
||||
error && error.cause && JSON.stringify(error.cause),
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
return (
|
||||
message.includes('429') ||
|
||||
message.includes('Too Many Requests') ||
|
||||
message.includes('RESOURCE_EXHAUSTED')
|
||||
);
|
||||
}
|
||||
|
||||
async function runVertexRequestOrSkip(requestFn) {
|
||||
try {
|
||||
return await requestFn();
|
||||
} catch (error) {
|
||||
if (isVertexQuotaError(error)) {
|
||||
console.warn('Vertex AI quota exhausted; skipping live provider assertions for this run');
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { isVertexQuotaError, runVertexRequestOrSkip };
|
||||
|
|
@ -9083,3 +9083,109 @@ def test_update_subpath_route_updates_registry():
|
|||
del _registered_pass_through_routes[route_key]
|
||||
|
||||
asyncio.run(_async_test())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(("operation", "stream"), (("generateContent", False), ("streamGenerateContent", True)))
|
||||
async def test_vertex_passthrough_forwards_body_and_request_tags_to_spend_payload(
|
||||
operation: str, stream: bool, httpx_transport: None
|
||||
) -> None:
|
||||
from datetime import datetime
|
||||
|
||||
import respx
|
||||
|
||||
from litellm.types.passthrough_endpoints.pass_through_endpoints import PassthroughStandardLoggingPayload
|
||||
|
||||
target: Final = (
|
||||
"https://aiplatform.googleapis.com/v1/projects/test-project/locations/global/publishers/google/"
|
||||
f"models/gemini-3.1-flash-lite:{operation}"
|
||||
)
|
||||
request_body: Final = {"contents": [{"role": "user", "parts": [{"text": "Say hello"}]}]}
|
||||
response_body: Final = {
|
||||
"candidates": [{"content": {"role": "model", "parts": [{"text": "hello"}]}, "finishReason": "STOP"}],
|
||||
"usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 1, "totalTokenCount": 2},
|
||||
}
|
||||
from starlette.types import Message
|
||||
|
||||
async def receive() -> Message:
|
||||
return {"type": "http.request", "body": json.dumps(request_body).encode(), "more_body": False}
|
||||
|
||||
request: Final = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"method": "POST",
|
||||
"path": "/test",
|
||||
"scheme": "http",
|
||||
"server": ("localhost", 8000),
|
||||
"query_string": b"",
|
||||
"headers": [(b"tags", b"vertex-js-sdk,pass-through-endpoint")],
|
||||
},
|
||||
receive=receive,
|
||||
)
|
||||
user_api_key_dict: Final = UserAPIKeyAuth(api_key="test-key", user_id="test-user", team_id="test-team")
|
||||
|
||||
with respx.mock(assert_all_called=True) as upstream:
|
||||
route: Final = upstream.post(target).respond(status_code=200, json=response_body)
|
||||
response: Final = await pass_through_request(
|
||||
request=request,
|
||||
target=target,
|
||||
custom_headers={},
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
custom_llm_provider="vertex_ai",
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
if stream:
|
||||
streamed_body: Final = b"".join([chunk async for chunk in response.body_iterator])
|
||||
assert json.loads(streamed_body) == response_body
|
||||
assert json.loads(route.calls.last.request.content) == request_body
|
||||
|
||||
start_time: Final = datetime(2026, 1, 15, 12, 0, 0)
|
||||
passthrough_payload: Final = PassthroughStandardLoggingPayload(
|
||||
url=target,
|
||||
request_body=request_body,
|
||||
request_method="POST",
|
||||
)
|
||||
logging_obj: Final = LiteLLMLoggingObj(
|
||||
model="gemini-3.1-flash-lite",
|
||||
messages=[],
|
||||
stream=stream,
|
||||
call_type="pass_through_endpoint",
|
||||
start_time=start_time,
|
||||
litellm_call_id="vertex-request-tags",
|
||||
function_id="vertex-request-tags",
|
||||
)
|
||||
logging_kwargs: Final = HttpPassThroughEndpointHelpers._init_kwargs_for_pass_through_endpoint(
|
||||
request=request,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
passthrough_logging_payload=passthrough_payload,
|
||||
_parsed_body=request_body,
|
||||
litellm_call_id="vertex-request-tags",
|
||||
logging_obj=logging_obj,
|
||||
)
|
||||
logging_obj.update_environment_variables(
|
||||
model="gemini-3.1-flash-lite",
|
||||
user="unknown",
|
||||
optional_params={},
|
||||
litellm_params=logging_kwargs["litellm_params"],
|
||||
call_type="pass_through_endpoint",
|
||||
)
|
||||
log_dispatch: Final = AsyncMock()
|
||||
await PassThroughEndpointLogging(log_dispatch=log_dispatch).pass_through_async_success_handler(
|
||||
httpx_response=route.calls.last.response,
|
||||
response_body=response_body,
|
||||
logging_obj=logging_obj,
|
||||
url_route=target,
|
||||
result=json.dumps(response_body),
|
||||
start_time=start_time,
|
||||
end_time=start_time,
|
||||
cache_hit=False,
|
||||
request_body=request_body,
|
||||
custom_llm_provider="vertex_ai",
|
||||
**logging_kwargs,
|
||||
)
|
||||
|
||||
logged_kwargs: Final = log_dispatch.await_args.kwargs
|
||||
assert logged_kwargs["call_type"] == "pass_through_endpoint"
|
||||
assert logged_kwargs["litellm_params"]["metadata"]["tags"] == ["vertex-js-sdk", "pass-through-endpoint"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue