From 180d1650fb7259aa35267aab0a8fff748f9646d1 Mon Sep 17 00:00:00 2001
From: J_Sinclair <56301946+E-R-Butch@users.noreply.github.com>
Date: Mon, 7 Sep 2026 16:17:13 +0800
Subject: [PATCH 1/6] feat(ui): add Chinese language switching for login and
navigation
---
ui/litellm-dashboard/package-lock.json | 74 ++++++++++-
ui/litellm-dashboard/package.json | 2 +
ui/litellm-dashboard/src/app/layout.tsx | 15 ++-
.../app/login/LoginPage.integration.test.tsx | 40 +++++-
.../src/app/login/LoginPage.tsx | 99 +++++++++------
.../src/components/DashboardHeader.tsx | 6 +-
.../src/components/LanguageSwitcher.tsx | 47 +++++++
.../Navbar/BlogDropdown/BlogDropdown.tsx | 13 +-
.../components/Navbar/DocsLink/DocsLink.tsx | 15 ++-
.../NotificationsBell/NotificationsBell.tsx | 12 +-
.../Navbar/UserDropdown/UserDropdown.tsx | 44 ++++---
.../src/components/Navbar/ViewSwitcher.tsx | 12 +-
.../components/Navbar/navDisplayName.test.ts | 6 +
.../src/components/Navbar/navDisplayName.ts | 6 +-
.../SidebarAccountMenu/SidebarAccountMenu.tsx | 47 +++----
.../components/ThemeToggle/ThemeToggle.tsx | 4 +-
.../common_components/LoadingScreen.tsx | 4 +-
.../src/components/leftnav.test.tsx | 36 ++++++
.../src/components/leftnav.tsx | 47 ++++---
.../src/components/navbar.tsx | 10 +-
.../src/components/shared/PasswordInput.tsx | 4 +-
.../LanguageProvider.integration.test.tsx | 82 +++++++++++++
.../src/i18n/LanguageProvider.tsx | 28 +++++
ui/litellm-dashboard/src/i18n/README.md | 13 ++
ui/litellm-dashboard/src/i18n/index.test.ts | 35 ++++++
ui/litellm-dashboard/src/i18n/index.ts | 27 ++++
ui/litellm-dashboard/src/i18n/locales/en.json | 115 ++++++++++++++++++
.../src/i18n/locales/zh-CN.json | 115 ++++++++++++++++++
.../src/i18n/useTranslation.ts | 12 ++
29 files changed, 834 insertions(+), 136 deletions(-)
create mode 100644 ui/litellm-dashboard/src/components/LanguageSwitcher.tsx
create mode 100644 ui/litellm-dashboard/src/i18n/LanguageProvider.integration.test.tsx
create mode 100644 ui/litellm-dashboard/src/i18n/LanguageProvider.tsx
create mode 100644 ui/litellm-dashboard/src/i18n/README.md
create mode 100644 ui/litellm-dashboard/src/i18n/index.test.ts
create mode 100644 ui/litellm-dashboard/src/i18n/index.ts
create mode 100644 ui/litellm-dashboard/src/i18n/locales/en.json
create mode 100644 ui/litellm-dashboard/src/i18n/locales/zh-CN.json
create mode 100644 ui/litellm-dashboard/src/i18n/useTranslation.ts
diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json
index 4e5b0c1dda6..6e84a605827 100644
--- a/ui/litellm-dashboard/package-lock.json
+++ b/ui/litellm-dashboard/package-lock.json
@@ -21,6 +21,7 @@
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
"dayjs": "1.11.19",
+ "i18next": "26.4.2",
"jwt-decode": "4.0.0",
"lucide-react": "0.513.0",
"moment": "2.30.1",
@@ -35,6 +36,7 @@
"react-copy-to-clipboard": "5.1.1",
"react-dom": "19.2.8",
"react-hook-form": "7.82.0",
+ "react-i18next": "17.0.13",
"react-json-view-lite": "2.5.0",
"react-markdown": "9.1.0",
"react-syntax-highlighter": "15.6.6",
@@ -414,9 +416,9 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.29.2",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
- "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -7290,6 +7292,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/html-parse-stringify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-4.0.1.tgz",
+ "integrity": "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://locize.com"
+ }
+ },
"node_modules/html-url-attributes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -7337,6 +7348,34 @@
"ms": "^2.0.0"
}
},
+ "node_modules/i18next": {
+ "version": "26.4.2",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.2.tgz",
+ "integrity": "sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://www.locize.com/i18next"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.locize.com"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": "^5 || ^6 || ^7"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -10650,6 +10689,33 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
+ "node_modules/react-i18next": {
+ "version": "17.0.13",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.13.tgz",
+ "integrity": "sha512-Cc1PscmblIHA1kljTqDwrcVMI21ydgmUzw0UAeQBe7pAOgfuRLfzXze4EUBQoeDiICzFIXXhHFoZxuetNg5D0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.7",
+ "html-parse-stringify": "^4.0.1",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "peerDependencies": {
+ "i18next": ">= 26.2.0",
+ "react": ">= 16.8.0",
+ "typescript": "^5 || ^6 || ^7"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -12171,7 +12237,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json
index ededdfb4606..f331200e3e0 100644
--- a/ui/litellm-dashboard/package.json
+++ b/ui/litellm-dashboard/package.json
@@ -37,6 +37,7 @@
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
"dayjs": "1.11.19",
+ "i18next": "26.4.2",
"jwt-decode": "4.0.0",
"lucide-react": "0.513.0",
"moment": "2.30.1",
@@ -51,6 +52,7 @@
"react-copy-to-clipboard": "5.1.1",
"react-dom": "19.2.8",
"react-hook-form": "7.82.0",
+ "react-i18next": "17.0.13",
"react-json-view-lite": "2.5.0",
"react-markdown": "9.1.0",
"react-syntax-highlighter": "15.6.6",
diff --git a/ui/litellm-dashboard/src/app/layout.tsx b/ui/litellm-dashboard/src/app/layout.tsx
index 60db7104a03..81a4109b9c6 100644
--- a/ui/litellm-dashboard/src/app/layout.tsx
+++ b/ui/litellm-dashboard/src/app/layout.tsx
@@ -1,3 +1,4 @@
+import LanguageProvider from "@/i18n/LanguageProvider";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
@@ -28,12 +29,14 @@ export default function RootLayout({
-
-
- {children}
-
-
-
+
+
+
+ {children}
+
+
+
+
diff --git a/ui/litellm-dashboard/src/app/login/LoginPage.integration.test.tsx b/ui/litellm-dashboard/src/app/login/LoginPage.integration.test.tsx
index bfcc895e579..7a101a3ce8c 100644
--- a/ui/litellm-dashboard/src/app/login/LoginPage.integration.test.tsx
+++ b/ui/litellm-dashboard/src/app/login/LoginPage.integration.test.tsx
@@ -3,6 +3,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import LoginPage from "./LoginPage";
+import LanguageProvider from "@/i18n/LanguageProvider";
const mockPush = vi.fn();
const mockReplace = vi.fn();
@@ -60,7 +61,9 @@ const renderLoginPage = () => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
return render(
-
+
+
+
,
);
};
@@ -77,6 +80,7 @@ const originalLocation = window.location;
describe("LoginPage submit payload", () => {
beforeEach(() => {
vi.clearAllMocks();
+ window.localStorage.removeItem("litellm_ui_language");
WORKERS = [];
setSearch("");
(useUIConfig as ReturnType).mockReturnValue({
@@ -91,6 +95,7 @@ describe("LoginPage submit payload", () => {
});
afterEach(() => {
+ window.localStorage.removeItem("litellm_ui_language");
Object.defineProperty(window, "location", { value: originalLocation, writable: true });
});
@@ -190,3 +195,36 @@ describe("LoginPage submit payload", () => {
expect(switchToWorkerUrl).toHaveBeenCalledWith("http://worker-a:4000");
});
});
+
+describe("localized login", () => {
+ afterEach(() => {
+ window.localStorage.removeItem("litellm_ui_language");
+ });
+
+ it("updates existing validation errors when switching language and keeps the submitted payload unchanged", async () => {
+ vi.clearAllMocks();
+ WORKERS = [];
+ setSearch("");
+ window.localStorage.removeItem("litellm_ui_language");
+ (useUIConfig as ReturnType).mockReturnValue({ data: { sso_configured: false }, isLoading: false });
+ const user = userEvent.setup();
+ renderLoginPage();
+ await user.click(await screen.findByRole("button", { name: "Login", exact: true }));
+ expect(await screen.findByText("Please enter your username")).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "Language" }));
+ await user.click(await screen.findByRole("menuitemradio", { name: "简体中文" }));
+ expect(await screen.findByText("请输入用户名")).toBeInTheDocument();
+ expect(screen.getByText("请输入密码")).toBeInTheDocument();
+ expect(mockMutate).not.toHaveBeenCalled();
+ fireEvent.change(screen.getByLabelText("用户名"), { target: { value: "admin" } });
+ fireEvent.change(screen.getByLabelText("密码"), { target: { value: "opaque-password" } });
+ await user.click(screen.getByRole("button", { name: "登录", exact: true }));
+ await waitFor(() => expect(mockMutate).toHaveBeenCalledTimes(1));
+ expect(mockMutate.mock.calls[0][0]).toStrictEqual({ username: "admin", password: "opaque-password", useV3: false });
+ expect(screen.getByText("MASTER_KEY")).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: "查看文档" })).toHaveAttribute(
+ "href",
+ "https://docs.litellm.ai/docs/proxy/ui",
+ );
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/login/LoginPage.tsx b/ui/litellm-dashboard/src/app/login/LoginPage.tsx
index 11cf5ffad3d..dcdf1dbef71 100644
--- a/ui/litellm-dashboard/src/app/login/LoginPage.tsx
+++ b/ui/litellm-dashboard/src/app/login/LoginPage.tsx
@@ -1,5 +1,8 @@
"use client";
+import LanguageSwitcher from "@/components/LanguageSwitcher";
+import { Trans } from "react-i18next";
+import { useTranslation } from "@/i18n/useTranslation";
import { useLogin } from "@/app/(dashboard)/hooks/login/useLogin";
import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig";
import LoadingScreen from "@/components/common_components/LoadingScreen";
@@ -24,14 +27,10 @@ import { useEffect, useId, useState } from "react";
import { z } from "zod/v4";
import { useWorker } from "@/hooks/useWorker";
-const loginSchema = z.object({
- username: z.string().min(1, "Please enter your username"),
- password: z.string().min(1, "Please enter your password"),
-});
-
-type LoginFormValues = z.infer;
+type LoginFormValues = { username: string; password: string };
function SsoEnabledNotice() {
+ const { t, i18n } = useTranslation();
const [dismissed, setDismissed] = useState(false);
if (dismissed) {
@@ -42,13 +41,15 @@ function SsoEnabledNotice() {
- Single Sign-On (SSO) is enabled. LiteLLM no longer automatically redirects to the SSO login flow upon loading
- this page. To re-enable auto-redirect-to-SSO, set{" "}
- AUTO_REDIRECT_UI_LOGIN_TO_SSO=true in your
- environment configuration.
+ }}
+ />
- setDismissed(true)}>
+ setDismissed(true)}>
@@ -57,6 +58,11 @@ function SsoEnabledNotice() {
}
function LoginPageContent() {
+ const { t, i18n } = useTranslation();
+ const loginSchema = z.object({
+ username: z.string().min(1, t("Please enter your username")),
+ password: z.string().min(1, t("Please enter your password")),
+ });
const [isLoading, setIsLoading] = useState(true);
const { data: uiConfig, isLoading: isConfigLoading } = useUIConfig();
const loginMutation = useLogin();
@@ -66,6 +72,12 @@ function LoginPageContent() {
const workerFieldId = useId();
const form = useZodForm(loginSchema, { defaultValues: { username: "", password: "" } });
+ useEffect(() => {
+ if (form.getFieldState("username").error || form.getFieldState("password").error) {
+ void form.trigger();
+ }
+ }, [form, t]);
+
// Pre-select worker from URL param (e.g. /ui/login?worker=team-b)
useEffect(() => {
const params = new URLSearchParams(window.location.search);
@@ -190,6 +202,9 @@ function LoginPageContent() {
+
+
+
🚅 LiteLLM
@@ -197,11 +212,12 @@ function LoginPageContent() {
- Admin UI Disabled
+ {t("Admin UI Disabled")}
- The Admin UI has been disabled by the administrator. To re-enable it, please update the following
- environment variable:
+ {t(
+ "The Admin UI has been disabled by the administrator. To re-enable it, please update the following environment variable:",
+ )}{" "}
DISABLE_ADMIN_UI=False
@@ -219,6 +235,9 @@ function LoginPageContent() {
+
+
+
@@ -226,26 +245,34 @@ function LoginPageContent() {
-
Login
-
Access your LiteLLM Admin UI.
+
{t("Login")}
+
{t("Access your LiteLLM Admin UI.")}
{!uiConfig?.hide_default_credentials_hint && (
- Default Credentials
+ {t("Default Credentials")}
- By default, Username is admin and
- Password is your set LiteLLM Proxy
- MASTER_KEY.
+ }}
+ />
- Need to set UI credentials or SSO?{" "}
-
- Check the documentation
-
- .
+
+ ),
+ }}
+ />
@@ -262,14 +289,14 @@ function LoginPageContent() {
{uiConfig?.is_control_plane && workers.length > 0 && (
- Worker
+ {t("Worker")}
({ label: worker.name, value: worker.worker_id }))}
value={selectedWorkerId}
onValueChange={(value: string | null) => setSelectedWorkerId(value)}
>
-
+
{workers.map((worker) => (
@@ -282,12 +309,12 @@ function LoginPageContent() {
)}
-
+
{({ ref, ...field }) => (
-
+
{({ ref, ...field }) => (
- {isLoginLoading && }
- {isLoginLoading ? "Logging in..." : "Login"}
+ {isLoginLoading && }
+ {isLoginLoading ? t("Logging in...") : t("Login")}
{!uiConfig?.sso_configured ? (
}>
- Login with SSO
+ {t("Login with SSO")}
- Please configure SSO to log in with SSO.
+ {t("Please configure SSO to log in with SSO.")}
) : (
- Login with SSO
+ {t("Login with SSO")}
)}
diff --git a/ui/litellm-dashboard/src/components/DashboardHeader.tsx b/ui/litellm-dashboard/src/components/DashboardHeader.tsx
index 57d734d05b3..5fb63c08397 100644
--- a/ui/litellm-dashboard/src/components/DashboardHeader.tsx
+++ b/ui/litellm-dashboard/src/components/DashboardHeader.tsx
@@ -1,5 +1,7 @@
"use client";
+import LanguageSwitcher from "@/components/LanguageSwitcher";
+import { useTranslation } from "@/i18n/useTranslation";
import {
Breadcrumb,
BreadcrumbItem,
@@ -25,6 +27,7 @@ import { usePathname } from "next/navigation";
// Top bar for the dashboard shell. Sits only over the content column (the brand
// lives in the sidebar header); mirrors the design's breadcrumb-left / tools-right layout.
export function DashboardHeader() {
+ const { t } = useTranslation();
const { title } = getBreadcrumb(usePathname());
const { isControlPlane, selectedWorker } = useWorker();
const showWorkerSwitch = isControlPlane && selectedWorker !== null;
@@ -47,7 +50,7 @@ export function DashboardHeader() {
- {title}
+ {t(title)}
@@ -63,6 +66,7 @@ export function DashboardHeader() {
{!hideCommunityLinks &&
}
+
diff --git a/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx b/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx
new file mode 100644
index 00000000000..a81a34fcb44
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import { Languages } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { isLanguage, LANGUAGE_STORAGE_KEY } from "@/i18n";
+import { useTranslation } from "@/i18n/useTranslation";
+
+export default function LanguageSwitcher() {
+ const { t, i18n } = useTranslation();
+
+ const selectLanguage = (language: string) => {
+ if (!isLanguage(language)) return;
+ void i18n.changeLanguage(language);
+ try {
+ window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language);
+ } catch {
+ // Storage can be disabled by the browser; keep the current selection usable
+ }
+ };
+
+ return (
+
+ }
+ >
+
+
+
+
+
+ English
+
+
+ 简体中文
+
+
+
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx
index eaaf3301a8b..aa9eeb93cba 100644
--- a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "@/i18n/useTranslation";
import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts";
import { useBlogPosts, type BlogPost } from "@/app/(dashboard)/hooks/blogPosts/useBlogPosts";
import { NAV_PRODUCT_LINK_CLASS } from "@/components/Navbar/navProductLinkClass";
@@ -22,6 +23,7 @@ function formatDate(dateStr: string): string {
}
export const BlogDropdown: React.FC = () => {
+ const { t } = useTranslation();
const disableBlogPosts = useDisableBlogPosts();
const { data, isLoading, isError, refetch } = useBlogPosts();
@@ -34,7 +36,7 @@ export const BlogDropdown: React.FC = () => {
if (isLoading) {
return (
-
+
);
}
@@ -42,7 +44,7 @@ export const BlogDropdown: React.FC = () => {
if (isError) {
return (
-
Failed to load posts
+
{t("Failed to load posts")}
refetch()}>
Retry
@@ -51,7 +53,7 @@ export const BlogDropdown: React.FC = () => {
}
if (!data || data.posts.length === 0) {
- return
No posts available
;
+ return
{t("No posts available")}
;
}
return (
@@ -72,7 +74,7 @@ export const BlogDropdown: React.FC = () => {
- View all posts
+ {t("View all posts")}
>
@@ -87,8 +89,7 @@ export const BlogDropdown: React.FC = () => {
closeDelay={100}
render={
}
>
- Blog
-
+ {t("Blog")}
{renderMenuContent()}
diff --git a/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.tsx b/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.tsx
index 9b4f4c90b64..21118f6ff5d 100644
--- a/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/DocsLink/DocsLink.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "@/i18n/useTranslation";
import { NAV_PRODUCT_LINK_CLASS } from "@/components/Navbar/navProductLinkClass";
import { ChevronDown } from "lucide-react";
import React from "react";
@@ -8,11 +9,13 @@ const ChevronWidthSpacer: React.FC = () => (
);
-export const DocsLink: React.FC = () => (
-
- Docs
-
-
-);
+export const DocsLink: React.FC = () => {
+ const { t } = useTranslation();
+ return (
+
+ {t("Docs")}
+
+ );
+};
export default DocsLink;
diff --git a/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx b/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx
index 8b9b7d1d0d2..f04ca7cad1b 100644
--- a/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/NotificationsBell/NotificationsBell.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useTranslation } from "@/i18n/useTranslation";
import {
HIDE_AUTO_ROUTER_ANNOUNCEMENT_KEY,
useHideAutoRouterAnnouncement,
@@ -15,6 +16,7 @@ import React, { useState } from "react";
export const AUTO_ROUTER_DOCS_URL = "https://docs.litellm.ai/docs/proxy/auto_routing";
export const NotificationsBell: React.FC = () => {
+ const { t } = useTranslation();
const hidden = useHideAutoRouterAnnouncement();
const hasUnread = !hidden;
const [open, setOpen] = useState(false);
@@ -27,9 +29,9 @@ export const NotificationsBell: React.FC = () => {
const content = (
-
LiteLLM Auto Router
+
{t("LiteLLM Auto Router")}
- Route every request to the cheapest model that can handle it, no prompt changes needed.
+ {t("Route every request to the cheapest model that can handle it, no prompt changes needed.")}
@@ -53,7 +55,7 @@ export const NotificationsBell: React.FC = () => {
diff --git a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx
index 95f76dbb2cc..3c207579303 100644
--- a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "@/i18n/useTranslation";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts";
import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon";
@@ -64,6 +65,7 @@ interface UserDropdownProps {
const UserDropdown: React.FC = ({ onLogout, variant = "navbar", collapsed = false }) => {
const { userId, userEmail, userRoleLabel: userRole, premiumUser } = useAuthorized();
+ const { t } = useTranslation();
const disableShowPrompts = useDisableShowPrompts();
const disableBlogPosts = useDisableBlogPosts();
const disableBouncingIcon = useDisableBouncingIcon();
@@ -84,16 +86,16 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
{premiumUser ? (
- Premium
+ {t("Premium")}
) : (
}>
- Standard
+ {t("Standard")}
- Upgrade to Premium for advanced features
+ {t("Upgrade to Premium for advanced features")}
)}
@@ -102,25 +104,25 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
- User ID
+ {t("User ID")}
{userId || "-"}
-
+
- Role
+ {t("Role")}
{userRole}
- Hide New Feature Indicators
+ {t("Hide New Feature Indicators")}
= ({ onLogout, variant = "navbar
emitLocalStorageChange("disableShowNewBadge");
}
}}
- aria-label="Toggle hide new feature indicators"
+ aria-label={t("Toggle hide new feature indicators")}
/>
- Hide All Prompts
+ {t("Hide All Prompts")}
= ({ onLogout, variant = "navbar
emitLocalStorageChange("disableShowPrompts");
}
}}
- aria-label="Toggle hide all prompts"
+ aria-label={t("Toggle hide all prompts")}
/>
- Hide Blog Posts
+ {t("Hide Blog Posts")}
= ({ onLogout, variant = "navbar
emitLocalStorageChange("disableBlogPosts");
}
}}
- aria-label="Toggle hide blog posts"
+ aria-label={t("Toggle hide blog posts")}
/>
- Hide Bouncing Icon
+ {t("Hide Bouncing Icon")}
= ({ onLogout, variant = "navbar
emitLocalStorageChange("disableBouncingIcon");
}
}}
- aria-label="Toggle hide bouncing icon"
+ aria-label={t("Toggle hide bouncing icon")}
/>
@@ -194,7 +196,7 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
const seed = userEmail || userId || "user";
const initials = initialsFromIdentity(userEmail, userId);
const hue = hueFromString(seed);
- const displayName = navAccountDisplayName(userEmail, userId);
+ const displayName = navAccountDisplayName(userEmail, userId, t("Account"));
return (
@@ -207,7 +209,10 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
"flex w-full items-center rounded-lg border border-transparent transition-colors hover:bg-sidebar-accent",
collapsed ? "justify-center px-0 py-1" : "gap-2.5 px-2 py-1.5 text-left",
)}
- aria-label={`Account menu — ${userRole ?? "Unknown role"} — signed in as ${userEmail || userId || "unknown"}`}
+ aria-label={t("accountMenu", {
+ role: userRole ?? t("Unknown role"),
+ identity: userEmail || userId || t("unknown"),
+ })}
aria-haspopup="dialog"
title={collapsed ? displayName : undefined}
/>
@@ -234,7 +239,10 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
}
@@ -264,7 +272,7 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
>
- Logout
+ {t("Logout")}
diff --git a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx
index b3aba7155d1..5de3420f72b 100644
--- a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "@/i18n/useTranslation";
import React from "react";
import { usePathname } from "next/navigation";
import {
@@ -22,6 +23,7 @@ interface ViewSwitcherItem {
}
export default function ViewSwitcher() {
+ const { t } = useTranslation();
const { mode, setMode, plugins } = usePluginMode();
const { data: uiSettings } = useUISettings();
const pathname = usePathname();
@@ -32,10 +34,10 @@ export default function ViewSwitcher() {
const normalizedPathname = (pathname ?? "").replace(/\/+$/, "");
const isChatRoute = chatEnabled && (normalizedPathname === chatHref || normalizedPathname.startsWith(`${chatHref}/`));
- const activeLabel = isChatRoute ? "Chat" : plugins.find((p) => p.name === mode)?.display_name ?? "AI Gateway";
+ const activeLabel = isChatRoute ? t("Chat") : plugins.find((p) => p.name === mode)?.display_name ?? t("AI Gateway");
const modeEntries = [
- { key: GATEWAY, label: "AI Gateway" },
+ { key: GATEWAY, label: t("AI Gateway") },
...plugins.map((p) => ({ key: p.name, label: p.display_name })),
];
@@ -53,7 +55,7 @@ export default function ViewSwitcher() {
key: CHAT,
label: (
- Chat
+ {t("Chat")}
{isChatRoute && }
),
@@ -64,9 +66,9 @@ export default function ViewSwitcher() {
disabled: true,
label: (
- Chat
+ {t("Chat")}
- Admins can enable in Settings
+ {t("Admins can enable in Settings")}
),
diff --git a/ui/litellm-dashboard/src/components/Navbar/navDisplayName.test.ts b/ui/litellm-dashboard/src/components/Navbar/navDisplayName.test.ts
index 96e768c5d31..f66a4b75ad0 100644
--- a/ui/litellm-dashboard/src/components/Navbar/navDisplayName.test.ts
+++ b/ui/litellm-dashboard/src/components/Navbar/navDisplayName.test.ts
@@ -15,3 +15,9 @@ describe("navAccountDisplayName", () => {
expect(navAccountDisplayName(null, "user-uuid-123")).toBe("user-uuid-123");
});
});
+
+it("localizes only the placeholder account label, preserving real account identities", () => {
+ expect(navAccountDisplayName(null, "default_user_id", "账户")).toBe("账户");
+ expect(navAccountDisplayName(null, "Account", "账户")).toBe("Account");
+ expect(navAccountDisplayName("admin@example.com", null, "账户")).toBe("admin@example.com");
+});
diff --git a/ui/litellm-dashboard/src/components/Navbar/navDisplayName.ts b/ui/litellm-dashboard/src/components/Navbar/navDisplayName.ts
index d6f51dc37a8..deb90ec53fb 100644
--- a/ui/litellm-dashboard/src/components/Navbar/navDisplayName.ts
+++ b/ui/litellm-dashboard/src/components/Navbar/navDisplayName.ts
@@ -1,15 +1,15 @@
/** Primary label for the navbar account control — avoids raw placeholder JWT/user IDs in the UI. */
-export function navAccountDisplayName(userEmail: string | null, userId: string | null): string {
+export function navAccountDisplayName(userEmail: string | null, userId: string | null, fallback = "Account"): string {
const email = userEmail?.trim();
if (email) {
return email;
}
const id = userId?.trim();
if (!id) {
- return "Account";
+ return fallback;
}
if (/^default[_\s-]?user[_\s-]?id$/i.test(id)) {
- return "Account";
+ return fallback;
}
return id;
}
diff --git a/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx
index ea0b82869cd..335e3457121 100644
--- a/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx
+++ b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "@/i18n/useTranslation";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails";
import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts";
@@ -82,6 +83,7 @@ interface SidebarAccountMenuProps {
const SidebarAccountMenu: React.FC = ({ onLogout, collapsed = false }) => {
const { userId, userEmail, userRoleLabel: userRole, premiumUser, accessToken } = useAuthorized();
+ const { t } = useTranslation();
const { data: healthData } = useHealthReadinessDetails(accessToken);
const version = healthData?.litellm_version;
const disableShowPrompts = useDisableShowPrompts();
@@ -101,29 +103,29 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla
const toggles = [
{
key: "disableShowNewBadge",
- label: "Hide New Feature Indicators",
- ariaLabel: "Toggle hide new feature indicators",
+ label: t("Hide New Feature Indicators"),
+ ariaLabel: t("Toggle hide new feature indicators"),
checked: disableShowNewBadge,
onCheckedChange: (checked: boolean) => setFlag("disableShowNewBadge", checked),
},
{
key: "disableShowPrompts",
- label: "Hide All Prompts",
- ariaLabel: "Toggle hide all prompts",
+ label: t("Hide All Prompts"),
+ ariaLabel: t("Toggle hide all prompts"),
checked: disableShowPrompts,
onCheckedChange: (checked: boolean) => setFlag("disableShowPrompts", checked),
},
{
key: "disableBlogPosts",
- label: "Hide Blog Posts",
- ariaLabel: "Toggle hide blog posts",
+ label: t("Hide Blog Posts"),
+ ariaLabel: t("Toggle hide blog posts"),
checked: disableBlogPosts,
onCheckedChange: (checked: boolean) => setFlag("disableBlogPosts", checked),
},
{
key: "disableBouncingIcon",
- label: "Hide Bouncing Icon",
- ariaLabel: "Toggle hide bouncing icon",
+ label: t("Hide Bouncing Icon"),
+ ariaLabel: t("Toggle hide bouncing icon"),
checked: disableBouncingIcon,
onCheckedChange: (checked: boolean) => setFlag("disableBouncingIcon", checked),
},
@@ -132,8 +134,11 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla
const seed = userEmail || userId || "user";
const initials = initialsFromIdentity(userEmail, userId);
const hue = hueFromString(seed);
- const displayName = navAccountDisplayName(userEmail, userId);
- const triggerLabel = `Account menu — ${userRole ?? "Unknown role"} — signed in as ${userEmail || userId || "unknown"}`;
+ const displayName = navAccountDisplayName(userEmail, userId, t("Account"));
+ const triggerLabel = t("accountMenu", {
+ role: userRole ?? t("Unknown role"),
+ identity: userEmail || userId || t("unknown"),
+ });
return (
@@ -174,7 +179,7 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla
🌴
@@ -193,27 +198,27 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla
- } label="Tier">
+ } label={t("Tier")}>
{premiumUser ? (
- Premium
+ {t("Premium")}
) : (
-
+
- Standard
+ {t("Standard")}
)}
- } label="Role">
+ } label={t("Role")}>
{userRole}
- } label="Email">
-
+ } label={t("Email")}>
+
- } label="User ID">
-
+ } label={t("User ID")}>
+
@@ -241,7 +246,7 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla
className="h-[42px] w-full justify-start gap-2.5 rounded-none px-3 text-sm font-medium text-foreground"
>
- Logout
+ {t("Logout")}
diff --git a/ui/litellm-dashboard/src/components/ThemeToggle/ThemeToggle.tsx b/ui/litellm-dashboard/src/components/ThemeToggle/ThemeToggle.tsx
index 2f941dbc2d2..ac1a6342356 100644
--- a/ui/litellm-dashboard/src/components/ThemeToggle/ThemeToggle.tsx
+++ b/ui/litellm-dashboard/src/components/ThemeToggle/ThemeToggle.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useTranslation } from "@/i18n/useTranslation";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import React from "react";
@@ -7,9 +8,10 @@ import React from "react";
import { Button } from "@/components/ui/button";
const ThemeToggle: React.FC = () => {
+ const { t } = useTranslation();
const { setTheme, resolvedTheme } = useTheme();
const isDark = resolvedTheme === "dark";
- const label = isDark ? "Switch to light mode" : "Switch to dark mode (beta)";
+ const label = isDark ? t("Switch to light mode") : t("Switch to dark mode (beta)");
return (
🚅 LiteLLM
- Loading...
+ {t("Loading...")}
);
diff --git a/ui/litellm-dashboard/src/components/leftnav.test.tsx b/ui/litellm-dashboard/src/components/leftnav.test.tsx
index 6eb0218c41d..665aa182616 100644
--- a/ui/litellm-dashboard/src/components/leftnav.test.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.test.tsx
@@ -1,5 +1,7 @@
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
+import { I18nextProvider } from "react-i18next";
+import { createDashboardI18n } from "@/i18n";
import { renderWithProviders } from "../../tests/test-utils";
import Sidebar, { menuGroups, getBreadcrumb } from "./leftnav";
@@ -622,3 +624,37 @@ describe("getBreadcrumb", () => {
expect(getBreadcrumb("/ui/some-unknown-page")).toEqual({ section: null, title: "Some Unknown Page" });
});
});
+
+describe("localized navigation", () => {
+ it("changes visible and collapsed labels while retaining routes and permission filtering", async () => {
+ const internalUser = {
+ userId: "test-user-id",
+ accessToken: "test-access-token",
+ userRole: "internal",
+ isViewOnly: false,
+ token: "test-token",
+ userEmail: "test@example.com",
+ premiumUser: false,
+ disabledPersonalKeyCreation: false,
+ showSSOBanner: false,
+ };
+ mockUseAuthorized.mockReturnValue(internalUser);
+ const instance = createDashboardI18n("zh-CN");
+ renderWithProviders(
+
+
+ ,
+ );
+ expect(screen.getByRole("link", { name: "虚拟密钥" })).toHaveAttribute("href", "/ui/api-keys");
+ expect(screen.getByRole("link", { name: "虚拟密钥" })).toHaveAttribute("title", "虚拟密钥");
+ expect(screen.getByRole("link", { name: "模型与端点" })).toHaveAttribute("href", "/ui/models-and-endpoints");
+ expect(screen.queryByRole("link", { name: "内部用户" })).not.toBeInTheDocument();
+ await act(async () => {
+ await instance.changeLanguage("en");
+ });
+ expect(screen.getByRole("link", { name: "Virtual Keys" })).toHaveAttribute("href", "/ui/api-keys");
+ expect(screen.getByRole("link", { name: "Virtual Keys" })).toHaveAttribute("title", "Virtual Keys");
+ expect(screen.queryByRole("link", { name: "Internal Users" })).not.toBeInTheDocument();
+ expect(menuGroups[0].items[0].label).toBe("Virtual Keys");
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx
index 9d772f45153..0b4f833a9d2 100644
--- a/ui/litellm-dashboard/src/components/leftnav.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "@/i18n/useTranslation";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useIsOrgAdmin from "@/app/(dashboard)/hooks/useIsOrgAdmin";
@@ -102,6 +103,7 @@ interface MenuItem {
roles?: string[];
children?: MenuItem[];
icon?: React.ReactNode;
+ badge?: React.ReactNode;
external_url?: string;
}
@@ -209,11 +211,8 @@ const menuGroups: MenuGroup[] = [
page: "cost-optimization",
icon: ,
roles: [...all_admin_roles, ...internalUserRoles],
- label: (
-
- Cost Optimization
-
- ),
+ label: "Cost Optimization",
+ badge: ,
},
{ key: "logs", page: "logs", label: "Logs", icon: },
{
@@ -232,11 +231,8 @@ const menuGroups: MenuGroup[] = [
{
key: "projects",
page: "projects",
- label: (
-
- Projects
-
- ),
+ label: "Projects",
+ badge: ,
icon: ,
roles: all_admin_roles,
},
@@ -428,6 +424,7 @@ const Sidebar_: React.FC = ({
disableVectorStoresForInternalUsers,
allowVectorStoresForTeamAdmins,
}) => {
+ const { t } = useTranslation();
const { userId, accessToken, userRole, isViewOnly } = useAuthorized();
const isOrgAdmin = useIsOrgAdmin();
const { data: teams } = useTeams();
@@ -521,10 +518,24 @@ const Sidebar_: React.FC = ({
});
};
+ const translatedLabel = (item: MenuItem) => {
+ const label = typeof item.label === "string" ? t(item.label) : item.label;
+ return item.badge ? (
+
+ {label}
+ {item.badge}
+
+ ) : (
+ label
+ );
+ };
+
const renderLeaf = (item: MenuItem, isChild: boolean) => {
const active = selectedKey === item.key;
const size = isChild ? "sub" : "default";
- const label = {item.label} ;
+ const label = (
+ {translatedLabel(item)}
+ );
if (item.external_url) {
return (
@@ -533,7 +544,7 @@ const Sidebar_: React.FC = ({
href={item.external_url}
target="_blank"
rel="noopener noreferrer"
- title={collapsed ? labelText(item) : undefined}
+ title={collapsed ? t(labelText(item)) : undefined}
data-active={active || undefined}
className={cn(sidebarMenuButtonVariants({ isActive: active, size }))}
>
@@ -548,7 +559,7 @@ const Sidebar_: React.FC = ({
@@ -572,10 +583,10 @@ const Sidebar_: React.FC = ({
isActive={active}
aria-expanded={open}
onClick={() => toggleGroup(item.key)}
- title={collapsed ? labelText(item) : undefined}
+ title={collapsed ? t(labelText(item)) : undefined}
>
{item.icon}
- {item.label}
+ {translatedLabel(item)}
= ({
-
+
= ({
variant="ghost"
size="icon-sm"
onClick={onToggleCollapsed}
- aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
+ aria-label={collapsed ? t("Expand sidebar") : t("Collapse sidebar")}
className="flex-none text-muted-foreground"
>
{collapsed ?
:
}
@@ -642,7 +653,7 @@ const Sidebar_: React.FC
= ({
{visibleGroups.map((group, gi) => (
{gi > 0 && }
- {group.groupLabel}
+ {t(group.groupLabel)}
{group.items.map((item) => renderItem(item))}
))}
diff --git a/ui/litellm-dashboard/src/components/navbar.tsx b/ui/litellm-dashboard/src/components/navbar.tsx
index feba3e16d4c..62dea018ccb 100644
--- a/ui/litellm-dashboard/src/components/navbar.tsx
+++ b/ui/litellm-dashboard/src/components/navbar.tsx
@@ -1,3 +1,5 @@
+import LanguageSwitcher from "@/components/LanguageSwitcher";
+import { useTranslation } from "@/i18n/useTranslation";
import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails";
import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon";
import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts";
@@ -38,6 +40,7 @@ const Navbar: React.FC = ({
onToggleSidebar,
}) => {
const baseUrl = getProxyBaseUrl();
+ const { t } = useTranslation();
const proxySettings = useProxySettings(accessToken);
const { logoUrl } = useTheme();
const { data: healthData } = useHealthReadinessDetails(accessToken);
@@ -74,7 +77,7 @@ const Navbar: React.FC = ({
{sidebarCollapsed ? (
@@ -106,7 +109,7 @@ const Navbar: React.FC = ({
🌑
@@ -140,7 +143,7 @@ const Navbar: React.FC = ({
)}
@@ -156,6 +159,7 @@ const Navbar: React.FC = ({
{!isPublicPage && (
+
diff --git a/ui/litellm-dashboard/src/components/shared/PasswordInput.tsx b/ui/litellm-dashboard/src/components/shared/PasswordInput.tsx
index e92385de8be..962201e4d77 100644
--- a/ui/litellm-dashboard/src/components/shared/PasswordInput.tsx
+++ b/ui/litellm-dashboard/src/components/shared/PasswordInput.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useTranslation } from "@/i18n/useTranslation";
import { Eye, EyeOff } from "lucide-react";
import * as React from "react";
@@ -12,6 +13,7 @@ export type PasswordInputProps = Omit
, "
export const PasswordInput = React.forwardRef(
({ className, groupClassName, disabled, ...props }, ref) => {
const [revealed, setRevealed] = React.useState(false);
+ const { t } = useTranslation();
return (
@@ -26,7 +28,7 @@ export const PasswordInput = React.forwardRef setRevealed((current) => !current)}
>
{revealed ? : }
diff --git a/ui/litellm-dashboard/src/i18n/LanguageProvider.integration.test.tsx b/ui/litellm-dashboard/src/i18n/LanguageProvider.integration.test.tsx
new file mode 100644
index 00000000000..f37abd1390c
--- /dev/null
+++ b/ui/litellm-dashboard/src/i18n/LanguageProvider.integration.test.tsx
@@ -0,0 +1,82 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { renderToString } from "react-dom/server";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import LanguageSwitcher from "@/components/LanguageSwitcher";
+import LoadingScreen from "@/components/common_components/LoadingScreen";
+import { PasswordInput } from "@/components/shared/PasswordInput";
+import LanguageProvider from "./LanguageProvider";
+import { LANGUAGE_STORAGE_KEY } from ".";
+
+function LanguageExample() {
+ return (
+
+
+
+
+
+ );
+}
+
+async function selectChinese() {
+ const user = userEvent.setup();
+ await user.click(screen.getByRole("button", { name: "Language" }));
+ await user.click(await screen.findByRole("menuitemradio", { name: "简体中文" }));
+}
+
+beforeEach(() => {
+ window.localStorage.clear();
+});
+afterEach(() => {
+ vi.restoreAllMocks();
+ window.localStorage.clear();
+});
+
+describe("dashboard language preference", () => {
+ it("switches all consumers, preserves password fields, and restores the explicit preference after remount", async () => {
+ const user = userEvent.setup();
+ const first = render( );
+ await user.type(screen.getByLabelText("API key"), "opaque-value");
+ await selectChinese();
+ expect(screen.getByText("加载中…")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "显示密码" })).toBeInTheDocument();
+ expect(screen.getByLabelText("API key")).toHaveValue("opaque-value");
+ expect(document.documentElement).toHaveAttribute("lang", "zh-CN");
+ expect(window.localStorage.getItem(LANGUAGE_STORAGE_KEY)).toBe("zh-CN");
+ first.unmount();
+ render( );
+ expect(await screen.findByRole("button", { name: "语言" })).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "语言" }));
+ expect(await screen.findByRole("menuitemradio", { name: "简体中文" })).toBeChecked();
+ await user.click(await screen.findByRole("menuitemradio", { name: "English" }));
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
+ expect(document.documentElement).toHaveAttribute("lang", "en");
+ expect(window.localStorage.getItem(LANGUAGE_STORAGE_KEY)).toBe("en");
+ });
+
+ it("renders English on the server even when the browser has a Chinese preference", () => {
+ window.localStorage.setItem(LANGUAGE_STORAGE_KEY, "zh-CN");
+ expect(renderToString( )).toContain("Loading...");
+ });
+
+ it("ignores invalid stored values without changing storage or the default UI", () => {
+ window.localStorage.setItem(LANGUAGE_STORAGE_KEY, "fr");
+ render( );
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
+ expect(document.documentElement).toHaveAttribute("lang", "en");
+ expect(window.localStorage.getItem(LANGUAGE_STORAGE_KEY)).toBe("fr");
+ });
+
+ it("remains usable when browser storage throws on both reads and writes", async () => {
+ vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
+ throw new DOMException("Blocked", "SecurityError");
+ });
+ vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
+ throw new DOMException("Blocked", "SecurityError");
+ });
+ render( );
+ await selectChinese();
+ expect(screen.getByText("加载中…")).toBeInTheDocument();
+ expect(document.documentElement).toHaveAttribute("lang", "zh-CN");
+ });
+});
diff --git a/ui/litellm-dashboard/src/i18n/LanguageProvider.tsx b/ui/litellm-dashboard/src/i18n/LanguageProvider.tsx
new file mode 100644
index 00000000000..de9efbb532c
--- /dev/null
+++ b/ui/litellm-dashboard/src/i18n/LanguageProvider.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import { useEffect, useState, type ReactNode } from "react";
+import { I18nextProvider } from "react-i18next";
+import { createDashboardI18n, isLanguage, LANGUAGE_STORAGE_KEY } from ".";
+
+export default function LanguageProvider({ children }: { children: ReactNode }) {
+ const [instance] = useState(createDashboardI18n);
+
+ useEffect(() => {
+ const updateDocumentLanguage = (language: string) => {
+ document.documentElement.lang = language;
+ };
+ instance.on("languageChanged", updateDocumentLanguage);
+ try {
+ const stored = window.localStorage.getItem(LANGUAGE_STORAGE_KEY);
+ if (isLanguage(stored)) void instance.changeLanguage(stored);
+ } catch {
+ // Storage can be disabled by the browser; language selection still works in memory
+ }
+ updateDocumentLanguage(instance.language);
+ return () => {
+ instance.off("languageChanged", updateDocumentLanguage);
+ };
+ }, [instance]);
+
+ return {children} ;
+}
diff --git a/ui/litellm-dashboard/src/i18n/README.md b/ui/litellm-dashboard/src/i18n/README.md
new file mode 100644
index 00000000000..2b8692d0339
--- /dev/null
+++ b/ui/litellm-dashboard/src/i18n/README.md
@@ -0,0 +1,13 @@
+# Dashboard translations
+
+The dashboard defaults to English. Select 简体中文 from the language menu on the login page or dashboard header to switch to Simplified Chinese. The explicit choice is saved in `localStorage.litellm_ui_language`. Unsupported stored values fall back to English. When browser storage is unavailable, the selection works until the page reloads
+
+This initial coverage includes login and validation, the sidebar and collapsed navigation labels, breadcrumbs, view switching, account controls, and header controls. Dashboard page bodies, server error messages, worker and plugin names, and role labels are outside this change
+
+Translations are bundled with the static export using i18next and react-i18next. Each mounted `LanguageProvider` owns its instance, starts in English for server rendering and hydration, then restores the stored preference after mounting. Changing language updates the HTML `lang` attribute without changing URLs or remounting the form. No translation service or language-specific backend is required
+
+Use `useTranslation` from `@/i18n/useTranslation` for React components. It uses the root provider, with an isolated English fallback for standalone components. Add the same key to `locales/en.json` and `locales/zh-CN.json`. Plain display text serves as its own key; longer sentences with markup use descriptive keys and `Trans`. Key and namespace separators are disabled, so punctuation in display text stays literal
+
+Translate labels at render time. Keep identifiers, URLs, form field names, permission checks, plugin names, and API payloads unchanged. Use interpolation for user values and `Trans` for inline links or code, rather than joining translated fragments. React escapes interpolated display values
+
+Chinese terminology adapts the MIT-licensed locale contributed by [2099383411 in PR #30499](https://github.com/BerriAI/litellm/pull/30499) at `e787b318a932c9c6b897830f6d01045f05bbd0a8`, with additions for the current Base UI dashboard. That PR's Ant Design integration is not included here
diff --git a/ui/litellm-dashboard/src/i18n/index.test.ts b/ui/litellm-dashboard/src/i18n/index.test.ts
new file mode 100644
index 00000000000..224dc1b601f
--- /dev/null
+++ b/ui/litellm-dashboard/src/i18n/index.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, it } from "vitest";
+import { createDashboardI18n, isLanguage } from ".";
+import en from "./locales/en.json";
+import zhCN from "./locales/zh-CN.json";
+
+describe("dashboard translations", () => {
+ it("keeps complete English and Chinese catalogs with matching interpolation fields and markup", () => {
+ expect(Object.keys(zhCN).sort()).toEqual(Object.keys(en).sort());
+ for (const key of Object.keys(en) as (keyof typeof en)[]) {
+ expect(zhCN[key].trim(), key).not.toBe("");
+ expect(zhCN[key].match(/{{\w+}}|<\/?\w+>/g) ?? [], key).toEqual(en[key].match(/{{\w+}}|<\/?\w+>/g) ?? []);
+ }
+ });
+
+ it("preserves punctuation in natural-language keys and falls back to English", () => {
+ const instance = createDashboardI18n("zh-CN");
+ expect(instance.t("Access your LiteLLM Admin UI.")).toBe("登录 LiteLLM 管理界面。");
+ instance.addResource("en", "translation", "English fallback.", "English fallback.");
+ expect(instance.t("English fallback.")).toBe("English fallback.");
+ expect(instance.t("An untranslated label")).toBe("An untranslated label");
+ });
+
+ it("keeps server-rendered and standalone instances independent of another user's selection", async () => {
+ const first = createDashboardI18n();
+ const second = createDashboardI18n();
+ await first.changeLanguage("zh-CN");
+ expect(first.t("Login")).toBe("登录");
+ expect(second.t("Login")).toBe("Login");
+ });
+
+ it("only accepts the supported explicit preferences", () => {
+ expect(["en", "zh-CN"].every(isLanguage)).toBe(true);
+ expect([null, undefined, "", "fr", "zh-TW", "__proto__"].some(isLanguage)).toBe(false);
+ });
+});
diff --git a/ui/litellm-dashboard/src/i18n/index.ts b/ui/litellm-dashboard/src/i18n/index.ts
new file mode 100644
index 00000000000..ff895316046
--- /dev/null
+++ b/ui/litellm-dashboard/src/i18n/index.ts
@@ -0,0 +1,27 @@
+import { createInstance, type InitOptions } from "i18next";
+import en from "./locales/en.json";
+import zhCN from "./locales/zh-CN.json";
+
+export type Language = "en" | "zh-CN";
+export const LANGUAGE_STORAGE_KEY = "litellm_ui_language";
+
+export function isLanguage(value: unknown): value is Language {
+ return value === "en" || value === "zh-CN";
+}
+
+export function createDashboardI18n(language: Language = "en") {
+ const instance = createInstance();
+ const options: InitOptions = {
+ lng: language,
+ fallbackLng: "en",
+ supportedLngs: ["en", "zh-CN"],
+ resources: { en: { translation: { ...en } }, "zh-CN": { translation: { ...zhCN } } },
+ initAsync: false,
+ keySeparator: false,
+ nsSeparator: false,
+ interpolation: { escapeValue: false },
+ react: { useSuspense: false },
+ };
+ void instance.init(options);
+ return instance;
+}
diff --git a/ui/litellm-dashboard/src/i18n/locales/en.json b/ui/litellm-dashboard/src/i18n/locales/en.json
new file mode 100644
index 00000000000..4886c58c131
--- /dev/null
+++ b/ui/litellm-dashboard/src/i18n/locales/en.json
@@ -0,0 +1,115 @@
+{
+ "ACCESS CONTROL": "ACCESS CONTROL",
+ "AI GATEWAY": "AI GATEWAY",
+ "AI Gateway": "AI Gateway",
+ "AI Hub": "AI Hub",
+ "API Playground": "API Playground",
+ "API Reference": "API Reference",
+ "Access Groups": "Access Groups",
+ "Access your LiteLLM Admin UI.": "Access your LiteLLM Admin UI.",
+ "Account": "Account",
+ "Admin Settings": "Admin Settings",
+ "Admin UI Disabled": "Admin UI Disabled",
+ "Admins can enable in Settings": "Admins can enable in Settings",
+ "Agentic": "Agentic",
+ "Agents": "Agents",
+ "Blog": "Blog",
+ "Budgets": "Budgets",
+ "Chat": "Chat",
+ "Choose a worker to connect to": "Choose a worker to connect to",
+ "Close": "Close",
+ "Collapse sidebar": "Collapse sidebar",
+ "Copy User ID": "Copy User ID",
+ "Copy email": "Copy email",
+ "Copy user ID": "Copy user ID",
+ "Cost Optimization": "Cost Optimization",
+ "Cost Tracking": "Cost Tracking",
+ "DEVELOPER TOOLS": "DEVELOPER TOOLS",
+ "Default Credentials": "Default Credentials",
+ "Docs": "Docs",
+ "Email": "Email",
+ "Enter your password": "Enter your password",
+ "Enter your username": "Enter your username",
+ "Expand sidebar": "Expand sidebar",
+ "Experimental": "Experimental",
+ "Failed to load posts": "Failed to load posts",
+ "Guardrails": "Guardrails",
+ "Guardrails Monitor": "Guardrails Monitor",
+ "Hide All Prompts": "Hide All Prompts",
+ "Hide Blog Posts": "Hide Blog Posts",
+ "Hide Bouncing Icon": "Hide Bouncing Icon",
+ "Hide New Feature Indicators": "Hide New Feature Indicators",
+ "Hide password": "Hide password",
+ "Internal Users": "Internal Users",
+ "Language": "Language",
+ "Learning Resources": "Learning Resources",
+ "LiteLLM Auto Router": "LiteLLM Auto Router",
+ "LiteLLM home": "LiteLLM home",
+ "Loading...": "Loading...",
+ "Logging & Alerts": "Logging & Alerts",
+ "Logging in...": "Logging in...",
+ "Login": "Login",
+ "Login with SSO": "Login with SSO",
+ "Logout": "Logout",
+ "Logs": "Logs",
+ "MCP Servers": "MCP Servers",
+ "Mark as read": "Mark as read",
+ "Memory": "Memory",
+ "Models + Endpoints": "Models + Endpoints",
+ "No posts available": "No posts available",
+ "Notifications": "Notifications",
+ "OBSERVABILITY": "OBSERVABILITY",
+ "Old Usage": "Old Usage",
+ "Organizations": "Organizations",
+ "Password": "Password",
+ "Playground": "Playground",
+ "Please configure SSO to log in with SSO.": "Please configure SSO to log in with SSO.",
+ "Please enter your password": "Please enter your password",
+ "Please enter your username": "Please enter your username",
+ "Policies": "Policies",
+ "Premium": "Premium",
+ "Product documentation": "Product documentation",
+ "Projects": "Projects",
+ "Prompts": "Prompts",
+ "Read the docs": "Read the docs",
+ "Response Cache": "Response Cache",
+ "Role": "Role",
+ "Route every request to the cheapest model that can handle it, no prompt changes needed.": "Route every request to the cheapest model that can handle it, no prompt changes needed.",
+ "Router Settings": "Router Settings",
+ "SETTINGS": "SETTINGS",
+ "Search Tools": "Search Tools",
+ "Settings": "Settings",
+ "Show password": "Show password",
+ "Skills": "Skills",
+ "Standard": "Standard",
+ "Switch to dark mode (beta)": "Switch to dark mode (beta)",
+ "Switch to light mode": "Switch to light mode",
+ "Tag Management": "Tag Management",
+ "Teams": "Teams",
+ "Thanks for using LiteLLM!": "Thanks for using LiteLLM!",
+ "The Admin UI has been disabled by the administrator. To re-enable it, please update the following environment variable:": "The Admin UI has been disabled by the administrator. To re-enable it, please update the following environment variable:",
+ "Tier": "Tier",
+ "Toggle hide all prompts": "Toggle hide all prompts",
+ "Toggle hide blog posts": "Toggle hide blog posts",
+ "Toggle hide bouncing icon": "Toggle hide bouncing icon",
+ "Toggle hide new feature indicators": "Toggle hide new feature indicators",
+ "Tool Policies": "Tool Policies",
+ "Tools": "Tools",
+ "UI Theme": "UI Theme",
+ "Unknown role": "Unknown role",
+ "Upgrade to Premium for advanced features": "Upgrade to Premium for advanced features",
+ "Usage": "Usage",
+ "User ID": "User ID",
+ "Username": "Username",
+ "Vector Stores": "Vector Stores",
+ "View all posts": "View all posts",
+ "Virtual Keys": "Virtual Keys",
+ "Worker": "Worker",
+ "Workflow Runs": "Workflow Runs",
+ "accountMenu": "Account menu — {{role}} — signed in as {{identity}}",
+ "loading": "loading",
+ "login.credentialsHelp": "Need to set UI credentials or SSO? Check the documentation .",
+ "login.defaultCredentials": "By default, Username is admin and Password is your set LiteLLM Proxy MASTER_KEY.",
+ "login.ssoNotice": "Single Sign-On (SSO) is enabled. LiteLLM no longer automatically redirects to the SSO login flow upon loading this page. To re-enable auto-redirect-to-SSO, set AUTO_REDIRECT_UI_LOGIN_TO_SSO=true in your environment configuration.",
+ "unknown": "unknown"
+}
diff --git a/ui/litellm-dashboard/src/i18n/locales/zh-CN.json b/ui/litellm-dashboard/src/i18n/locales/zh-CN.json
new file mode 100644
index 00000000000..bf2706ad15c
--- /dev/null
+++ b/ui/litellm-dashboard/src/i18n/locales/zh-CN.json
@@ -0,0 +1,115 @@
+{
+ "ACCESS CONTROL": "访问控制",
+ "AI GATEWAY": "AI 网关",
+ "AI Gateway": "AI 网关",
+ "AI Hub": "AI 资源中心",
+ "API Playground": "API 试用",
+ "API Reference": "API 参考",
+ "Access Groups": "访问组",
+ "Access your LiteLLM Admin UI.": "登录 LiteLLM 管理界面。",
+ "Account": "账户",
+ "Admin Settings": "管理设置",
+ "Admin UI Disabled": "管理界面已禁用",
+ "Admins can enable in Settings": "管理员可在设置中启用",
+ "Agentic": "智能体应用",
+ "Agents": "智能体",
+ "Blog": "博客",
+ "Budgets": "预算",
+ "Chat": "聊天",
+ "Choose a worker to connect to": "选择要连接的工作节点",
+ "Close": "关闭",
+ "Collapse sidebar": "收起侧栏",
+ "Copy User ID": "复制用户 ID",
+ "Copy email": "复制邮箱",
+ "Copy user ID": "复制用户 ID",
+ "Cost Optimization": "成本优化",
+ "Cost Tracking": "成本追踪",
+ "DEVELOPER TOOLS": "开发工具",
+ "Default Credentials": "默认登录信息",
+ "Docs": "文档",
+ "Email": "邮箱",
+ "Enter your password": "请输入密码",
+ "Enter your username": "请输入用户名",
+ "Expand sidebar": "展开侧栏",
+ "Experimental": "实验功能",
+ "Failed to load posts": "加载文章失败",
+ "Guardrails": "安全护栏",
+ "Guardrails Monitor": "护栏监控",
+ "Hide All Prompts": "隐藏所有提示",
+ "Hide Blog Posts": "隐藏博客文章",
+ "Hide Bouncing Icon": "隐藏跳动图标",
+ "Hide New Feature Indicators": "隐藏新功能提示",
+ "Hide password": "隐藏密码",
+ "Internal Users": "内部用户",
+ "Language": "语言",
+ "Learning Resources": "学习资料",
+ "LiteLLM Auto Router": "LiteLLM 自动路由",
+ "LiteLLM home": "LiteLLM 首页",
+ "Loading...": "加载中…",
+ "Logging & Alerts": "日志与告警",
+ "Logging in...": "正在登录…",
+ "Login": "登录",
+ "Login with SSO": "使用单点登录",
+ "Logout": "退出登录",
+ "Logs": "请求日志",
+ "MCP Servers": "MCP 服务",
+ "Mark as read": "标记为已读",
+ "Memory": "记忆",
+ "Models + Endpoints": "模型与端点",
+ "No posts available": "暂无文章",
+ "Notifications": "通知",
+ "OBSERVABILITY": "运行监控",
+ "Old Usage": "旧版用量",
+ "Organizations": "组织",
+ "Password": "密码",
+ "Playground": "模型试用",
+ "Please configure SSO to log in with SSO.": "请先配置单点登录。",
+ "Please enter your password": "请输入密码",
+ "Please enter your username": "请输入用户名",
+ "Policies": "策略",
+ "Premium": "高级版",
+ "Product documentation": "产品文档",
+ "Projects": "项目",
+ "Prompts": "提示词",
+ "Read the docs": "查看文档",
+ "Response Cache": "响应缓存",
+ "Role": "角色",
+ "Route every request to the cheapest model that can handle it, no prompt changes needed.": "自动将每个请求分配给能够处理它的最低成本模型,无需修改提示词。",
+ "Router Settings": "路由设置",
+ "SETTINGS": "设置",
+ "Search Tools": "搜索工具",
+ "Settings": "设置",
+ "Show password": "显示密码",
+ "Skills": "技能",
+ "Standard": "标准版",
+ "Switch to dark mode (beta)": "切换到深色模式(测试版)",
+ "Switch to light mode": "切换到浅色模式",
+ "Tag Management": "标签管理",
+ "Teams": "团队",
+ "Thanks for using LiteLLM!": "感谢使用 LiteLLM!",
+ "The Admin UI has been disabled by the administrator. To re-enable it, please update the following environment variable:": "管理员已禁用管理界面。如需重新启用,请修改以下环境变量:",
+ "Tier": "套餐",
+ "Toggle hide all prompts": "切换所有提示显示",
+ "Toggle hide blog posts": "切换博客文章显示",
+ "Toggle hide bouncing icon": "切换跳动图标显示",
+ "Toggle hide new feature indicators": "切换新功能提示显示",
+ "Tool Policies": "工具策略",
+ "Tools": "工具",
+ "UI Theme": "界面主题",
+ "Unknown role": "未知角色",
+ "Upgrade to Premium for advanced features": "升级到高级版以使用更多功能",
+ "Usage": "用量统计",
+ "User ID": "用户 ID",
+ "Username": "用户名",
+ "Vector Stores": "向量存储",
+ "View all posts": "查看全部文章",
+ "Virtual Keys": "虚拟密钥",
+ "Worker": "工作节点",
+ "Workflow Runs": "工作流运行",
+ "accountMenu": "账户菜单:{{role}},当前登录为 {{identity}}",
+ "loading": "加载中",
+ "login.credentialsHelp": "需要设置管理界面登录凭据或单点登录?查看文档 。",
+ "login.defaultCredentials": "默认用户名为 admin,密码为你设置的 LiteLLM 网关 MASTER_KEY。",
+ "login.ssoNotice": "已启用单点登录(SSO)。打开此页面时,LiteLLM 不再自动跳转到单点登录流程。若要恢复自动跳转,请在环境配置中设置 AUTO_REDIRECT_UI_LOGIN_TO_SSO=true。",
+ "unknown": "未知"
+}
diff --git a/ui/litellm-dashboard/src/i18n/useTranslation.ts b/ui/litellm-dashboard/src/i18n/useTranslation.ts
new file mode 100644
index 00000000000..c3633dbe27c
--- /dev/null
+++ b/ui/litellm-dashboard/src/i18n/useTranslation.ts
@@ -0,0 +1,12 @@
+"use client";
+
+import { useContext } from "react";
+import { I18nContext, useTranslation as useReactTranslation } from "react-i18next";
+import { createDashboardI18n } from ".";
+
+const englishFallback = createDashboardI18n();
+
+export function useTranslation() {
+ const context = useContext(I18nContext);
+ return useReactTranslation(undefined, { i18n: context?.i18n ?? englishFallback });
+}
From eb999e069b03c60342bd343d73cb728011d5eace Mon Sep 17 00:00:00 2001
From: J_Sinclair <56301946+E-R-Butch@users.noreply.github.com>
Date: Mon, 7 Sep 2026 18:12:58 +0800
Subject: [PATCH 2/6] fix(ui): complete blog dropdown localization and address
review comments
---
.../src/components/LanguageSwitcher.tsx | 4 +-
.../BlogDropdown.integration.test.tsx | 78 +++++++++++++++++++
.../Navbar/BlogDropdown/BlogDropdown.tsx | 10 +--
.../src/i18n/LanguageProvider.tsx | 4 +-
ui/litellm-dashboard/src/i18n/locales/en.json | 1 +
.../src/i18n/locales/zh-CN.json | 1 +
6 files changed, 87 insertions(+), 11 deletions(-)
create mode 100644 ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx
diff --git a/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx b/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx
index a81a34fcb44..c95565e1869 100644
--- a/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx
+++ b/ui/litellm-dashboard/src/components/LanguageSwitcher.tsx
@@ -20,9 +20,7 @@ export default function LanguageSwitcher() {
void i18n.changeLanguage(language);
try {
window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language);
- } catch {
- // Storage can be disabled by the browser; keep the current selection usable
- }
+ } catch {}
};
return (
diff --git a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx
new file mode 100644
index 00000000000..fc69c1a59ea
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx
@@ -0,0 +1,78 @@
+import { act } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { I18nextProvider } from "react-i18next";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { createDashboardI18n } from "@/i18n";
+import { renderWithProviders, screen } from "../../../../tests/test-utils";
+import { BlogDropdown } from "./BlogDropdown";
+
+const mockRefetch = vi.fn();
+let mockIsError = false;
+
+vi.mock("@/app/(dashboard)/hooks/useDisableBlogPosts", () => ({
+ useDisableBlogPosts: () => false,
+}));
+
+vi.mock("@/app/(dashboard)/hooks/blogPosts/useBlogPosts", () => ({
+ useBlogPosts: () => ({
+ data: { posts: [{ title: "Post One", date: "2026-02-01", description: "Description one", url: "https://example.com/1" }] },
+ isLoading: false,
+ isError: mockIsError,
+ refetch: mockRefetch,
+ }),
+}));
+
+async function openDropdown() {
+ await userEvent.setup().hover(screen.getByRole("button", { name: "Blog" }));
+}
+
+describe("blog dropdown language changes", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockIsError = false;
+ });
+
+ it("should translate Retry after switching languages and still refetch posts", async () => {
+ mockIsError = true;
+ const user = userEvent.setup();
+ const i18n = createDashboardI18n();
+ renderWithProviders(
+
+
+ ,
+ );
+
+ await openDropdown();
+ expect(await screen.findByRole("button", { name: "Retry" })).toBeInTheDocument();
+ await act(async () => {
+ await i18n.changeLanguage("zh-CN");
+ });
+ expect(screen.getByText("加载文章失败")).toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: "Retry" })).not.toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "重试" }));
+ expect(mockRefetch).toHaveBeenCalledTimes(1);
+ });
+
+ it("should update visible dates when switching between English and Chinese", async () => {
+ const i18n = createDashboardI18n();
+ renderWithProviders(
+
+
+ ,
+ );
+
+ await openDropdown();
+ expect(await screen.findByText("Feb 1, 2026")).toBeInTheDocument();
+ await act(async () => {
+ await i18n.changeLanguage("zh-CN");
+ });
+ expect(screen.getByText("2026年2月1日")).toBeInTheDocument();
+ expect(screen.queryByText("Feb 1, 2026")).not.toBeInTheDocument();
+ expect(screen.getByText("Post One")).toBeInTheDocument();
+ await act(async () => {
+ await i18n.changeLanguage("en");
+ });
+ expect(screen.getByText("Feb 1, 2026")).toBeInTheDocument();
+ expect(screen.queryByText("2026年2月1日")).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx
index aa9eeb93cba..19880e07b16 100644
--- a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx
@@ -13,9 +13,9 @@ import {
import { ChevronDown, LoaderCircle } from "lucide-react";
import React from "react";
-function formatDate(dateStr: string): string {
+function formatDate(dateStr: string, language: string): string {
const date = new Date(dateStr + "T00:00:00");
- return date.toLocaleDateString("en-US", {
+ return date.toLocaleDateString(language === "zh-CN" ? "zh-CN" : "en-US", {
month: "short",
day: "numeric",
year: "numeric",
@@ -23,7 +23,7 @@ function formatDate(dateStr: string): string {
}
export const BlogDropdown: React.FC = () => {
- const { t } = useTranslation();
+ const { t, i18n } = useTranslation();
const disableBlogPosts = useDisableBlogPosts();
const { data, isLoading, isError, refetch } = useBlogPosts();
@@ -46,7 +46,7 @@ export const BlogDropdown: React.FC = () => {
{t("Failed to load posts")}
refetch()}>
- Retry
+ {t("Retry")}
);
@@ -65,7 +65,7 @@ export const BlogDropdown: React.FC = () => {
{post.title}
- {formatDate(post.date)}
+ {formatDate(post.date, i18n.language)}
{post.description}
diff --git a/ui/litellm-dashboard/src/i18n/LanguageProvider.tsx b/ui/litellm-dashboard/src/i18n/LanguageProvider.tsx
index de9efbb532c..c68a018b044 100644
--- a/ui/litellm-dashboard/src/i18n/LanguageProvider.tsx
+++ b/ui/litellm-dashboard/src/i18n/LanguageProvider.tsx
@@ -15,9 +15,7 @@ export default function LanguageProvider({ children }: { children: ReactNode })
try {
const stored = window.localStorage.getItem(LANGUAGE_STORAGE_KEY);
if (isLanguage(stored)) void instance.changeLanguage(stored);
- } catch {
- // Storage can be disabled by the browser; language selection still works in memory
- }
+ } catch {}
updateDocumentLanguage(instance.language);
return () => {
instance.off("languageChanged", updateDocumentLanguage);
diff --git a/ui/litellm-dashboard/src/i18n/locales/en.json b/ui/litellm-dashboard/src/i18n/locales/en.json
index 4886c58c131..67ea7246186 100644
--- a/ui/litellm-dashboard/src/i18n/locales/en.json
+++ b/ui/litellm-dashboard/src/i18n/locales/en.json
@@ -73,6 +73,7 @@
"Prompts": "Prompts",
"Read the docs": "Read the docs",
"Response Cache": "Response Cache",
+ "Retry": "Retry",
"Role": "Role",
"Route every request to the cheapest model that can handle it, no prompt changes needed.": "Route every request to the cheapest model that can handle it, no prompt changes needed.",
"Router Settings": "Router Settings",
diff --git a/ui/litellm-dashboard/src/i18n/locales/zh-CN.json b/ui/litellm-dashboard/src/i18n/locales/zh-CN.json
index bf2706ad15c..0bb1cf75bb7 100644
--- a/ui/litellm-dashboard/src/i18n/locales/zh-CN.json
+++ b/ui/litellm-dashboard/src/i18n/locales/zh-CN.json
@@ -73,6 +73,7 @@
"Prompts": "提示词",
"Read the docs": "查看文档",
"Response Cache": "响应缓存",
+ "Retry": "重试",
"Role": "角色",
"Route every request to the cheapest model that can handle it, no prompt changes needed.": "自动将每个请求分配给能够处理它的最低成本模型,无需修改提示词。",
"Router Settings": "路由设置",
From f2b414d420f7f8a3b9d5000c47850198b4c881aa Mon Sep 17 00:00:00 2001
From: J_Sinclair <56301946+E-R-Butch@users.noreply.github.com>
Date: Mon, 7 Sep 2026 18:26:39 +0800
Subject: [PATCH 3/6] style(ui): format blog localization regression test
---
.../Navbar/BlogDropdown/BlogDropdown.integration.test.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx
index fc69c1a59ea..f3ef3f267d8 100644
--- a/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.integration.test.tsx
@@ -15,7 +15,9 @@ vi.mock("@/app/(dashboard)/hooks/useDisableBlogPosts", () => ({
vi.mock("@/app/(dashboard)/hooks/blogPosts/useBlogPosts", () => ({
useBlogPosts: () => ({
- data: { posts: [{ title: "Post One", date: "2026-02-01", description: "Description one", url: "https://example.com/1" }] },
+ data: {
+ posts: [{ title: "Post One", date: "2026-02-01", description: "Description one", url: "https://example.com/1" }],
+ },
isLoading: false,
isError: mockIsError,
refetch: mockRefetch,
From a3fa63e5a10faa7444f7aa2aad00f567b3ed9ba4 Mon Sep 17 00:00:00 2001
From: J_Sinclair <56301946+E-R-Butch@users.noreply.github.com>
Date: Fri, 23 Oct 2026 13:07:49 +0800
Subject: [PATCH 4/6] fix(deps): patch security dependencies and preserve
telemetry exports
Adapt dependency updates from Ishaan's upstream PR #47974, including f3bbb9eb022202f0322a5dcefe6302e9e57b5a85
Adapt legacy log-record test migration from a31f64819f8cd4bf42a9ceece086201c7979b63d
Preserve tenant credential isolation and replace realistic test fixtures with synthetic values
---
litellm/integrations/langfuse/langfuse_sdk.py | 3 +
litellm/integrations/opentelemetry.py | 6 +-
litellm/integrations/otel/mount.py | 3 +
.../integrations/otel/plumbing/otlp_json.py | 102 +++-----
.../integrations/otel/plumbing/otlp_tls.py | 70 ++++-
.../integrations/otel/plumbing/providers.py | 32 +--
litellm/integrations/otel/presets/agentops.py | 73 +++---
pyproject.toml | 31 ++-
.../langfuse/test_langfuse_sdk.py | 24 ++
.../otel/test_otel_v2_components.py | 74 +++++-
.../otel/test_otel_v2_destinations.py | 168 ++++++++++++
.../integrations/otel/test_otel_v2_mount.py | 17 +-
.../integrations/otel/test_otel_v2_presets.py | 87 ++++---
tests/unit/integrations/test_langfuse_otel.py | 26 +-
tests/unit/integrations/test_opentelemetry.py | 52 ++--
.../test_litellm_logging.py | 18 +-
ui/litellm-dashboard/package-lock.json | 210 +++++++--------
ui/litellm-dashboard/package.json | 13 +-
uv.lock | 246 ++++++++++++------
vscode-extension/package-lock.json | 90 +++----
vscode-extension/package.json | 2 +-
21 files changed, 864 insertions(+), 483 deletions(-)
diff --git a/litellm/integrations/langfuse/langfuse_sdk.py b/litellm/integrations/langfuse/langfuse_sdk.py
index 51b08cb72df..1bbc18d5dd4 100644
--- a/litellm/integrations/langfuse/langfuse_sdk.py
+++ b/litellm/integrations/langfuse/langfuse_sdk.py
@@ -317,6 +317,9 @@ class _RequestedIdGenerator(RandomIdGenerator):
the tracer's id generator, so the request rides on a context variable instead.
"""
+ def is_trace_id_random(self) -> bool:
+ return _requested_trace_id.get() is None
+
def generate_trace_id(self) -> int:
requested: Final = _requested_trace_id.get()
return super().generate_trace_id() if requested is None else requested
diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py
index 0d0e309034a..2226651227f 100644
--- a/litellm/integrations/opentelemetry.py
+++ b/litellm/integrations/opentelemetry.py
@@ -3161,7 +3161,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
otel_exporter,
)
normalized_endpoint = self._normalize_otel_endpoint(otel_endpoint, "traces")
- tls: Final = resolve_otlp_http_tls("TRACES")
+ tls: Final = resolve_otlp_http_tls("TRACES", _split_otel_headers)
return BatchSpanProcessor(
OTLPSpanExporterHTTP(
endpoint=normalized_endpoint,
@@ -3249,7 +3249,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
self.OTEL_EXPORTER,
normalized_endpoint,
)
- tls: Final = resolve_otlp_http_tls("LOGS")
+ tls: Final = resolve_otlp_http_tls("LOGS", _split_otel_headers)
return OTLPLogExporter(
endpoint=normalized_endpoint,
headers=_split_otel_headers,
@@ -3318,7 +3318,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
OTLPMetricExporter,
)
- tls: Final = resolve_otlp_http_tls("METRICS")
+ tls: Final = resolve_otlp_http_tls("METRICS", _split_otel_headers)
exporter = OTLPMetricExporter(
endpoint=normalized_endpoint,
headers=_split_otel_headers,
diff --git a/litellm/integrations/otel/mount.py b/litellm/integrations/otel/mount.py
index cb52ad24c8e..c033bc1935b 100644
--- a/litellm/integrations/otel/mount.py
+++ b/litellm/integrations/otel/mount.py
@@ -126,6 +126,9 @@ def instrument_fastapi_app(app: "FastAPI", tracer_provider: "TracerProvider | No
try:
if not is_otel_v2_enabled():
return
+ if app.middleware_stack is not None:
+ verbose_logger.debug("Skipping OTel V2 FastAPI instrumentation after the middleware stack was built")
+ return
# Lazy: only the V2-enabled path needs the optional
# ``opentelemetry-instrumentation-fastapi`` package, which is not part of the
diff --git a/litellm/integrations/otel/plumbing/otlp_json.py b/litellm/integrations/otel/plumbing/otlp_json.py
index bc6d7d435b0..3c853c3ea8f 100644
--- a/litellm/integrations/otel/plumbing/otlp_json.py
+++ b/litellm/integrations/otel/plumbing/otlp_json.py
@@ -1,77 +1,47 @@
-"""OTLP/HTTP span exporter that sends the OTLP/JSON encoding instead of protobuf.
+"""OTLP/JSON export with the same requests and TLS configuration as OTLP/protobuf."""
-The SDK only ships a protobuf OTLP/HTTP exporter; this reuses its transport and
-retry loop and swaps the payload for OTLP/JSON (enums as integers, ids as hex).
-"""
-
-import base64
-import json
-from collections.abc import Mapping, Sequence
-from types import MappingProxyType
-from typing import Final, TypeAlias
+import os
+from collections.abc import Mapping
+from typing import Final
import requests
-from google.protobuf.json_format import MessageToDict
-from opentelemetry.exporter.otlp.proto.common.trace_encoder import encode_spans
-from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
-from opentelemetry.sdk.trace import ReadableSpan
-
-JSON_CONTENT_TYPE: Final = "application/json"
-_HEX_ID_KEYS: Final = frozenset({"traceId", "spanId", "parentSpanId"})
-
-_JsonValue: TypeAlias = "Mapping[str, _JsonValue] | Sequence[_JsonValue] | str | int | float | bool | None"
-_JsonObject: TypeAlias = Mapping[str, "_JsonValue"]
-
-
-def _objects(node: _JsonObject, key: str) -> tuple[_JsonObject, ...]:
- items: Final = node.get(key)
- if isinstance(items, str) or not isinstance(items, Sequence):
- return ()
- return tuple(item for item in items if isinstance(item, Mapping))
-
-
-def _hex_ids(node: _JsonObject) -> _JsonObject:
- return MappingProxyType(
- {
- key: base64.b64decode(item).hex() if key in _HEX_ID_KEYS and isinstance(item, str) else item
- for key, item in node.items()
- }
- )
-
-
-def _hex_span(span: _JsonObject) -> _JsonObject:
- links: Final = _objects(span, "links")
- if not links:
- return _hex_ids(span)
- return MappingProxyType({**_hex_ids(span), "links": tuple(_hex_ids(link) for link in links)})
-
-
-def _hex_scope_spans(scope: _JsonObject) -> _JsonObject:
- return MappingProxyType({**scope, "spans": tuple(_hex_span(span) for span in _objects(scope, "spans"))})
-
-
-def _hex_resource_spans(resource: _JsonObject) -> _JsonObject:
- scope_spans: Final = tuple(_hex_scope_spans(scope) for scope in _objects(resource, "scopeSpans"))
- return MappingProxyType({**resource, "scopeSpans": scope_spans})
-
-
-def encode_spans_json(spans: Sequence[ReadableSpan]) -> bytes:
- payload: Final[_JsonObject] = MessageToDict(encode_spans(spans), use_integers_for_enums=True)
- resource_spans: Final = tuple(_hex_resource_spans(resource) for resource in _objects(payload, "resourceSpans"))
- hexed: Final[_JsonObject] = MappingProxyType({**payload, "resourceSpans": resource_spans})
- return json.dumps(hexed, default=dict, separators=(",", ":")).encode()
+from opentelemetry.exporter.http.transport._requests import RequestsHTTPTransport
+from opentelemetry.exporter.otlp.json.http.trace_exporter import OTLPSpanExporter
+from opentelemetry.sdk.environment_variables import (
+ OTEL_EXPORTER_OTLP_CERTIFICATE,
+ OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE,
+ OTEL_EXPORTER_OTLP_CLIENT_KEY,
+ OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE,
+ OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE,
+ OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY,
+)
class OTLPJsonSpanExporter(OTLPSpanExporter):
def __init__(
self,
endpoint: str | None,
- headers: dict[str, str], # mutable-ok: SDK __init__ takes Dict
+ headers: Mapping[str, str],
certificate_file: str | None = None,
- session: "requests.Session | None" = None,
+ session: requests.Session | None = None,
) -> None:
- super().__init__(endpoint=endpoint, headers=headers, certificate_file=certificate_file, session=session)
- self._session.headers["Content-Type"] = JSON_CONTENT_TYPE
-
- def _serialize_spans(self, spans: Sequence[ReadableSpan]) -> bytes:
- return encode_spans_json(spans)
+ verify: Final = (
+ certificate_file
+ or os.getenv(OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE)
+ or os.getenv(OTEL_EXPORTER_OTLP_CERTIFICATE)
+ or True
+ )
+ client_key: Final = os.getenv(OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY) or os.getenv(OTEL_EXPORTER_OTLP_CLIENT_KEY)
+ client_certificate: Final = os.getenv(OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE) or os.getenv(
+ OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE
+ )
+ # The JSON SDK has no session argument. Its transport injection retains
+ # requests proxies, session authentication and the existing TLS adapters.
+ transport: Final = RequestsHTTPTransport(
+ verify=verify,
+ cert=(client_certificate, client_key) if client_certificate and client_key else client_certificate,
+ session=session,
+ )
+ super().__init__(
+ endpoint=endpoint, headers={**headers, "content-type": "application/json"}, _transport=transport
+ )
diff --git a/litellm/integrations/otel/plumbing/otlp_tls.py b/litellm/integrations/otel/plumbing/otlp_tls.py
index 9de97e0c77a..959e45e9bd7 100644
--- a/litellm/integrations/otel/plumbing/otlp_tls.py
+++ b/litellm/integrations/otel/plumbing/otlp_tls.py
@@ -1,11 +1,19 @@
import os
+from collections.abc import Mapping
+from copy import copy
from dataclasses import dataclass
from typing import Final, Literal
import requests
+from pydantic import BaseModel, ConfigDict
from requests.adapters import HTTPAdapter
+class _RequestHeaders(BaseModel):
+ model_config = ConfigDict(frozen=True, from_attributes=True)
+ headers: Mapping[str, str] | None = None
+
+
@dataclass(frozen=True, slots=True)
class OtlpHttpTls:
certificate_file: str | None
@@ -25,17 +33,65 @@ class _NoVerifyAdapter(HTTPAdapter):
)
-def resolve_otlp_http_tls(signal: Literal["TRACES", "METRICS", "LOGS"]) -> OtlpHttpTls:
+class _IsolatedHeadersSession(requests.Session):
+ def __init__(self, signal: str, headers: Mapping[str, str], content_type: str) -> None:
+ from opentelemetry.util.re import parse_env_headers
+
+ super().__init__()
+ environment: Final = parse_env_headers(
+ os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_HEADERS") or os.getenv("OTEL_EXPORTER_OTLP_HEADERS", ""),
+ liberal=True,
+ )
+ explicit: Final = frozenset(key.lower() for key in headers)
+ self._excluded_headers: Final = frozenset(key.lower() for key in environment) - explicit
+ self._content_type: Final = content_type
+ self._compression: Final = (
+ (
+ os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_COMPRESSION")
+ or os.getenv("OTEL_EXPORTER_OTLP_COMPRESSION", "none")
+ )
+ .strip()
+ .lower()
+ )
+
+ def prepare_request(self, request: requests.Request) -> requests.PreparedRequest:
+ isolated: Final = copy(request)
+ source: Final[Mapping[str, str]] = _RequestHeaders.model_validate(request).headers or {}
+ kept: Final = {key: value for key, value in source.items() if key.lower() not in self._excluded_headers}
+ content_type: Final = {"Content-Type": self._content_type} if "content-type" in self._excluded_headers else {}
+ compression: Final = (
+ {"Content-Encoding": self._compression}
+ if "content-encoding" in self._excluded_headers and self._compression in ("gzip", "deflate")
+ else {}
+ )
+ isolated.headers = {**kept, **content_type, **compression}
+ session: Final = copy(self)
+ session.headers = {
+ key: value for key, value in self.headers.items() if key.lower() not in self._excluded_headers
+ }
+ return requests.Session.prepare_request(session, isolated)
+
+
+def resolve_otlp_http_tls(
+ signal: Literal["TRACES", "METRICS", "LOGS"],
+ headers: Mapping[str, str] | None = None,
+ *,
+ content_type: str = "application/x-protobuf",
+ isolate_headers: bool = False,
+) -> OtlpHttpTls:
+ session: Final = (
+ _IsolatedHeadersSession(signal, headers or {}, content_type) if headers or isolate_headers else None
+ )
if os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_CERTIFICATE") or os.getenv("OTEL_EXPORTER_OTLP_CERTIFICATE"):
- return OtlpHttpTls(certificate_file=None, session=None)
+ return OtlpHttpTls(certificate_file=None, session=session)
from litellm.llms.custom_httpx.http_handler import get_ssl_verify
verify: Final = get_ssl_verify()
if verify is False:
- session: Final = requests.Session()
- session.mount("https://", _NoVerifyAdapter())
- return OtlpHttpTls(certificate_file=None, session=session)
+ unverified: Final = session or requests.Session()
+ unverified.mount("https://", _NoVerifyAdapter())
+ return OtlpHttpTls(certificate_file=None, session=unverified)
if isinstance(verify, str):
- return OtlpHttpTls(certificate_file=verify, session=None)
- return OtlpHttpTls(certificate_file=None, session=None)
+ return OtlpHttpTls(certificate_file=verify, session=session)
+ return OtlpHttpTls(certificate_file=None, session=session)
diff --git a/litellm/integrations/otel/plumbing/providers.py b/litellm/integrations/otel/plumbing/providers.py
index 898eab5715b..4aae13d822c 100644
--- a/litellm/integrations/otel/plumbing/providers.py
+++ b/litellm/integrations/otel/plumbing/providers.py
@@ -16,9 +16,9 @@ from opentelemetry.metrics import MeterProvider, NoOpMeterProvider
from opentelemetry.sdk._logs import LoggerProvider as SDKLoggerProvider
from opentelemetry.sdk._logs.export import (
BatchLogRecordProcessor,
- ConsoleLogExporter,
- InMemoryLogExporter,
- LogExporter,
+ ConsoleLogRecordExporter,
+ InMemoryLogRecordExporter,
+ LogRecordExporter,
SimpleLogRecordProcessor,
)
from opentelemetry.sdk.metrics import MeterProvider as SDKMeterProvider
@@ -194,7 +194,7 @@ def exporter_transport(kind: str) -> Literal["http", "grpc", "headerless"]:
return "headerless"
-def _exporter_from_spec(spec: ExporterSpec) -> SpanExporter:
+def _exporter_from_spec(spec: ExporterSpec, *, isolate_headers: bool = False) -> SpanExporter:
kind: Final = (spec.kind or "console").lower()
factory: Final = _EXPORTER_FACTORIES.get(kind)
if factory is not None:
@@ -204,7 +204,9 @@ def _exporter_from_spec(spec: ExporterSpec) -> SpanExporter:
if kind in _OTLP_HTTP_JSON_KINDS:
from litellm.integrations.otel.plumbing.otlp_json import OTLPJsonSpanExporter
- tls: Final = resolve_otlp_http_tls("TRACES")
+ tls: Final = resolve_otlp_http_tls(
+ "TRACES", parse_headers(spec.headers), content_type="application/json", isolate_headers=isolate_headers
+ )
return OTLPJsonSpanExporter(
endpoint=spec.traces_endpoint or _otlp_traces_endpoint(spec.endpoint),
headers=parse_headers(spec.headers),
@@ -216,7 +218,7 @@ def _exporter_from_spec(spec: ExporterSpec) -> SpanExporter:
OTLPSpanExporter as HTTPExporter,
)
- http_tls: Final = resolve_otlp_http_tls("TRACES")
+ http_tls: Final = resolve_otlp_http_tls("TRACES", parse_headers(spec.headers), isolate_headers=isolate_headers)
return HTTPExporter(
endpoint=spec.traces_endpoint or _otlp_traces_endpoint(spec.endpoint),
headers=parse_headers(spec.headers),
@@ -1013,7 +1015,7 @@ def _destination_processor(destination: "OtelDestination") -> SpanProcessor | No
headers=destination.header_string(),
owner=None,
)
- return _processor_for(_exporter_from_spec(spec), use_simple=False)
+ return _processor_for(_exporter_from_spec(spec, isolate_headers=True), use_simple=False)
except Exception as exc: # noqa: BLE001 # a malformed destination must not break the request or the other destinations
verbose_logger.debug("OTel V2 fan-out: no processor for %s: %s", destination.endpoint, exc)
return None
@@ -1186,7 +1188,7 @@ def build_metric_reader(config: OpenTelemetryV2Config) -> "MetricReader":
OTLPMetricExporter as HTTPMetricExporter,
)
- tls: Final = resolve_otlp_http_tls("METRICS")
+ tls: Final = resolve_otlp_http_tls("METRICS", parse_headers(config.headers))
exporter: object = HTTPMetricExporter(
endpoint=_otlp_metrics_endpoint(config.endpoint),
headers=parse_headers(config.headers),
@@ -1233,7 +1235,7 @@ def _otlp_logs_endpoint(endpoint: str | None) -> str | None:
return endpoint + "/v1/logs"
-def build_log_exporter(config: OpenTelemetryV2Config) -> LogExporter:
+def build_log_exporter(config: OpenTelemetryV2Config) -> LogRecordExporter:
"""Build a log exporter mirroring the exporter selection of the other signals.
``console`` (and any unrecognized kind) exports to the console; ``otlp_http``
@@ -1243,13 +1245,13 @@ def build_log_exporter(config: OpenTelemetryV2Config) -> LogExporter:
"""
kind: Final = (config.exporter or "console").lower()
if kind in ("in_memory", "inmemory", "memory"):
- return InMemoryLogExporter()
+ return InMemoryLogRecordExporter()
if kind in ("otlp_http", "http", "http/protobuf", "http/json"):
from opentelemetry.exporter.otlp.proto.http._log_exporter import (
OTLPLogExporter as HTTPLogExporter,
)
- tls: Final = resolve_otlp_http_tls("LOGS")
+ tls: Final = resolve_otlp_http_tls("LOGS", parse_headers(config.headers))
return HTTPLogExporter(
endpoint=_otlp_logs_endpoint(config.endpoint),
headers=parse_headers(config.headers),
@@ -1268,24 +1270,24 @@ def build_log_exporter(config: OpenTelemetryV2Config) -> LogExporter:
) from exc
return GRPCLogExporter(endpoint=config.endpoint, headers=parse_headers(config.headers))
- return ConsoleLogExporter()
+ return ConsoleLogRecordExporter()
def build_logger_provider(
config: OpenTelemetryV2Config,
- log_exporter: LogExporter | None = None,
+ log_exporter: LogRecordExporter | None = None,
) -> SDKLoggerProvider:
"""Build the :class:`LoggerProvider` GenAI events export through.
``log_exporter`` is an explicit override (tests inject an
- ``InMemoryLogExporter``); otherwise the exporter is selected from the config's
+ ``InMemoryLogRecordExporter``); otherwise the exporter is selected from the config's
exporter kind via :func:`build_log_exporter`. Console and in-memory exporters
get a Simple processor (synchronous export, which tests rely on), everything
else a Batch processor — the same split as span processing.
"""
exporter: Final = log_exporter if log_exporter is not None else build_log_exporter(config)
provider: Final = SDKLoggerProvider(resource=build_resource(config))
- use_simple: Final = isinstance(exporter, (ConsoleLogExporter, InMemoryLogExporter))
+ use_simple: Final = isinstance(exporter, (ConsoleLogRecordExporter, InMemoryLogRecordExporter))
provider.add_log_record_processor(
SimpleLogRecordProcessor(exporter) if use_simple else BatchLogRecordProcessor(exporter)
)
diff --git a/litellm/integrations/otel/presets/agentops.py b/litellm/integrations/otel/presets/agentops.py
index 3befb73b0cc..30758f7ed4b 100644
--- a/litellm/integrations/otel/presets/agentops.py
+++ b/litellm/integrations/otel/presets/agentops.py
@@ -9,14 +9,14 @@ this preset registers a custom exporter (``kind="agentops"``) that mints the JWT
worker thread, off any event loop — and caches it for the process lifetime.
"""
-from collections.abc import Sequence
from typing import Final
import httpx
-from opentelemetry.sdk.trace import ReadableSpan
-from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
+import requests
+from opentelemetry.sdk.trace.export import SpanExporter
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
+from requests.auth import AuthBase
from litellm._logging import verbose_logger
from litellm.integrations.otel.model.config import (
@@ -24,6 +24,7 @@ from litellm.integrations.otel.model.config import (
ExporterSpec,
OpenTelemetryV2Config,
)
+from litellm.integrations.otel.plumbing.otlp_tls import resolve_otlp_http_tls
from litellm.integrations.otel.plumbing.providers import register_exporter_factory
_AGENTOPS_ENDPOINT: Final = "https://otlp.agentops.ai/v1/traces"
@@ -74,47 +75,37 @@ def agentops_preset(
)
+class _AgentOpsAuth(AuthBase):
+ def __init__(self, api_key: str | None) -> None:
+ self._api_key = api_key
+ self._resolved = False
+ self._token: str | None = None
+
+ def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
+ if not self._resolved:
+ self._resolved = True
+ if self._api_key:
+ try:
+ token: Final = _fetch_agentops_jwt(self._api_key).get("token")
+ if isinstance(token, str) and token:
+ self._token = token
+ except Exception as error:
+ verbose_logger.debug("AgentOps JWT fetch failed: %s", error)
+ authenticated: Final = request.copy()
+ if self._token:
+ authenticated.headers["Authorization"] = f"Bearer {self._token}"
+ return authenticated
+
+
def _build_agentops_exporter(spec: ExporterSpec) -> SpanExporter:
- """Factory for the ``agentops`` exporter kind: a lazy-auth OTLP/HTTP exporter."""
- from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
- OTLPSpanExporter,
- )
-
- class _LazyAuthAgentOpsExporter(OTLPSpanExporter):
- """OTLP/HTTP exporter that mints the AgentOps JWT on its first export.
-
- ``export`` runs in the ``BatchSpanProcessor`` worker thread, so the
- blocking token fetch never touches an event loop. The result is cached
- after the first attempt (success or failure) so it runs at most once.
- """
-
- def __init__(self, *, endpoint: str | None, api_key: str | None) -> None:
- super().__init__(endpoint=endpoint)
- self._agentops_api_key = api_key
- self._auth_resolved = False
-
- def _ensure_authenticated(self) -> None:
- if self._auth_resolved:
- return
- self._auth_resolved = True
- if not self._agentops_api_key:
- return
- try:
- token: Final = _fetch_agentops_jwt(self._agentops_api_key).get("token")
- if token:
- # ``_session`` is the requests.Session the base exporter
- # POSTs through; updating its Authorization header is how the
- # minted JWT reaches every subsequent export.
- self._session.headers["Authorization"] = f"Bearer {token}"
- except Exception as e:
- verbose_logger.debug("AgentOps JWT fetch failed: %s", e)
-
- def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
- self._ensure_authenticated()
- return super().export(spans)
+ """Authenticate lazily through requests' public session authentication API."""
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
options: Final = spec.options or {}
- return _LazyAuthAgentOpsExporter(endpoint=spec.endpoint, api_key=options.get("api_key"))
+ tls: Final = resolve_otlp_http_tls("TRACES", isolate_headers=True)
+ session: Final = tls.session or requests.Session()
+ session.auth = _AgentOpsAuth(options.get("api_key"))
+ return OTLPSpanExporter(endpoint=spec.endpoint, certificate_file=tls.certificate_file, session=session)
def _fetch_agentops_jwt(api_key: str) -> dict[str, object]:
diff --git a/pyproject.toml b/pyproject.toml
index 3aefd911937..1789d5503c0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -177,16 +177,17 @@ proxy-runtime = [
"grpcio==1.78.0",
"prometheus-client>=0.20.0,<1.0",
"langfuse>=4.7,<5.0",
- "opentelemetry-api==1.33.1",
- "opentelemetry-sdk==1.33.1",
- "opentelemetry-exporter-otlp==1.33.1",
- "opentelemetry-instrumentation-fastapi==0.54b1",
+ "opentelemetry-api==1.45.1",
+ "opentelemetry-sdk==1.45.1",
+ "opentelemetry-exporter-otlp==1.45.1",
+ "opentelemetry-exporter-otlp-json-http==0.66b1",
+ "opentelemetry-instrumentation-fastapi==0.66b1",
"ddtrace>=4.8.2,<5.0",
"sentry-sdk>=2.21.0,<3.0",
"mangum>=0.17.0,<1.0",
"azure-ai-contentsafety>=1.0.0,<2.0",
"azure-storage-file-datalake>=12.20.0,<13.0",
- "pypdf>=6.16.1,<7.0",
+ "pypdf>=6.21.1,<7.0",
"llm-sandbox>=0.3.39,<1.0",
"detect-secrets>=1.5.0,<2.0",
]
@@ -223,10 +224,11 @@ dev = [
"types-PyYAML==6.0.12.20250915",
"botocore-stubs==1.43.14",
"types-boto3[bedrock,bedrock-agent,bedrock-runtime,kms,s3,sagemaker-runtime,sts]==1.43.30",
- "opentelemetry-api==1.33.1",
- "opentelemetry-sdk==1.33.1",
- "opentelemetry-exporter-otlp==1.33.1",
- "opentelemetry-instrumentation-fastapi==0.54b1",
+ "opentelemetry-api==1.45.1",
+ "opentelemetry-sdk==1.45.1",
+ "opentelemetry-exporter-otlp==1.45.1",
+ "opentelemetry-exporter-otlp-json-http==0.66b1",
+ "opentelemetry-instrumentation-fastapi==0.66b1",
"langfuse>=4.7,<5.0",
"fastapi-offline==1.7.6",
"fakeredis==2.34.1",
@@ -255,10 +257,11 @@ proxy-dev = [
"hypercorn==0.17.3",
"prometheus-client==0.20.0",
"sentry-sdk==2.21.0",
- "opentelemetry-api==1.33.1",
- "opentelemetry-sdk==1.33.1",
- "opentelemetry-exporter-otlp==1.33.1",
- "opentelemetry-instrumentation-fastapi==0.54b1",
+ "opentelemetry-api==1.45.1",
+ "opentelemetry-sdk==1.45.1",
+ "opentelemetry-exporter-otlp==1.45.1",
+ "opentelemetry-exporter-otlp-json-http==0.66b1",
+ "opentelemetry-instrumentation-fastapi==0.66b1",
"azure-identity==1.25.2",
"a2a-sdk==1.1.0",
"backports-zstd==1.8.0; python_version < '3.14'",
@@ -280,7 +283,7 @@ ci = [
"langchain==1.3.9",
"lunary==1.4.36; python_version == '3.10'",
"lunary==1.4.37; python_version >= '3.11'",
- "logfire==4.6.0",
+ "logfire==5.2.0",
"traceloop-sdk==0.34.0",
"detect-secrets==1.5.0",
"PyGithub==2.8.1",
diff --git a/tests/unit/integrations/langfuse/test_langfuse_sdk.py b/tests/unit/integrations/langfuse/test_langfuse_sdk.py
index cef2b13f0fd..8f5a8fc603b 100644
--- a/tests/unit/integrations/langfuse/test_langfuse_sdk.py
+++ b/tests/unit/integrations/langfuse/test_langfuse_sdk.py
@@ -165,6 +165,30 @@ def test_requested_trace_id_is_the_exported_trace_id_and_the_generation_is_its_r
assert span.parent is None
+
+@pytest.mark.parametrize("trace_id", ["0" * 31 + "1", None])
+def test_trace_randomness_flag_matches_the_id_source(channel, trace_id):
+ from opentelemetry.context import Context
+ from litellm.integrations.langfuse.langfuse_sdk import _start_span
+
+ tracing, exporter = channel
+ span = _start_span(
+ tracing.tracer,
+ name="id-source",
+ context=Context(),
+ start_time=CALL_START,
+ trace_id=trace_id,
+ observation_id=None,
+ attributes={},
+ )
+ span.end(end_time=to_unix_nanos(CALL_END))
+ tracing.flush()
+ exported = _only_span(exporter, "id-source")
+ assert exported.context.trace_flags.random_trace_id is (trace_id is None)
+ assert exported.context.trace_flags.sampled
+ if trace_id is not None:
+ assert exported.context.trace_id == int(trace_id, 16)
+
def test_parent_observation_id_nests_the_generation_under_the_callers_observation(channel):
tracing, exporter = channel
_generation(tracing, name="child-gen", parent=PARENT_C).end(CALL_END)
diff --git a/tests/unit/integrations/otel/test_otel_v2_components.py b/tests/unit/integrations/otel/test_otel_v2_components.py
index 3f4389765d3..15ea206cf93 100644
--- a/tests/unit/integrations/otel/test_otel_v2_components.py
+++ b/tests/unit/integrations/otel/test_otel_v2_components.py
@@ -23,8 +23,9 @@ from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( # noqa:
from opentelemetry import baggage # noqa: E402
from opentelemetry.context import attach, detach # noqa: E402
from opentelemetry._logs.severity import SeverityNumber # noqa: E402
-from opentelemetry.sdk._logs import LogData, LogRecord # noqa: E402
-from opentelemetry.sdk._logs.export import LogExportResult # noqa: E402
+from opentelemetry._logs import LogRecord
+from opentelemetry.sdk._logs import ReadableLogRecord # noqa: E402
+from opentelemetry.sdk._logs.export import LogRecordExportResult # noqa: E402
from opentelemetry.sdk.metrics import MeterProvider # noqa: E402
from opentelemetry.sdk.metrics.export import InMemoryMetricReader # noqa: E402
from opentelemetry.sdk.trace import TracerProvider # noqa: E402
@@ -676,7 +677,9 @@ def test_parse_headers_percent_decodes_values():
documents ``Authorization=Basic%20``. Forwarding the literal ``%20``
makes the backend reject the export as a malformed credential.
"""
- token = "MTMzNzc4MzpnbGNfZXlKdklqb2lNVEl6TkNJPQ=="
+ from base64 import b64encode
+
+ token: Final = b64encode(b"TEST_ONLY_USER:TEST_ONLY_PASSWORD").decode()
assert providers.parse_headers(f"Authorization=Basic%20{token}") == {"authorization": f"Basic {token}"}
assert providers.parse_headers("x-scope-orgid=team%20a") == {"x-scope-orgid": "team a"}
@@ -733,7 +736,7 @@ def _export_one_trace_to_local_collector(exporter_kind: str) -> tuple[list[dict]
class Collector(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers["Content-Length"]))
- received.append({"path": self.path, "headers": dict(self.headers), "body": body})
+ received.append({"path": self.path, "headers": requests.structures.CaseInsensitiveDict(self.headers), "body": body})
self.send_response(200)
self.end_headers()
@@ -786,13 +789,58 @@ def test_http_json_exporter_posts_otlp_json_to_traces_endpoint():
assert parent["kind"] == 2
assert parent["traceId"] == format(trace_id, "032x")
assert parent["spanId"] == format(parent_id, "016x")
- assert "parentSpanId" not in parent
+ assert parent.get("parentSpanId", "") == ""
child = _only_span(child_request)
assert child["traceId"] == format(trace_id, "032x")
assert child["spanId"] == format(child_id, "016x")
assert child["parentSpanId"] == format(parent_id, "016x")
+
+@pytest.mark.parametrize("compression", ["none", "gzip", "deflate"])
+def test_http_json_exporter_preserves_session_tls_auth_and_compression(monkeypatch, requests_mock, compression):
+ import gzip
+ import zlib
+
+ from opentelemetry.sdk.trace.export import SpanExportResult
+ from litellm.integrations.otel.plumbing.otlp_json import OTLPJsonSpanExporter
+
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", compression)
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE", "/collector/client.crt")
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY", "/collector/client.key")
+ monkeypatch.setenv("HTTPS_PROXY", "http://proxy.example:8080")
+ collector = requests_mock.post("https://collector.example/v1/traces", status_code=200)
+ session = requests.Session()
+ session.headers["x-session-marker"] = "retained"
+ session.auth = requests.auth.HTTPBasicAuth("session-user", "synthetic-password")
+ exporter = OTLPJsonSpanExporter(
+ endpoint="https://collector.example/v1/traces",
+ headers={"x-exporter-marker": "configured"},
+ certificate_file="/collector/ca.crt",
+ session=session,
+ )
+ provider = TracerProvider()
+ span = provider.get_tracer(__name__).start_span("session-export")
+ span.end()
+ try:
+ assert exporter.export([span]) is SpanExportResult.SUCCESS
+ request = collector.last_request
+ assert request.headers["Content-Type"] == "application/json"
+ assert request.headers["x-session-marker"] == "retained"
+ assert request.headers["x-exporter-marker"] == "configured"
+ assert request.headers["Authorization"] == "Basic c2Vzc2lvbi11c2VyOnN5bnRoZXRpYy1wYXNzd29yZA=="
+ assert request.verify == "/collector/ca.crt"
+ assert request.cert == ("/collector/client.crt", "/collector/client.key")
+ assert request.proxies["https"] == "http://proxy.example:8080"
+ decompress = {"none": lambda body: body, "gzip": gzip.decompress, "deflate": zlib.decompress}[compression]
+ payload = json.loads(decompress(request.body))
+ exported = payload["resourceSpans"][0]["scopeSpans"][0]["spans"][0]
+ assert exported["name"] == "session-export"
+ assert exported["traceId"] == f"{span.get_span_context().trace_id:032x}"
+ finally:
+ exporter.shutdown()
+ provider.shutdown()
+
def test_http_protobuf_exporter_still_posts_protobuf():
(_child_request, parent_request), (trace_id, _parent_id, _child_id) = _export_one_trace_to_local_collector(
"http/protobuf"
@@ -924,20 +972,20 @@ def test_otlp_logs_endpoint_normalization():
def test_build_log_exporter_variants():
- from opentelemetry.sdk._logs.export import ConsoleLogExporter, InMemoryLogExporter
+ from opentelemetry.sdk._logs.export import ConsoleLogRecordExporter, InMemoryLogRecordExporter
assert isinstance(
providers.build_log_exporter(OpenTelemetryV2Config(exporter="console")),
- ConsoleLogExporter,
+ ConsoleLogRecordExporter,
)
assert isinstance(
providers.build_log_exporter(OpenTelemetryV2Config(exporter="in_memory")),
- InMemoryLogExporter,
+ InMemoryLogRecordExporter,
)
# An unrecognized kind falls back to console rather than dropping events.
assert isinstance(
providers.build_log_exporter(OpenTelemetryV2Config(exporter="unknown")),
- ConsoleLogExporter,
+ ConsoleLogRecordExporter,
)
http_exporter = providers.build_log_exporter(OpenTelemetryV2Config(exporter="otlp_http", endpoint="http://h:4318"))
assert "OTLPLogExporter" in type(http_exporter).__name__
@@ -1630,9 +1678,13 @@ def test_v2_otlp_http_log_export_trusts_ssl_cert_file(monkeypatch: pytest.Monkey
severity_number=SeverityNumber.INFO,
body="v2-tls-test",
)
- log_data = LogData(log_record=record, instrumentation_scope=InstrumentationScope("v2-tls-test"))
+ log_data = ReadableLogRecord(
+ log_record=record,
+ resource=providers.build_resource(cfg),
+ instrumentation_scope=InstrumentationScope("v2-tls-test"),
+ )
result = exporter.export([log_data])
- assert result is LogExportResult.SUCCESS, f"log export failed: {result}"
+ assert result is LogRecordExportResult.SUCCESS, f"log export failed: {result}"
assert tls_sink.received.get(timeout=5) == "/v1/logs"
finally:
exporter.shutdown()
diff --git a/tests/unit/integrations/otel/test_otel_v2_destinations.py b/tests/unit/integrations/otel/test_otel_v2_destinations.py
index c7a057cca5c..e556cce6d00 100644
--- a/tests/unit/integrations/otel/test_otel_v2_destinations.py
+++ b/tests/unit/integrations/otel/test_otel_v2_destinations.py
@@ -2,6 +2,8 @@
import asyncio
import contextvars
+import gzip
+import os
import threading
import time
from base64 import b64encode
@@ -5067,3 +5069,169 @@ class TestArizeProjectRouting:
monkeypatch.setenv("ARIZE_PROJECT_NAME", "gateway-prod")
assert self._forwarded_projects() == {"gateway-prod"}
+
+
+def _export_header_probe(api: str, signal: str, endpoint: str, headers: str | None) -> None:
+ from opentelemetry.sdk.metrics import MeterProvider
+
+ from litellm.integrations.opentelemetry import OpenTelemetry, OpenTelemetryConfig
+
+ config: Final = OpenTelemetryV2Config(
+ exporter="http/json" if api == "v2-json" else "otlp_http", endpoint=endpoint, headers=headers
+ )
+ tracing: Final = TracerProvider()
+ legacy: Final = (
+ OpenTelemetry(
+ config=OpenTelemetryConfig(exporter="otlp_http", endpoint=endpoint, headers=headers),
+ tracer_provider=tracing,
+ )
+ if api == "v1"
+ else None
+ )
+ try:
+ if signal == "metrics":
+ reader: Final = legacy._get_metric_reader() if legacy else otel_providers.build_metric_reader(config)
+ metrics: Final = MeterProvider(metric_readers=(reader,))
+ try:
+ metrics.get_meter("header-isolation").create_counter("header_probe").add(1)
+ assert metrics.force_flush()
+ finally:
+ metrics.shutdown()
+ elif signal == "logs":
+ logs: Final = legacy._get_log_exporter() if legacy else otel_providers.build_log_exporter(config)
+ try:
+ assert logs.export(()).name == "SUCCESS"
+ finally:
+ logs.shutdown()
+ elif legacy:
+ processor: Final = legacy._get_span_processor()
+ try:
+ assert processor.span_exporter.export(()).name == "SUCCESS"
+ finally:
+ processor.shutdown()
+ else:
+ spans: Final = otel_providers.build_span_exporter(config)
+ try:
+ assert spans.export(()).name == "SUCCESS"
+ finally:
+ spans.shutdown()
+ finally:
+ tracing.shutdown()
+
+
+@pytest.mark.parametrize(
+ ("api", "signal"),
+ (
+ ("v1", "traces"),
+ ("v1", "metrics"),
+ ("v1", "logs"),
+ ("v2", "traces"),
+ ("v2", "metrics"),
+ ("v2", "logs"),
+ ("v2-json", "traces"),
+ ),
+)
+@pytest.mark.parametrize("signal_headers", (False, True))
+def test_http_destinations_do_not_inherit_other_accounts_headers(
+ monkeypatch, requests_mock, api: str, signal: str, signal_headers: bool
+) -> None:
+ variable: Final = f"OTEL_EXPORTER_OTLP_{signal.upper()}_HEADERS" if signal_headers else "OTEL_EXPORTER_OTLP_HEADERS"
+ operator_headers: Final = "Authorization=Bearer%20operator-key,x-operator-key=operator-only"
+ monkeypatch.setenv(variable, operator_headers)
+ for account, configured in (("tenant-a", "a-key"), ("tenant-b", "b-key"), ("operator", None)):
+ endpoint: Final = f"https://{account}.example.test"
+ captured: Final = requests_mock.post(f"{endpoint}/v1/{signal}", content=b"")
+
+ _export_header_probe(api, signal, endpoint, f"x-tenant-key={configured}" if configured else None)
+
+ assert captured.request_history, f"{account} never exported {signal}"
+ for request in captured.request_history:
+ credentials: Final = {
+ name.lower(): value
+ for name, value in request.headers.items()
+ if name.lower() in {"authorization", "x-operator-key", "x-tenant-key"}
+ }
+ assert credentials == (
+ {"x-tenant-key": configured}
+ if configured
+ else {"authorization": "Bearer operator-key", "x-operator-key": "operator-only"}
+ )
+ assert os.environ[variable] == operator_headers
+
+
+@pytest.mark.parametrize("protocol", ("otlp_http", "http/json"))
+def test_headerless_tenant_destination_does_not_receive_operator_credentials(monkeypatch, requests_mock, protocol):
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer%20operator-key")
+ captured: Final = requests_mock.post("https://tenant.example.test/v1/traces", content=b"")
+ processor: Final = otel_providers._destination_processor(
+ OtelDestination(endpoint="https://tenant.example.test", headers={}, protocol=protocol)
+ )
+ assert processor is not None
+ provider: Final = TracerProvider()
+ provider.add_span_processor(processor)
+ try:
+ emit(provider, "headerless-tenant")
+ assert provider.force_flush()
+ assert captured.request_history
+ assert all("Authorization" not in request.headers for request in captured.request_history)
+ finally:
+ provider.shutdown()
+
+
+@pytest.mark.parametrize("api", ("v2", "v2-json"))
+def test_isolated_headers_keep_the_exporters_content_type_and_compression(monkeypatch, requests_mock, api):
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "Content-Type=operator-only,Content-Encoding=operator-only")
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", "gzip")
+ captured: Final = requests_mock.post("https://tenant.example.test/v1/traces", content=b"")
+
+ _export_header_probe(api, "traces", "https://tenant.example.test", "x-tenant-key=tenant-key")
+
+ assert len(captured.request_history) == 1
+ request: Final = captured.last_request
+ assert request.headers["Content-Type"] == ("application/json" if api == "v2-json" else "application/x-protobuf")
+ assert request.headers["Content-Encoding"] == "gzip"
+ monkeypatch.delenv("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION")
+
+ _export_header_probe(api, "traces", "https://tenant.example.test", "x-tenant-key=tenant-key")
+
+ assert gzip.decompress(request.body) == (captured.last_request.body or b"")
+
+
+@pytest.mark.parametrize("api", ("v2", "v2-json"))
+@pytest.mark.parametrize("header", ("Authorization", "AUTHORIZATION", "authorization"))
+def test_tenant_authorization_replaces_operator_authorization(monkeypatch, requests_mock, api, header):
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer%20operator-key")
+ captured: Final = requests_mock.post("https://tenant.example.test/v1/traces", content=b"")
+
+ _export_header_probe(api, "traces", "https://tenant.example.test", f"{header}=Bearer tenant-key")
+
+ assert len(captured.request_history) == 1
+ assert captured.last_request.headers["Authorization"] == "Bearer tenant-key"
+ assert "Bearer operator-key" not in captured.last_request.headers.values()
+
+
+@pytest.mark.parametrize("protocol", ("otlp_http", "http/json"))
+def test_session_defaults_cannot_restore_excluded_operator_headers(monkeypatch, requests_mock, protocol):
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
+
+ from litellm.integrations.otel.plumbing.otlp_json import OTLPJsonSpanExporter
+ from litellm.integrations.otel.plumbing.otlp_tls import resolve_otlp_http_tls
+
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer%20operator-key,x-operator-key=operator-only")
+ captured: Final = requests_mock.post("https://tenant.example.test/v1/traces", content=b"")
+ headers: Final = {"x-tenant-key": "tenant-key"}
+ tls: Final = resolve_otlp_http_tls("TRACES", headers)
+ assert tls.session is not None
+ tls.session.headers.update({"AUTHORIZATION": "Bearer operator-key", "X-Operator-Key": "operator-only"})
+ exporter_type: Final = OTLPJsonSpanExporter if protocol == "http/json" else OTLPSpanExporter
+ exporter: Final = exporter_type(
+ endpoint="https://tenant.example.test/v1/traces", headers=headers, session=tls.session
+ )
+ try:
+ assert exporter.export(()).name == "SUCCESS"
+ assert len(captured.request_history) == 1
+ assert captured.last_request.headers["x-tenant-key"] == "tenant-key"
+ assert "Authorization" not in captured.last_request.headers
+ assert "x-operator-key" not in captured.last_request.headers
+ finally:
+ exporter.shutdown()
diff --git a/tests/unit/integrations/otel/test_otel_v2_mount.py b/tests/unit/integrations/otel/test_otel_v2_mount.py
index 3a27c3ec4e4..22d7e4cd317 100644
--- a/tests/unit/integrations/otel/test_otel_v2_mount.py
+++ b/tests/unit/integrations/otel/test_otel_v2_mount.py
@@ -382,7 +382,7 @@ def test_every_traced_response_names_its_own_server_span(monkeypatch):
assert responses["/stream"].text == "data: first\n\ndata: [DONE]\n\n"
for path, response in responses.items():
context = server_spans[path].context
- assert response.headers.get_list("traceresponse") == [f"00-{context.trace_id:032x}-{context.span_id:016x}-01"]
+ assert response.headers.get_list("traceresponse") == [f"00-{context.trace_id:032x}-{context.span_id:016x}-{context.trace_flags:02x}"]
@pytest.mark.parametrize("path", ["/relayed", "/relayed/health"])
@@ -472,13 +472,20 @@ def test_gate_off_responses_carry_no_traceresponse(monkeypatch):
assert "traceresponse" not in TestClient(app).get("/ping").headers
-def test_failed_instrumentation_registers_no_response_propagator(monkeypatch):
+def test_started_app_keeps_serving_without_registering_a_response_propagator(monkeypatch):
monkeypatch.setenv("LITELLM_OTEL_V2", "1")
app = fastapi.FastAPI()
- with TestClient(app):
- instrument_fastapi_app(app)
+ @app.get("/running")
+ def running():
+ return {"ok": True}
+ with TestClient(app) as client:
+ instrument_fastapi_app(app)
+ response = client.get("/running")
+
+ assert response.json() == {"ok": True}
+ assert "traceresponse" not in response.headers
assert get_global_response_propagator() is None
@@ -545,4 +552,4 @@ def test_the_unhandled_error_helper_names_the_span_it_is_given_in_any_context(mo
headers = contextvars.Context().run(trace_response_headers, span)
- assert headers["traceresponse"] == f"00-{context.trace_id:032x}-{context.span_id:016x}-01"
+ assert headers["traceresponse"] == f"00-{context.trace_id:032x}-{context.span_id:016x}-{context.trace_flags:02x}"
diff --git a/tests/unit/integrations/otel/test_otel_v2_presets.py b/tests/unit/integrations/otel/test_otel_v2_presets.py
index 64366626784..60207a2b5bc 100644
--- a/tests/unit/integrations/otel/test_otel_v2_presets.py
+++ b/tests/unit/integrations/otel/test_otel_v2_presets.py
@@ -2,6 +2,8 @@
event loop: the preset does no network I/O, and a custom exporter mints the JWT
lazily on its first export (in the BatchSpanProcessor worker thread)."""
+import json
+
import httpx
import pytest
@@ -75,50 +77,53 @@ def test_dynamic_cred_presets_tag_exporter_with_matching_owner(monkeypatch):
)
-def test_agentops_exporter_mints_jwt_lazily(monkeypatch):
- pytest.importorskip("opentelemetry.exporter.otlp.proto.http.trace_exporter")
- monkeypatch.setattr(
- agentops_mod, "_fetch_agentops_jwt", lambda _k: {"token": "jwt-xyz"}
- )
- spec = ExporterSpec(
- kind=_AGENTOPS_EXPORTER_KIND,
- endpoint=_AGENTOPS_ENDPOINT,
- options={"api_key": "ak"},
- )
- exporter = _build_agentops_exporter(spec)
+def test_agentops_exporter_mints_jwt_lazily(monkeypatch, requests_mock, respx_mock):
+ from opentelemetry.sdk.trace import TracerProvider
+ from opentelemetry.sdk.trace.export import SpanExportResult
- # No auth header until the first export triggers the (off-loop) fetch.
- assert "Authorization" not in exporter._session.headers
- exporter._ensure_authenticated()
- assert exporter._session.headers["Authorization"] == "Bearer jwt-xyz"
-
- # Cached: a second resolution does not re-fetch.
- calls = []
- monkeypatch.setattr(
- agentops_mod,
- "_fetch_agentops_jwt",
- lambda k: calls.append(k) or {"token": "again"},
- )
- exporter._ensure_authenticated()
- assert calls == []
-
-
-def test_agentops_exporter_tolerates_fetch_failure(monkeypatch):
- pytest.importorskip("opentelemetry.exporter.otlp.proto.http.trace_exporter")
-
- def _raise(_k):
- raise RuntimeError("auth down")
-
- monkeypatch.setattr(agentops_mod, "_fetch_agentops_jwt", _raise)
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer%20operator,x-operator-key=operator-secret")
+ auth = respx_mock.post(agentops_mod._AGENTOPS_AUTH_ENDPOINT).respond(200, json={"token": "jwt-xyz"})
+ collector = requests_mock.post(_AGENTOPS_ENDPOINT, status_code=200)
exporter = _build_agentops_exporter(
- ExporterSpec(
- kind=_AGENTOPS_EXPORTER_KIND,
- endpoint=_AGENTOPS_ENDPOINT,
- options={"api_key": "ak"},
- )
+ ExporterSpec(kind=_AGENTOPS_EXPORTER_KIND, endpoint=_AGENTOPS_ENDPOINT, options={"api_key": "ak"})
)
- exporter._ensure_authenticated() # must not raise
- assert "Authorization" not in exporter._session.headers
+ provider = TracerProvider()
+ span = provider.get_tracer(__name__).start_span("authenticated")
+ span.end()
+ try:
+ assert auth.call_count == 0
+ assert exporter.export([span]) is SpanExportResult.SUCCESS
+ assert exporter.export([]) is SpanExportResult.SUCCESS
+ assert auth.call_count == 1
+ assert json.loads(auth.calls[0].request.content) == {"api_key": "ak"}
+ assert [request.headers["Authorization"] for request in collector.request_history] == [
+ "Bearer jwt-xyz",
+ "Bearer jwt-xyz",
+ ]
+ assert all("x-operator-key" not in request.headers for request in collector.request_history)
+ finally:
+ exporter.shutdown()
+ provider.shutdown()
+
+
+def test_agentops_exporter_tolerates_fetch_failure(monkeypatch, requests_mock, respx_mock):
+ from opentelemetry.sdk.trace.export import SpanExportResult
+
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "Authorization=Bearer%20operator,x-operator-key=operator-secret")
+ auth = respx_mock.post(agentops_mod._AGENTOPS_AUTH_ENDPOINT).respond(503, text="auth down")
+ collector = requests_mock.post(_AGENTOPS_ENDPOINT, status_code=200)
+ exporter = _build_agentops_exporter(
+ ExporterSpec(kind=_AGENTOPS_EXPORTER_KIND, endpoint=_AGENTOPS_ENDPOINT, options={"api_key": "ak"})
+ )
+ try:
+ assert exporter.export([]) is SpanExportResult.SUCCESS
+ assert exporter.export([]) is SpanExportResult.SUCCESS
+ assert auth.call_count == 1
+ assert collector.call_count == 2
+ assert all("Authorization" not in request.headers for request in collector.request_history)
+ assert all("x-operator-key" not in request.headers for request in collector.request_history)
+ finally:
+ exporter.shutdown()
def test_fetch_jwt_uses_owned_client_not_shared_pool(monkeypatch):
diff --git a/tests/unit/integrations/test_langfuse_otel.py b/tests/unit/integrations/test_langfuse_otel.py
index 9f536ab0dac..cc31e2788da 100644
--- a/tests/unit/integrations/test_langfuse_otel.py
+++ b/tests/unit/integrations/test_langfuse_otel.py
@@ -591,7 +591,7 @@ class TestLangfuseOtelKeyDynamicConfig:
assert logger.construct_dynamic_otel_config(self._dynamic_params(langfuse_secret_key=None)) is None
assert logger.construct_dynamic_otel_config(self._dynamic_params(langfuse_public_key=None)) is None
- def test_key_dynamic_params_create_otlp_exporter_without_global_env(self):
+ def test_key_dynamic_params_create_otlp_exporter_without_global_env(self, requests_mock):
"""Without global LANGFUSE_* env vars, a request carrying key-scoped Langfuse
credentials must get a tracer exporting via OTLP HTTP to that key's host."""
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
@@ -617,15 +617,17 @@ class TestLangfuseOtelKeyDynamicConfig:
exporter = span_processors[0].span_exporter
assert isinstance(exporter, OTLPSpanExporter)
- assert exporter._endpoint == "https://langfuse.example.com/api/public/otel/v1/traces"
import base64
expected_auth = base64.b64encode(b"key_public:key_secret").decode()
- assert exporter._headers == {
- "Authorization": f"Basic {expected_auth}",
- "x-langfuse-ingestion-version": "4",
- }
+ collector = requests_mock.post("https://langfuse.example.com/api/public/otel/v1/traces", status_code=200)
+ with tracer.start_as_current_span("dynamic-credentials"):
+ pass
+ assert provider.force_flush()
+ assert collector.call_count == 1
+ assert collector.last_request.headers["Authorization"] == f"Basic {expected_auth}"
+ assert collector.last_request.headers["x-langfuse-ingestion-version"] == "4"
def test_key_dynamic_params_reuse_cached_provider(self):
with self._clean_env():
@@ -644,7 +646,7 @@ class TestLangfuseOtelKeyDynamicConfig:
assert tracer is logger.tracer
assert logger._tracer_provider_cache == {}
- def test_key_credentials_never_passed_to_debug_logger(self):
+ def test_key_credentials_never_passed_to_debug_logger(self, requests_mock):
"""The span-processor debug logs must receive a redacted header value, so the
key-scoped Langfuse secret never enters a log record regardless of downstream
handler configuration, while the exporter still gets the real header."""
@@ -678,10 +680,12 @@ class TestLangfuseOtelKeyDynamicConfig:
provider = next(iter(logger._tracer_provider_cache.values())).provider
exporter = provider._active_span_processor._span_processors[0].span_exporter
assert isinstance(exporter, OTLPSpanExporter)
- assert exporter._headers == {
- "Authorization": f"Basic {secret}",
- "x-langfuse-ingestion-version": "4",
- }
+ from opentelemetry.sdk.trace.export import SpanExportResult
+
+ collector = requests_mock.post("https://langfuse.example.com/api/public/otel/v1/traces", status_code=200)
+ assert exporter.export([]) is SpanExportResult.SUCCESS
+ assert collector.last_request.headers["Authorization"] == f"Basic {secret}"
+ assert collector.last_request.headers["x-langfuse-ingestion-version"] == "4"
class TestLangfuseOtelResponsesAPI:
diff --git a/tests/unit/integrations/test_opentelemetry.py b/tests/unit/integrations/test_opentelemetry.py
index d14703e48a1..8b14fbddaf8 100644
--- a/tests/unit/integrations/test_opentelemetry.py
+++ b/tests/unit/integrations/test_opentelemetry.py
@@ -12,21 +12,23 @@ import weakref
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import MappingProxyType
-from typing import Final
+from typing import ClassVar, Final
from unittest.mock import MagicMock, patch
import pytest
# Adds the grandparent directory to sys.path to allow importing project modules
from opentelemetry import trace
+from opentelemetry._logs import LogRecord
from opentelemetry._logs.severity import SeverityNumber
-from opentelemetry.sdk._logs import LogData, LogRecord
+from opentelemetry.sdk._logs import ReadableLogRecord
from opentelemetry.sdk._logs import LoggerProvider as OTLoggerProvider
-from opentelemetry.sdk._logs.export import InMemoryLogExporter, LogExportResult, SimpleLogRecordProcessor
+from opentelemetry.sdk._logs.export import InMemoryLogExporter, LogRecordExportResult, SimpleLogRecordProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader, MetricsData
+from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import ReadableSpan, SpanLimits, TracerProvider
-from opentelemetry.sdk.trace.export import SimpleSpanProcessor, SpanExportResult
+from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor, SpanExportResult
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from parameterized import parameterized
@@ -2321,9 +2323,13 @@ def test_otlp_http_log_export_trusts_ssl_cert_file(monkeypatch: pytest.MonkeyPat
severity_number=SeverityNumber.INFO,
body="tls-export-test",
)
- log_data: Final = LogData(log_record=record, instrumentation_scope=InstrumentationScope("tls-export-test"))
+ log_data: Final = ReadableLogRecord(
+ log_record=record,
+ resource=Resource.create({}),
+ instrumentation_scope=InstrumentationScope("tls-export-test"),
+ )
result: Final = exporter.export([log_data])
- assert result is LogExportResult.SUCCESS, f"log export failed: {result}"
+ assert result is LogRecordExportResult.SUCCESS, f"log export failed: {result}"
assert tls_sink.received.get(timeout=5) == "/v1/logs"
finally:
exporter.shutdown()
@@ -6639,7 +6645,7 @@ class TestOpenTelemetryProviderlessCallAttributes(unittest.TestCase):
time.sleep(self.POLL_INTERVAL)
return None
- def _emitted_log_records(self, semconv_opt_in: str) -> tuple[LogData, ...]:
+ def _emitted_log_records(self, semconv_opt_in: str) -> tuple[ReadableLogRecord, ...]:
log_exporter = InMemoryLogExporter()
logger_provider = OTLoggerProvider()
logger_provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter))
@@ -6713,7 +6719,19 @@ class TestDynamicTracerProviderCache(unittest.TestCase):
must be bounded and must shut down whatever it drops (LIT-5437: threads accumulated
until pods were OOMKilled)."""
- BSP_THREAD_NAME = "OtelBatchSpanProcessor"
+ BSP_THREAD_NAME: ClassVar[str]
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ before: Final = frozenset(threading.enumerate())
+ processor: Final = BatchSpanProcessor(InMemorySpanExporter())
+ try:
+ workers: Final = tuple(thread for thread in threading.enumerate() if thread not in before)
+ assert len(workers) == 1
+ assert workers[0].is_alive()
+ cls.BSP_THREAD_NAME = workers[0].name
+ finally:
+ processor.shutdown()
def _logger(self, cap=3, exporter="console"):
logger = OpenTelemetry(
@@ -6751,8 +6769,6 @@ class TestDynamicTracerProviderCache(unittest.TestCase):
logger._get_tracer_with_dynamic_headers({"authorization": f"Basic tenant-{i}"})
self.assertEqual(len(logger._tracer_provider_cache), 3)
- # Guards the thread-name constant: a rename upstream would make this read 0 and the
- # bound assertion below would pass while measuring nothing.
self.assertGreaterEqual(len(self._live_exporter_threads()), 1)
self.assertLessEqual(self._wait_for_exporter_threads(before + 3) - before, 3)
@@ -6923,8 +6939,8 @@ class TestOpenTelemetryDatabaseSemconvAttributes(unittest.TestCase):
database's own metrics.
"""
- DSN = "postgresql://llmproxy:dbpassword9090@litellm-prod.abc123.us-east-1.rds.amazonaws.com:6432/litellm?schema=reporting"
- REPLICA_DSN = "postgresql://reader:r3ad0nly@litellm-prod-ro.abc123.us-east-1.rds.amazonaws.com/litellm"
+ DSN = "postgresql://TEST_ONLY_USER:TEST_ONLY_PASSWORD@primary-db.example.invalid:6432/TEST_ONLY_DATABASE?schema=reporting"
+ REPLICA_DSN = "postgresql://TEST_ONLY_REPLICA_USER:TEST_ONLY_REPLICA_PASSWORD@replica-db.example.invalid/TEST_ONLY_DATABASE"
def _service_span(self, service, call_type, dsn, error=None, replica_dsn=None):
exporter = InMemorySpanExporter()
@@ -6965,10 +6981,10 @@ class TestOpenTelemetryDatabaseSemconvAttributes(unittest.TestCase):
self.assertEqual(span.attributes["db.operation.name"], "get_data")
self.assertEqual(
span.attributes["server.address"],
- "litellm-prod.abc123.us-east-1.rds.amazonaws.com",
+ "primary-db.example.invalid",
)
self.assertEqual(span.attributes["server.port"], 6432)
- self.assertEqual(span.attributes["db.namespace"], "litellm|reporting")
+ self.assertEqual(span.attributes["db.namespace"], "TEST_ONLY_DATABASE|reporting")
def test_datastore_span_is_a_client_span_carrying_the_legacy_db_system(self):
"""Datadog types a span as a database call from CLIENT kind plus
@@ -6994,16 +7010,16 @@ class TestOpenTelemetryDatabaseSemconvAttributes(unittest.TestCase):
self.assertEqual(span.kind, trace.SpanKind.CLIENT)
self.assertEqual(
span.attributes["server.address"],
- "litellm-prod.abc123.us-east-1.rds.amazonaws.com",
+ "primary-db.example.invalid",
)
self.assertEqual(span.attributes["error"], "connection refused")
def test_no_credential_from_the_dsn_lands_on_the_span(self):
span = self._service_span(ServiceTypes.DB, "get_data", self.DSN)
exported = " ".join(str(value) for value in span.attributes.values())
- self.assertIn("litellm-prod.abc123.us-east-1.rds.amazonaws.com", exported)
- self.assertNotIn("dbpassword9090", exported)
- self.assertNotIn("llmproxy", exported)
+ self.assertIn("primary-db.example.invalid", exported)
+ self.assertNotIn("TEST_ONLY_PASSWORD", exported)
+ self.assertNotIn("TEST_ONLY_USER", exported)
def test_redis_span_does_not_borrow_the_postgres_endpoint(self):
span = self._service_span(ServiceTypes.REDIS, "async_set_cache", self.DSN)
diff --git a/tests/unit/litellm_core_utils/test_litellm_logging.py b/tests/unit/litellm_core_utils/test_litellm_logging.py
index bbfae2229ce..5b67250ad00 100644
--- a/tests/unit/litellm_core_utils/test_litellm_logging.py
+++ b/tests/unit/litellm_core_utils/test_litellm_logging.py
@@ -2043,7 +2043,7 @@ async def test_logfire_logger_accepts_env_vars_for_base_url(monkeypatch):
],
)
def test_langtrace_callback_exports_to_api_trace_with_x_api_key(
- monkeypatch: pytest.MonkeyPatch, api_host: str | None, expected_endpoint: str
+ monkeypatch: pytest.MonkeyPatch, api_host: str | None, expected_endpoint: str, requests_mock
) -> None:
"""The exporter must post to Langtrace's complete /api/trace path with the key in x-api-key,
without leaking it into the process-wide OTEL_EXPORTER_OTLP_TRACES_HEADERS."""
@@ -2069,8 +2069,12 @@ def test_langtrace_callback_exports_to_api_trace_with_x_api_key(
assert type(logger) is OpenTelemetry and logger.callback_name == "langtrace"
exporter: Final = logger._get_span_processor().span_exporter
assert isinstance(exporter, OTLPSpanExporter)
- assert exporter._endpoint == expected_endpoint
- assert exporter._headers == {"x-api-key": api_key}
+ from opentelemetry.sdk.trace.export import SpanExportResult
+
+ collector: Final = requests_mock.post(expected_endpoint, status_code=200)
+ assert exporter.export([]) is SpanExportResult.SUCCESS
+ assert collector.call_count == 1
+ assert collector.last_request.headers["x-api-key"] == api_key
assert "OTEL_EXPORTER_OTLP_TRACES_HEADERS" not in os.environ
finally:
logging_module._in_memory_loggers.clear()
@@ -8019,14 +8023,14 @@ def test_payload_without_guardrail_cost_is_unchanged(logging_obj):
assert payload["cost_breakdown"] is None
-_AWS_SECRET = "wJalrXUtnFEMIK7MDENGbPxRfiCYEXAMPLEKEY"
-_GEMINI_KEY = "AIzaSyC0000000000000000000000000000000"
+_AWS_SECRET = "TEST_ONLY_AWS_SECRET_NOT_A_CREDENTIAL"
+_GEMINI_KEY = "TEST_ONLY_GEMINI_KEY_NOT_A_CREDENTIAL"
def test_empty_api_base_does_not_dump_call_state(logging_obj):
"""Direct (non-HTTP) providers pass api_base='', which used to echo model_call_details."""
logging_obj.model_call_details["litellm_params"] = {
- "api_key": "sk-proj-hunter2hunter2hunter2hunter2",
+ "api_key": "sk-TEST_ONLY_API_KEY_NOT_A_CREDENTIAL",
"aws_secret_access_key": _AWS_SECRET,
}
@@ -8039,7 +8043,7 @@ def test_empty_api_base_does_not_dump_call_state(logging_obj):
assert "litellm_call_id" not in curl_command
assert _AWS_SECRET not in curl_command
- assert "hunter2" not in curl_command
+ assert "sk-TEST_ONLY_API_KEY_NOT_A_CREDENTIAL" not in curl_command
def test_pre_call_redacts_and_masks_raw_request(logging_obj):
diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json
index d6b98d9be80..c6f45fd19f5 100644
--- a/ui/litellm-dashboard/package-lock.json
+++ b/ui/litellm-dashboard/package-lock.json
@@ -29,7 +29,7 @@
"jwt-decode": "4.0.0",
"lucide-react": "0.513.0",
"moment": "2.31.0",
- "next": "16.3.8",
+ "next": "16.4.1",
"next-themes": "^0.4.6",
"nuqs": "^2.9.4",
"openai": "6.49.0",
@@ -71,8 +71,8 @@
"@types/react-copy-to-clipboard": "5.0.7",
"@types/react-dom": "19.2.4",
"@types/react-syntax-highlighter": "15.5.13",
- "@vitest/coverage-v8": "4.1.11",
- "@vitest/ui": "4.1.11",
+ "@vitest/coverage-v8": "4.1.12",
+ "@vitest/ui": "4.1.12",
"eslint": "9.39.2",
"eslint-config-next": "16.3.3",
"eslint-config-prettier": "10.1.8",
@@ -89,7 +89,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.60.1",
"vite": "7.3.7",
- "vitest": "4.1.11"
+ "vitest": "4.1.12"
},
"engines": {
"node": ">=24.14.1",
@@ -2155,9 +2155,9 @@
}
},
"node_modules/@next/env": {
- "version": "16.3.8",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.8.tgz",
- "integrity": "sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==",
+ "version": "16.4.1",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.1.tgz",
+ "integrity": "sha512-5sUUrrSvs/2/2LnLoesybMGx12EtPB46fdZ6RhAw0XDGSPxU2LFyQBB7yuEiOFFJPKmBY6+q2jTr1/AMbnp0/g==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -2172,9 +2172,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "16.3.8",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.8.tgz",
- "integrity": "sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==",
+ "version": "16.4.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.1.tgz",
+ "integrity": "sha512-U+hQ5Gj3fpzf4fveTIF3dpUFYhaSW8vHhdAnbiIomzXzg7boreYFgBqprepXFtW4sw+aTttiNmg0gew/iFNhbg==",
"cpu": [
"arm64"
],
@@ -2188,9 +2188,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "16.3.8",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.8.tgz",
- "integrity": "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==",
+ "version": "16.4.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.1.tgz",
+ "integrity": "sha512-/hqC3V1XLUjhMASf/WridKdO9FXoEgN3HBWmtKbj00MPgnltutfraWJ3bYTpCRIgEIpbjJyOVLpYgAhh/BkdcQ==",
"cpu": [
"x64"
],
@@ -2204,15 +2204,12 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.3.8",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.8.tgz",
- "integrity": "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==",
+ "version": "16.4.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.1.tgz",
+ "integrity": "sha512-GHzncHbcaS9g95kcvTNtKQW3aCLoR+xc3Jm7qrkpk1R+D2BjMHIPyvQGRPKCdMX95BsH/421jBUK06q9tEwzjw==",
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2223,15 +2220,12 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.3.8",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.8.tgz",
- "integrity": "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==",
+ "version": "16.4.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.1.tgz",
+ "integrity": "sha512-1SuJyDxW/PR9dHJat/ZRgcYLhuSZ+1psP4DlKb2tKIuXbjygqpLrJWRSq+onZQBv8LcmU67EIrmb6c3XTaJLjw==",
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2242,15 +2236,12 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.3.8",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.8.tgz",
- "integrity": "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==",
+ "version": "16.4.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.1.tgz",
+ "integrity": "sha512-Istng1hMGp9TGO4lUqeKLd70MPN/OOkucVTP2Nmj+gLkwBgmPqUw8CJYBCC7nbl/B/23bBCcjCI5Hsh6hazC1A==",
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2261,15 +2252,12 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "16.3.8",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.8.tgz",
- "integrity": "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==",
+ "version": "16.4.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.1.tgz",
+ "integrity": "sha512-aSC9wMNiZRRFVpPkPWBzRwLQxIGm/CiQcXyFF1XNcgodb+uMxdk7aSSGGpDXCbnvZdf4itjHxpOd5h7i7zaiYw==",
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2280,9 +2268,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.3.8",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.8.tgz",
- "integrity": "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==",
+ "version": "16.4.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.1.tgz",
+ "integrity": "sha512-t/myvwFDlypMUjRvbBf8qLPyuaB0iXYYdBtwMt6DM6f+60wtGr4bSYholdPqNeV5VUXZhrpMwfza829kmhS3iA==",
"cpu": [
"arm64"
],
@@ -2296,9 +2284,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.3.8",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.8.tgz",
- "integrity": "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==",
+ "version": "16.4.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.1.tgz",
+ "integrity": "sha512-6ICqPo9awCkrtYNPhwkx1uU7rlFWrZRrIboDHS2LU+jv088UBYQtmJ25h6pCp27/+FZjF6BQ4Te4ErugyMeuBg==",
"cpu": [
"x64"
],
@@ -4492,14 +4480,14 @@
]
},
"node_modules/@vitest/coverage-v8": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz",
- "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.12.tgz",
+ "integrity": "sha512-6S7/iVknKglLAxMFxQGNppBI0iqJS2NgM6FBxavitj7KaDq/vywOWGf6dGu7F2L1Qy0G6S3uTp7aGshHrzK8JA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
- "@vitest/utils": "4.1.11",
+ "@vitest/utils": "4.1.12",
"ast-v8-to-istanbul": "^1.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
@@ -4513,8 +4501,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "@vitest/browser": "4.1.11",
- "vitest": "4.1.11"
+ "@vitest/browser": "4.1.12",
+ "vitest": "4.1.12"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -4523,16 +4511,16 @@
}
},
"node_modules/@vitest/expect": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
- "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.12.tgz",
+ "integrity": "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.11",
- "@vitest/utils": "4.1.11",
+ "@vitest/spy": "4.1.12",
+ "@vitest/utils": "4.1.12",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -4541,13 +4529,13 @@
}
},
"node_modules/@vitest/mocker": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
- "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz",
+ "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.1.11",
+ "@vitest/spy": "4.1.12",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -4568,9 +4556,9 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
- "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.12.tgz",
+ "integrity": "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4581,13 +4569,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
- "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz",
+ "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.11",
+ "@vitest/utils": "4.1.12",
"pathe": "^2.0.3"
},
"funding": {
@@ -4595,14 +4583,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
- "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.12.tgz",
+ "integrity": "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.11",
- "@vitest/utils": "4.1.11",
+ "@vitest/pretty-format": "4.1.12",
+ "@vitest/utils": "4.1.12",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -4611,9 +4599,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
- "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz",
+ "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==",
"dev": true,
"license": "MIT",
"funding": {
@@ -4621,13 +4609,13 @@
}
},
"node_modules/@vitest/ui": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.11.tgz",
- "integrity": "sha512-r/rwyKoev21mWdRGSEkZOqkQ2BYy68mwjihg9M90nNRbf4NGrgzZ4cj6JNCEwlOGJkbKeMgsjlykvwKUbRr7gw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.12.tgz",
+ "integrity": "sha512-eYUTHtsp52Fu6YGUw9T9173bba7YUr9j/Xj9CdhUaOb8AppLKcknHUaiZQeqJ8MiJ8C2VyKiZuUGSKdvPIKQKA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.11",
+ "@vitest/utils": "4.1.12",
"fflate": "^0.8.2",
"flatted": "^3.4.2",
"pathe": "^2.0.3",
@@ -4639,17 +4627,17 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "vitest": "4.1.11"
+ "vitest": "4.1.12"
}
},
"node_modules/@vitest/utils": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
- "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.12.tgz",
+ "integrity": "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.11",
+ "@vitest/pretty-format": "4.1.12",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -9749,12 +9737,12 @@
"license": "MIT"
},
"node_modules/next": {
- "version": "16.3.8",
- "resolved": "https://registry.npmjs.org/next/-/next-16.3.8.tgz",
- "integrity": "sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==",
+ "version": "16.4.1",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.4.1.tgz",
+ "integrity": "sha512-uc657V62E34fOI6W5NtG9IzwsPcms0Z60grz2MJGtglgD+pTFnNt4Yw2PzaU622tfX4vhrDgLZzicE2YlhI20Q==",
"license": "MIT",
"dependencies": {
- "@next/env": "16.3.8",
+ "@next/env": "16.4.1",
"@swc/helpers": "0.5.23",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
@@ -9768,15 +9756,15 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "16.3.8",
- "@next/swc-darwin-x64": "16.3.8",
- "@next/swc-linux-arm64-gnu": "16.3.8",
- "@next/swc-linux-arm64-musl": "16.3.8",
- "@next/swc-linux-x64-gnu": "16.3.8",
- "@next/swc-linux-x64-musl": "16.3.8",
- "@next/swc-win32-arm64-msvc": "16.3.8",
- "@next/swc-win32-x64-msvc": "16.3.8",
- "sharp": "^0.35.4"
+ "@next/swc-darwin-arm64": "16.4.1",
+ "@next/swc-darwin-x64": "16.4.1",
+ "@next/swc-linux-arm64-gnu": "16.4.1",
+ "@next/swc-linux-arm64-musl": "16.4.1",
+ "@next/swc-linux-x64-gnu": "16.4.1",
+ "@next/swc-linux-x64-musl": "16.4.1",
+ "@next/swc-win32-arm64-msvc": "16.4.1",
+ "@next/swc-win32-x64-msvc": "16.4.1",
+ "sharp": "^0.35.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@@ -12578,19 +12566,19 @@
}
},
"node_modules/vitest": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
- "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz",
+ "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/expect": "4.1.11",
- "@vitest/mocker": "4.1.11",
- "@vitest/pretty-format": "4.1.11",
- "@vitest/runner": "4.1.11",
- "@vitest/snapshot": "4.1.11",
- "@vitest/spy": "4.1.11",
- "@vitest/utils": "4.1.11",
+ "@vitest/expect": "4.1.12",
+ "@vitest/mocker": "4.1.12",
+ "@vitest/pretty-format": "4.1.12",
+ "@vitest/runner": "4.1.12",
+ "@vitest/snapshot": "4.1.12",
+ "@vitest/spy": "4.1.12",
+ "@vitest/utils": "4.1.12",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -12618,12 +12606,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.11",
- "@vitest/browser-preview": "4.1.11",
- "@vitest/browser-webdriverio": "4.1.11",
- "@vitest/coverage-istanbul": "4.1.11",
- "@vitest/coverage-v8": "4.1.11",
- "@vitest/ui": "4.1.11",
+ "@vitest/browser-playwright": "4.1.12",
+ "@vitest/browser-preview": "4.1.12",
+ "@vitest/browser-webdriverio": "^4.1.11",
+ "@vitest/coverage-istanbul": "4.1.12",
+ "@vitest/coverage-v8": "4.1.12",
+ "@vitest/ui": "4.1.12",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json
index 8d1b7752b25..432e327ec5a 100644
--- a/ui/litellm-dashboard/package.json
+++ b/ui/litellm-dashboard/package.json
@@ -46,7 +46,7 @@
"jwt-decode": "4.0.0",
"lucide-react": "0.513.0",
"moment": "2.31.0",
- "next": "16.3.8",
+ "next": "16.4.1",
"next-themes": "^0.4.6",
"nuqs": "^2.9.4",
"openai": "6.49.0",
@@ -88,8 +88,8 @@
"@types/react-copy-to-clipboard": "5.0.7",
"@types/react-dom": "19.2.4",
"@types/react-syntax-highlighter": "15.5.13",
- "@vitest/coverage-v8": "4.1.11",
- "@vitest/ui": "4.1.11",
+ "@vitest/coverage-v8": "4.1.12",
+ "@vitest/ui": "4.1.12",
"eslint": "9.39.2",
"eslint-config-next": "16.3.3",
"eslint-config-prettier": "10.1.8",
@@ -106,7 +106,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.60.1",
"vite": "7.3.7",
- "vitest": "4.1.11"
+ "vitest": "4.1.12"
},
"overrides": {
"prismjs": "1.30.0",
@@ -119,7 +119,10 @@
"axios": "1.13.6",
"postcss": "8.5.23",
"esbuild": "0.28.1",
- "sharp": "^0.35.5"
+ "sharp": "^0.35.5",
+ "@litellm/lens-ui": {
+ "next": "$next"
+ }
},
"engines": {
"node": ">=24.14.1",
diff --git a/uv.lock b/uv.lock
index 8d2a38c1806..17247935ebc 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2482,14 +2482,14 @@ wheels = [
[[package]]
name = "gitpython"
-version = "3.1.62"
+version = "3.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gitdb" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e0/db/3ca813cbacb23ab6fe46ff38a9b5ef8e73e970c8051f2ce903aacafe0446/gitpython-3.1.62.tar.gz", hash = "sha256:1791de66309bc0c7cfca40bf8d2e3de7ca091cbf94e6051be1ad0722c61062af", size = 231728, upload-time = "2026-09-07T02:57:21.155Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c0/ff/fee293f1ec3e1b64db6eb270e00964f29c086d769201474cedb8d1608930/gitpython-3.2.1.tar.gz", hash = "sha256:0f1229ec5c057bdabdeeda8bda4f17bda679ab33dbccc00f24e6a480553c8613", size = 241688, upload-time = "2026-10-14T12:20:20.589Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d6/0b/29d7965215f8ef830a7ca1f42997fe13e5693d85e9edb18f938d063ef5f2/gitpython-3.1.62-py3-none-any.whl", hash = "sha256:7002251225e10e29d2e1f49e6532613fe5d5d9f0b6f1f02997a52b38fe56899e", size = 222753, upload-time = "2026-09-07T02:57:19.762Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/50/2a923c4dc02522ff9567f07c67ed1deca6d5fa9474f4708168b8088dfa37/gitpython-3.2.1-py3-none-any.whl", hash = "sha256:d66b0832aa8755c1a84892937282df0befb16c97e5a64aeebd68be9c5e323f53", size = 231548, upload-time = "2026-10-14T12:20:18.844Z" },
]
[[package]]
@@ -2987,11 +2987,11 @@ wheels = [
[[package]]
name = "graphql-core"
-version = "3.2.8"
+version = "3.2.13"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/68/c5/36aa96205c3ecbb3d34c7c24189e4553c7ca2ebc7e1dd07432339b980272/graphql_core-3.2.8.tar.gz", hash = "sha256:015457da5d996c924ddf57a43f4e959b0b94fb695b85ed4c29446e508ed65cf3", size = 513181, upload-time = "2026-03-05T19:55:37.332Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/5e/aa0d4e701b50db0bab71b125dd19ddb0f98c638d008d6f7e3d8ce9cbc92e/graphql_core-3.2.13.tar.gz", hash = "sha256:bb81dd266d4ab7b591bd976f1b23639d97776cb9ac1a896b4a93c271e11ed618", size = 577471, upload-time = "2026-09-27T12:22:57.136Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/86/41/cb887d9afc5dabd78feefe6ccbaf83ff423c206a7a1b7aeeac05120b2125/graphql_core-3.2.8-py3-none-any.whl", hash = "sha256:cbee07bee1b3ed5e531723685369039f32ff815ef60166686e0162f540f1520c", size = 207349, upload-time = "2026-03-05T19:55:35.911Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/b4/b52a324b0b297ddc49b8ea31a51d39fae63132f694a267b9381d94e2e0af/graphql_core-3.2.13-py3-none-any.whl", hash = "sha256:b0eb04f2c31556b2310a77c8fb53c74e8b56570f8fea594d89c6ef7827dbb497", size = 268191, upload-time = "2026-09-27T12:22:55.661Z" },
]
[[package]]
@@ -4727,6 +4727,7 @@ proxy-runtime = [
{ name = "mangum" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp" },
+ { name = "opentelemetry-exporter-otlp-json-http" },
{ name = "opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-sdk" },
{ name = "prometheus-client" },
@@ -4810,6 +4811,7 @@ dev = [
{ name = "openapi-core" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp" },
+ { name = "opentelemetry-exporter-otlp-json-http" },
{ name = "opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-sdk" },
{ name = "parameterized" },
@@ -4857,6 +4859,7 @@ proxy-dev = [
{ name = "litellm-admin-mcp", marker = "python_full_version >= '3.12'" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp" },
+ { name = "opentelemetry-exporter-otlp-json-http" },
{ name = "opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-sdk" },
{ name = "prisma" },
@@ -4930,10 +4933,11 @@ requires-dist = [
{ name = "numpydoc", marker = "extra == 'utils'", specifier = ">=1.8.0,<2.0" },
{ name = "nvidia-riva-client", marker = "extra == 'stt-nvidia-riva'", specifier = ">=2.15.0" },
{ name = "openai", specifier = ">=2.20.0,<4.0.0" },
- { name = "opentelemetry-api", marker = "extra == 'proxy-runtime'", specifier = "==1.33.1" },
- { name = "opentelemetry-exporter-otlp", marker = "extra == 'proxy-runtime'", specifier = "==1.33.1" },
- { name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'proxy-runtime'", specifier = "==0.54b1" },
- { name = "opentelemetry-sdk", marker = "extra == 'proxy-runtime'", specifier = "==1.33.1" },
+ { name = "opentelemetry-api", marker = "extra == 'proxy-runtime'", specifier = "==1.45.1" },
+ { name = "opentelemetry-exporter-otlp", marker = "extra == 'proxy-runtime'", specifier = "==1.45.1" },
+ { name = "opentelemetry-exporter-otlp-json-http", marker = "extra == 'proxy-runtime'", specifier = "==0.66b1" },
+ { name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'proxy-runtime'", specifier = "==0.66b1" },
+ { name = "opentelemetry-sdk", marker = "extra == 'proxy-runtime'", specifier = "==1.45.1" },
{ name = "orjson", marker = "extra == 'proxy'", specifier = ">=3.11.6,<4.0" },
{ name = "packaging", specifier = ">=24.0" },
{ name = "polars", marker = "extra == 'proxy'", specifier = ">=1.38.1,<2.0" },
@@ -4948,7 +4952,7 @@ requires-dist = [
{ name = "pydantic-settings", specifier = ">=2.14.1,<3.0" },
{ name = "pyjwt", marker = "extra == 'proxy'", specifier = ">=2.13.0,<3.0" },
{ name = "pynacl", marker = "extra == 'proxy'", specifier = ">=1.6.2,<2.0" },
- { name = "pypdf", marker = "extra == 'proxy-runtime'", specifier = ">=6.16.1,<7.0" },
+ { name = "pypdf", marker = "extra == 'proxy-runtime'", specifier = ">=6.21.1,<7.0" },
{ name = "pyroscope-io", marker = "sys_platform != 'win32' and extra == 'proxy'", specifier = ">=0.8.16,<1.0" },
{ name = "python-dotenv", specifier = ">=1.0.0,<2.0" },
{ name = "python-multipart", marker = "extra == 'proxy'", specifier = ">=0.0.27,<1.0" },
@@ -5004,7 +5008,7 @@ ci = [
{ name = "langchain-openai", specifier = "==1.1.14" },
{ name = "langgraph", specifier = ">=1.2.4,<1.3.0" },
{ name = "langgraph-prebuilt", specifier = ">=1.1.0,<1.3.0" },
- { name = "logfire", specifier = "==4.6.0" },
+ { name = "logfire", specifier = "==5.2.0" },
{ name = "lunary", marker = "python_full_version == '3.10.*'", specifier = "==1.4.36" },
{ name = "lunary", marker = "python_full_version >= '3.11'", specifier = "==1.4.37" },
{ name = "pillow", marker = "python_full_version == '3.10.*'", specifier = "==12.3.0" },
@@ -5031,10 +5035,11 @@ dev = [
{ name = "mypy", specifier = "==1.20.1" },
{ name = "numpy", specifier = ">=1.26.0,<3.0" },
{ name = "openapi-core", specifier = "==0.22.0" },
- { name = "opentelemetry-api", specifier = "==1.33.1" },
- { name = "opentelemetry-exporter-otlp", specifier = "==1.33.1" },
- { name = "opentelemetry-instrumentation-fastapi", specifier = "==0.54b1" },
- { name = "opentelemetry-sdk", specifier = "==1.33.1" },
+ { name = "opentelemetry-api", specifier = "==1.45.1" },
+ { name = "opentelemetry-exporter-otlp", specifier = "==1.45.1" },
+ { name = "opentelemetry-exporter-otlp-json-http", specifier = "==0.66b1" },
+ { name = "opentelemetry-instrumentation-fastapi", specifier = "==0.66b1" },
+ { name = "opentelemetry-sdk", specifier = "==1.45.1" },
{ name = "parameterized", specifier = "==0.9.0" },
{ name = "psycopg", specifier = "==3.3.3" },
{ name = "psycopg-binary", specifier = "==3.3.3" },
@@ -5078,10 +5083,11 @@ proxy-dev = [
{ name = "backports-zstd", marker = "python_full_version < '3.14'", specifier = "==1.8.0" },
{ name = "hypercorn", specifier = "==0.17.3" },
{ name = "litellm-admin-mcp", marker = "python_full_version >= '3.12'", url = "https://github.com/BerriAI/liteadmin-mcp/archive/d35ec9c19c117d4c50cc1eccf6ce1296aac25a1a.tar.gz" },
- { name = "opentelemetry-api", specifier = "==1.33.1" },
- { name = "opentelemetry-exporter-otlp", specifier = "==1.33.1" },
- { name = "opentelemetry-instrumentation-fastapi", specifier = "==0.54b1" },
- { name = "opentelemetry-sdk", specifier = "==1.33.1" },
+ { name = "opentelemetry-api", specifier = "==1.45.1" },
+ { name = "opentelemetry-exporter-otlp", specifier = "==1.45.1" },
+ { name = "opentelemetry-exporter-otlp-json-http", specifier = "==0.66b1" },
+ { name = "opentelemetry-instrumentation-fastapi", specifier = "==0.66b1" },
+ { name = "opentelemetry-sdk", specifier = "==1.45.1" },
{ name = "prisma", specifier = "==0.11.0" },
{ name = "prometheus-client", specifier = "==0.20.0" },
{ name = "sentry-sdk", specifier = "==2.21.0" },
@@ -5173,21 +5179,24 @@ wheels = [
[[package]]
name = "logfire"
-version = "4.6.0"
+version = "5.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "executing" },
+ { name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-sdk" },
+ { name = "opentelemetry-semantic-conventions" },
{ name = "protobuf" },
+ { name = "requests" },
{ name = "rich" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/19/fa/14cbd976e8ff77290c0c87f93c45029785537ffb84e2f5215416786cda11/logfire-4.6.0.tar.gz", hash = "sha256:974bc8f4efd3aa9df2d0c7b1d53b35dc4612ff5cee2be6c40dd65e5c26eab694", size = 533174, upload-time = "2025-09-10T14:56:42.168Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/56/5a/e6cccc98d30358b8beee47c61c2a1ca66ac7a76a07de9ba333aa5219b9a6/logfire-5.2.0.tar.gz", hash = "sha256:5fb9ddda303759766e86f5704a1ae96f3a0072906d52e21fbaad2e3c3b03f823", size = 1404990, upload-time = "2026-10-13T17:36:30.746Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/04/9c/6384dc69601bfba19c1f5909a472b00cc4b5863f411ecf85be308beacf8f/logfire-4.6.0-py3-none-any.whl", hash = "sha256:d322844db7a2f1935976f2852a07e3753489ecc4e83fa288173b05a453bb1cbf", size = 219556, upload-time = "2025-09-10T14:56:37.278Z" },
+ { url = "https://files.pythonhosted.org/packages/73/cb/bfcd484c16eeac8cdeb2defe1a2ee39bb1b4b6e69324b85515e7818eb678/logfire-5.2.0-py3-none-any.whl", hash = "sha256:c73dd02cfe048f2254e803962aa992d7f53217d121f3488e797926c30442342c", size = 495128, upload-time = "2026-10-13T17:36:27.305Z" },
]
[[package]]
@@ -6306,81 +6315,144 @@ wheels = [
[[package]]
name = "opentelemetry-api"
-version = "1.33.1"
+version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "deprecated" },
- { name = "importlib-metadata" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9a/8d/1f5a45fbcb9a7d87809d460f09dc3399e3fbd31d7f3e14888345e9d29951/opentelemetry_api-1.33.1.tar.gz", hash = "sha256:1c6055fc0a2d3f23a50c7e17e16ef75ad489345fd3df1f8b8af7c0bbf8a109e8", size = 65002, upload-time = "2025-05-16T18:52:41.146Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/05/44/4c45a34def3506122ae61ad684139f0bbc4e00c39555d4f7e20e0e001c8a/opentelemetry_api-1.33.1-py3-none-any.whl", hash = "sha256:4db83ebcf7ea93e64637ec6ee6fabee45c5cbe4abd9cf3da95c43828ddb50b83", size = 65771, upload-time = "2025-05-16T18:52:17.419Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-http-transport"
+version = "0.66b1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/62/0c/e3ebdb4b507f66afcc905e6885a4946969bd75b45988492643356fbbdc63/opentelemetry_exporter_http_transport-0.66b1.tar.gz", hash = "sha256:443080203bf52586ce0b2ad901e8951c61833eab1aa539ae6f1f16fe9e8e7952", size = 11693, upload-time = "2026-10-06T17:32:59.65Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/69/6af86ff66492b481c6a4c05dcfd68beb47ed8ba046440a26a2aac76b95c7/opentelemetry_exporter_http_transport-0.66b1-py3-none-any.whl", hash = "sha256:2f95404bdee7f9d2d529c7de56c7bd86d014d774d8fbf137810e0167f8a492bf", size = 12155, upload-time = "2026-10-06T17:32:35.454Z" },
+]
+
+[package.optional-dependencies]
+requests = [
+ { name = "requests" },
+]
+urllib3 = [
+ { name = "urllib3" },
]
[[package]]
name = "opentelemetry-exporter-otlp"
-version = "1.33.1"
+version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-exporter-otlp-proto-grpc" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/3f/c8ad4f1c3aaadcea2b0f1b4d7970e7b7898c145699769a789f3435143f69/opentelemetry_exporter_otlp-1.33.1.tar.gz", hash = "sha256:4d050311ea9486e3994575aa237e32932aad58330a31fba24fdba5c0d531cf04", size = 6189, upload-time = "2025-05-16T18:52:43.176Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/6f/5a561048ea372894f22f58f70e8478e4cd97d9a4e2d6f014559584a29bc5/opentelemetry_exporter_otlp-1.45.1.tar.gz", hash = "sha256:d0ac35592e77663a9fabf2740b4818c57196b1b764e0c8449d0d7bb2c7b2bc67", size = 6094, upload-time = "2026-10-06T17:33:00.966Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4d/32/b9add70dd4e845654fc9fcd1401a705477743880be6c3e62acb1ad0d8662/opentelemetry_exporter_otlp-1.33.1-py3-none-any.whl", hash = "sha256:9bcf1def35b880b55a49e31ebd63910edac14b294fd2ab884953c4deaff5b300", size = 7045, upload-time = "2025-05-16T18:52:21.022Z" },
+ { url = "https://files.pythonhosted.org/packages/36/e1/68c28d7da4482ce882a640b2c61405a78284c0a3a17a4db125647189b0b7/opentelemetry_exporter_otlp-1.45.1-py3-none-any.whl", hash = "sha256:ef3910d32b36ccbaf62390189759bd43a8109885e2d50b738ed9c9b255bf5cb5", size = 6730, upload-time = "2026-10-06T17:32:37.236Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-common"
+version = "0.66b1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-sdk" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cb/19/41de712173f43057e4532d42ece7d0c6d4210d353e5752433cb14987643f/opentelemetry_exporter_otlp_common-0.66b1.tar.gz", hash = "sha256:6b1403487a2185ac1feb45fd5546fdf8630ce71c36bcefaadf51e2130e9e23f9", size = 14325, upload-time = "2026-10-06T17:33:01.725Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/39/8c23d67665c762aa51840fa06f86e902e8f6f1693bc8d7e3d98cd6e2f753/opentelemetry_exporter_otlp_common-0.66b1-py3-none-any.whl", hash = "sha256:00ff8592c3a7cb729ff3fdc7ffa12372c243bdf2163e80c180994d0c7bd83ee9", size = 12385, upload-time = "2026-10-06T17:32:38.177Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-json-common"
+version = "0.66b1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-proto-json" },
+ { name = "opentelemetry-sdk" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ca/27/fd51023a4916ad00ef0c970577e148fba36e3682716d15a1495f74f830fb/opentelemetry_exporter_otlp_json_common-0.66b1.tar.gz", hash = "sha256:d8bd656ee03be5d2f54b533e03e297792c5017501417d132c3c67eb2ac95b3de", size = 24519, upload-time = "2026-10-06T17:33:02.52Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/34/6e7a4794ff9995042c8372af5c7e4e5127aea10dfeec64402fbec93a6d43/opentelemetry_exporter_otlp_json_common-0.66b1-py3-none-any.whl", hash = "sha256:1ae23eb3dfcd756607367c1ff5df2b4120fe481a523e7868b26b61720c558875", size = 14826, upload-time = "2026-10-06T17:32:39.052Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-json-http"
+version = "0.66b1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-exporter-http-transport", extra = ["urllib3"] },
+ { name = "opentelemetry-exporter-otlp-common" },
+ { name = "opentelemetry-exporter-otlp-json-common" },
+ { name = "opentelemetry-proto-json" },
+ { name = "opentelemetry-sdk" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/44/30/47c00f9329242b71724df87536613cb21596b41f294cdc37d8b216c5afaa/opentelemetry_exporter_otlp_json_http-0.66b1.tar.gz", hash = "sha256:940ca96fff3509ca06b3ce8733d4818ee022620aa9329cb7025c63e43c2d224d", size = 17583, upload-time = "2026-10-06T17:33:03.868Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e2/2d/469689a34b17ed83d4ddee480db8322246dcffeb95ef5fc5050bb8a16c8c/opentelemetry_exporter_otlp_json_http-0.66b1-py3-none-any.whl", hash = "sha256:4bd7607120fb7cc77aae88d5c7f629c40515d225b3ca5db293d20d091a248156", size = 14523, upload-time = "2026-10-06T17:32:40.837Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
-version = "1.33.1"
+version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/7a/18/a1ec9dcb6713a48b4bdd10f1c1e4d5d2489d3912b80d2bcc059a9a842836/opentelemetry_exporter_otlp_proto_common-1.33.1.tar.gz", hash = "sha256:c57b3fa2d0595a21c4ed586f74f948d259d9949b58258f11edb398f246bec131", size = 20828, upload-time = "2025-05-16T18:52:43.795Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c1/8e/65e85e5137991a3c493b11682151d198638a5bc1dd4b4c5f67e013c57d7c/opentelemetry_exporter_otlp_proto_common-1.45.1.tar.gz", hash = "sha256:2e4adcc3a67bcf57804fc49514f0ef64974ca7590aa3491da389852b4a0628f6", size = 18873, upload-time = "2026-10-06T17:33:04.471Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/09/52/9bcb17e2c29c1194a28e521b9d3f2ced09028934c3c52a8205884c94b2df/opentelemetry_exporter_otlp_proto_common-1.33.1-py3-none-any.whl", hash = "sha256:b81c1de1ad349785e601d02715b2d29d6818aed2c809c20219f3d1f20b038c36", size = 18839, upload-time = "2025-05-16T18:52:22.447Z" },
+ { url = "https://files.pythonhosted.org/packages/84/aa/92f225d353904e7f70b8b3e3c1b02db0cf56f744c2e83c581dc372e78873/opentelemetry_exporter_otlp_proto_common-1.45.1-py3-none-any.whl", hash = "sha256:2f446183ae7047b036226f1d846c41a834b0e8755ad13b51a51dd38952eb466c", size = 15393, upload-time = "2026-10-06T17:32:41.911Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-grpc"
-version = "1.33.1"
+version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "deprecated" },
{ name = "googleapis-common-protos" },
{ name = "grpcio" },
{ name = "opentelemetry-api" },
+ { name = "opentelemetry-exporter-otlp-common" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d8/5f/75ef5a2a917bd0e6e7b83d3fb04c99236ee958f6352ba3019ea9109ae1a6/opentelemetry_exporter_otlp_proto_grpc-1.33.1.tar.gz", hash = "sha256:345696af8dc19785fac268c8063f3dc3d5e274c774b308c634f39d9c21955728", size = 22556, upload-time = "2025-05-16T18:52:44.76Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/00/a82af0be959dc58495740b169c6669a86e0811f6cd353a01eda34d255db3/opentelemetry_exporter_otlp_proto_grpc-1.45.1.tar.gz", hash = "sha256:3b3dcfbfdcb4e35149fcf309972282054b45228f5c10547d0095d6578510a9a0", size = 25997, upload-time = "2026-10-06T17:33:05.114Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ba/ec/6047e230bb6d092c304511315b13893b1c9d9260044dd1228c9d48b6ae0e/opentelemetry_exporter_otlp_proto_grpc-1.33.1-py3-none-any.whl", hash = "sha256:7e8da32c7552b756e75b4f9e9c768a61eb47dee60b6550b37af541858d669ce1", size = 18591, upload-time = "2025-05-16T18:52:23.772Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/46/2d1da202f1e17c81aae7efcf702898d524b46709e4d3e2bf1f7f8ca8fbc6/opentelemetry_exporter_otlp_proto_grpc-1.45.1-py3-none-any.whl", hash = "sha256:e42ecb789d2fc5d8145e3dadc3e2991c9f18cd166d7c7514e234702540274b76", size = 19492, upload-time = "2026-10-06T17:32:42.838Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
-version = "1.33.1"
+version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "deprecated" },
{ name = "googleapis-common-protos" },
{ name = "opentelemetry-api" },
+ { name = "opentelemetry-exporter-http-transport", extra = ["requests"] },
+ { name = "opentelemetry-exporter-otlp-common" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "requests" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/60/48/e4314ac0ed2ad043c07693d08c9c4bf5633857f5b72f2fefc64fd2b114f6/opentelemetry_exporter_otlp_proto_http-1.33.1.tar.gz", hash = "sha256:46622d964a441acb46f463ebdc26929d9dec9efb2e54ef06acdc7305e8593c38", size = 15353, upload-time = "2025-05-16T18:52:45.522Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/17/26487707ea4caa97b17e6e4b5fa72133a53512ffa2f5cf7a49ef284b29cb/opentelemetry_exporter_otlp_proto_http-1.45.1.tar.gz", hash = "sha256:45c218405ce3fd879596924b1874bf9a8f6880206d61065c5a912c8e5c297fb7", size = 28839, upload-time = "2026-10-06T17:33:05.713Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/63/ba/5a4ad007588016fe37f8d36bf08f325fe684494cc1e88ca8fa064a4c8f57/opentelemetry_exporter_otlp_proto_http-1.33.1-py3-none-any.whl", hash = "sha256:ebd6c523b89a2ecba0549adb92537cc2bf647b4ee61afbbd5a4c6535aa3da7cf", size = 17733, upload-time = "2025-05-16T18:52:25.137Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/1f/517eaa0187ba106a9da97160ce2add3a371812681dc440930b267f714e42/opentelemetry_exporter_otlp_proto_http-1.45.1-py3-none-any.whl", hash = "sha256:24a97cf3753c7fb52fad44a696e452ff371686339e2acf3309e2eda3d0230700", size = 22180, upload-time = "2026-10-06T17:32:43.946Z" },
]
[[package]]
name = "opentelemetry-instrumentation"
-version = "0.54b1"
+version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@@ -6388,9 +6460,9 @@ dependencies = [
{ name = "packaging" },
{ name = "wrapt" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c3/fd/5756aea3fdc5651b572d8aef7d94d22a0a36e49c8b12fcb78cb905ba8896/opentelemetry_instrumentation-0.54b1.tar.gz", hash = "sha256:7658bf2ff914b02f246ec14779b66671508125c0e4227361e56b5ebf6cef0aec", size = 28436, upload-time = "2025-05-16T19:03:22.223Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/03/89e47ff8d52a4f83b343e6eb9ef1698ff45357216e5b6b2b21e0da5c5c7d/opentelemetry_instrumentation-0.66b1.tar.gz", hash = "sha256:e79a510f7d87c72d95e964ddb42193a0d9a75668c027d980eab032ea1322a5ce", size = 43308, upload-time = "2026-10-06T17:36:10.703Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f4/89/0790abc5d9c4fc74bd3e03cb87afe2c820b1d1a112a723c1163ef32453ee/opentelemetry_instrumentation-0.54b1-py3-none-any.whl", hash = "sha256:a4ae45f4a90c78d7006c51524f57cd5aa1231aef031eae905ee34d5423f5b198", size = 31019, upload-time = "2025-05-16T19:02:15.611Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b2/d1413681ff43e13ac9860df27e1226d3199ab0b97b352ceea41abcc660a5/opentelemetry_instrumentation-0.66b1-py3-none-any.whl", hash = "sha256:4c4aa14dc9a24a02325a9d4c42c4d0208dbb1374c2b1b8fe6c9392d59f3e1008", size = 36904, upload-time = "2026-10-06T17:35:11.663Z" },
]
[[package]]
@@ -6425,7 +6497,7 @@ wheels = [
[[package]]
name = "opentelemetry-instrumentation-asgi"
-version = "0.54b1"
+version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
@@ -6434,9 +6506,9 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/20/f7/a3377f9771947f4d3d59c96841d3909274f446c030dbe8e4af871695ddee/opentelemetry_instrumentation_asgi-0.54b1.tar.gz", hash = "sha256:ab4df9776b5f6d56a78413c2e8bbe44c90694c67c844a1297865dc1bd926ed3c", size = 24230, upload-time = "2025-05-16T19:03:30.234Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/d9/ff522f5c3e340e9007554923b1a4d2ac451676f8757bafb3d0057f68b5c3/opentelemetry_instrumentation_asgi-0.66b1.tar.gz", hash = "sha256:78cdc5e45e897e16a8dac9d282e8d5bdf9af2d58e1313fa0bdd4a134c6f9dafc", size = 25992, upload-time = "2026-10-06T17:36:14.593Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/20/24/7a6f0ae79cae49927f528ecee2db55a5bddd87b550e310ce03451eae7491/opentelemetry_instrumentation_asgi-0.54b1-py3-none-any.whl", hash = "sha256:84674e822b89af563b283a5283c2ebb9ed585d1b80a1c27fb3ac20b562e9f9fc", size = 16338, upload-time = "2025-05-16T19:02:22.808Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ea/10ba99110bf3c9fb736af39c96ca8f3668b988cabb6b59309e058c44461c/opentelemetry_instrumentation_asgi-0.66b1-py3-none-any.whl", hash = "sha256:78b3f9bdf0fa38c65935a2ab46d59e0f9de873a51e0c95b0329f106e2ccb5274", size = 15812, upload-time = "2026-10-06T17:35:17.638Z" },
]
[[package]]
@@ -6487,7 +6559,7 @@ wheels = [
[[package]]
name = "opentelemetry-instrumentation-fastapi"
-version = "0.54b1"
+version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@@ -6496,9 +6568,9 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/98/3b/9a262cdc1a4defef0e52afebdde3e8add658cc6f922e39e9dcee0da98349/opentelemetry_instrumentation_fastapi-0.54b1.tar.gz", hash = "sha256:1fcad19cef0db7092339b571a59e6f3045c9b58b7fd4670183f7addc459d78df", size = 19325, upload-time = "2025-05-16T19:03:45.359Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5d/2a/cd4125b7acbea2ed17f1d31b58c184cb0a79fcb5541ceb4de90ffc6d8c01/opentelemetry_instrumentation_fastapi-0.66b1.tar.gz", hash = "sha256:584cf9d2c4417ff8b2d6ff2bc606bfe13c8b3456018bf94f50f2cf658492505b", size = 26634, upload-time = "2026-10-06T17:36:25.157Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/df/9c/6b2b0f9d6c5dea7528ae0bf4e461dd765b0ae35f13919cd452970bb0d0b3/opentelemetry_instrumentation_fastapi-0.54b1-py3-none-any.whl", hash = "sha256:fb247781cfa75fd09d3d8713c65e4a02bd1e869b00e2c322cc516d4b5429860c", size = 12125, upload-time = "2025-05-16T19:02:41.172Z" },
+ { url = "https://files.pythonhosted.org/packages/75/70/676928d537978acc7bff2ac8657bd0836ba608ffc8f65455238f1fa2bd0f/opentelemetry_instrumentation_fastapi-0.66b1-py3-none-any.whl", hash = "sha256:97f8ac8fd7537517f9e6988bd0aca04bfa5aad564bcd46c245530739e2be72d1", size = 14216, upload-time = "2026-10-06T17:35:32.827Z" },
]
[[package]]
@@ -6594,15 +6666,16 @@ wheels = [
[[package]]
name = "opentelemetry-instrumentation-logging"
-version = "0.54b1"
+version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d9/5b/88ed39f22e8c6eb4f6192ab9a62adaa115579fcbcadb3f0241ee645eea56/opentelemetry_instrumentation_logging-0.54b1.tar.gz", hash = "sha256:893a3cbfda893b64ff71b81991894e2fd6a9267ba85bb6c251f51c0419fbe8fa", size = 9976, upload-time = "2025-05-16T19:03:49.976Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/28/ca/0fcf525f238f4ab9df3a6d0423fb057085ea39a3e79ac912fef3d5fa16b8/opentelemetry_instrumentation_logging-0.66b1.tar.gz", hash = "sha256:2250240ecb1fce36939f64eafd988832338684564fb4c7b401c9b9af0ee11d46", size = 20353, upload-time = "2026-10-06T17:36:29.546Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/96/0c/b441fb30d860f25040eaed61e89d68f4d9ee31873159ed18cbc1b92eba56/opentelemetry_instrumentation_logging-0.54b1-py3-none-any.whl", hash = "sha256:01a4cec54348f13941707d857b850b0febf9d49f45d0fcf0673866e079d7357b", size = 12579, upload-time = "2025-05-16T19:02:49.039Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/7b/76bea1e80787848a9ac9882a598cbf1e761f16cebdb399d0e675221e34a0/opentelemetry_instrumentation_logging-0.66b1-py3-none-any.whl", hash = "sha256:f13761110c52eecae9d7debedb0f88b46acc248c20437e97c2a5b3a2b4f96a02", size = 15928, upload-time = "2026-10-06T17:35:39.915Z" },
]
[[package]]
@@ -6728,7 +6801,7 @@ wheels = [
[[package]]
name = "opentelemetry-instrumentation-requests"
-version = "0.54b1"
+version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@@ -6736,9 +6809,9 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e7/45/116da84930d3dc2f5cdd876283ca96e9b96547bccee7eaa0bd01ce6bf046/opentelemetry_instrumentation_requests-0.54b1.tar.gz", hash = "sha256:3eca5d697c5564af04c6a1dd23b6a3ffbaf11e64887c6051655cee03998f4654", size = 15148, upload-time = "2025-05-16T19:04:00.488Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/08/62/dcca0b7a2008675056040c61b70d37b5a24439613c7822068908ad3255ae/opentelemetry_instrumentation_requests-0.66b1.tar.gz", hash = "sha256:28578f72e68e3a5be3226c618ac9570ed360ef1dd22d5d6e0721c6905a4ccc67", size = 17985, upload-time = "2026-10-06T17:36:37.478Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2b/b1/6e33d2c3d3cc9e3ae20a9a77625ec81a509a0e5d7fa87e09e7f879468990/opentelemetry_instrumentation_requests-0.54b1-py3-none-any.whl", hash = "sha256:a0c4cd5d946224f336d6bd73cdabdecc6f80d5c39208f84eb96eb15f16cd41a0", size = 12968, upload-time = "2025-05-16T19:03:03.131Z" },
+ { url = "https://files.pythonhosted.org/packages/88/01/98e9bb7fef12784cd19811e454cf25bce4d774fd3e46b3f5fd2bce237075/opentelemetry_instrumentation_requests-0.66b1-py3-none-any.whl", hash = "sha256:7ba17d984a2bd876b88bf0aafcca27b5e3ff4c1821e0fe07641e380a3cf3a3a2", size = 13320, upload-time = "2026-10-06T17:35:52.477Z" },
]
[[package]]
@@ -6758,7 +6831,7 @@ wheels = [
[[package]]
name = "opentelemetry-instrumentation-sqlalchemy"
-version = "0.54b1"
+version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@@ -6767,23 +6840,23 @@ dependencies = [
{ name = "packaging" },
{ name = "wrapt" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ac/33/78a25ae4233d42058bb0b363ba4fea7d7210e53c24e5e31f16d5cf6cf957/opentelemetry_instrumentation_sqlalchemy-0.54b1.tar.gz", hash = "sha256:97839acf1c9b96ded857fca57a09b86a56cf8d9eb6d706b7ceaee9352a460e03", size = 14620, upload-time = "2025-05-16T19:04:01.215Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d4/3e/d69fb08dacc4c248daedf7357732ddca7a0aaff7072a55e311d3da3ea51c/opentelemetry_instrumentation_sqlalchemy-0.66b1.tar.gz", hash = "sha256:a10043953fcba71911bf29a024f8cc337260c1ef0b4fc844b96cae0de0947baa", size = 18181, upload-time = "2026-10-06T17:36:38.111Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/2b/1c954885815614ef5c1e8c7bbf57a5275e64cd6fb5946b65e17162a34037/opentelemetry_instrumentation_sqlalchemy-0.54b1-py3-none-any.whl", hash = "sha256:d2ca5edb4c7ecef120d51aad6793b7da1cc80207ccfd31c437ee18f098e7c4c4", size = 14169, upload-time = "2025-05-16T19:03:04.119Z" },
+ { url = "https://files.pythonhosted.org/packages/08/05/f8cff0c68a7f9ab8fab01904be5a49c5214435109c2a6b6d173a974c10d9/opentelemetry_instrumentation_sqlalchemy-0.66b1-py3-none-any.whl", hash = "sha256:aa30b10d880d7e91cf94b23a92ac85cec09ffddd8f0d40256d7c510e3dd33971", size = 14425, upload-time = "2026-10-06T17:35:53.436Z" },
]
[[package]]
name = "opentelemetry-instrumentation-threading"
-version = "0.54b1"
+version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "wrapt" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a0/bd/561245292e7cc78ac7a0a75537873aea87440cb9493d41371421b3308c2b/opentelemetry_instrumentation_threading-0.54b1.tar.gz", hash = "sha256:3a081085b59675baf7bd93126a681903e6304a5f283df5eaecdd44bcb66df578", size = 8774, upload-time = "2025-05-16T19:04:04.482Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/55/4f/5fa05266cf24cae93fc32d557d0d2d2f605f81cca1df90614231bbad3cd1/opentelemetry_instrumentation_threading-0.66b1.tar.gz", hash = "sha256:2c217359439d09ca4a4be524db368c05a3cc7f218b85e8086f22664db0a03538", size = 9031, upload-time = "2026-10-06T17:36:41.147Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/81/10/d87ec07d69546adaad525ba5d40d27324a45cba29097d9854a53d9af5047/opentelemetry_instrumentation_threading-0.54b1-py3-none-any.whl", hash = "sha256:bc229e6cd3f2b29fafe0a8dd3141f452e16fcb4906bca4fbf52609f99fb1eb42", size = 9314, upload-time = "2025-05-16T19:03:09.527Z" },
+ { url = "https://files.pythonhosted.org/packages/86/da/1cd52b8be7a521958ccdb9a1c4e5ae743aa2be295ba21ec0b620dc87842a/opentelemetry_instrumentation_threading-0.66b1-py3-none-any.whl", hash = "sha256:94039c60603060d26524e31aed1181a5d153e43f58462d110dbeb4830687d7d7", size = 8493, upload-time = "2026-10-06T17:35:57.958Z" },
]
[[package]]
@@ -6818,7 +6891,7 @@ wheels = [
[[package]]
name = "opentelemetry-instrumentation-urllib3"
-version = "0.54b1"
+version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@@ -6827,9 +6900,9 @@ dependencies = [
{ name = "opentelemetry-util-http" },
{ name = "wrapt" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ed/6f/76a46806cd21002cac1bfd087f5e4674b195ab31ab44c773ca534b6bb546/opentelemetry_instrumentation_urllib3-0.54b1.tar.gz", hash = "sha256:0d30ba3b230e4100cfadaad29174bf7bceac70e812e4f5204e681e4b55a74cd9", size = 15697, upload-time = "2025-05-16T19:04:07.709Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/08/ab/2dd6b3a69df31ae12714bf24fcc2772956d7cb8f9082432c9e63a37e6930/opentelemetry_instrumentation_urllib3-0.66b1.tar.gz", hash = "sha256:31e84a2ccfa86ca1d4f1209ea66c1f709c335348d5c487477d3843172efcc408", size = 18826, upload-time = "2026-10-06T17:36:43.547Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ff/7a/d75bec41edb6deaf1d2859bab66a84c8ba03e822e7eafdb245da205e53f6/opentelemetry_instrumentation_urllib3-0.54b1-py3-none-any.whl", hash = "sha256:e87958c297ddd36d30e1c9069f34a9690e845e4ccc2662dd80e99ed976d4c03e", size = 13123, upload-time = "2025-05-16T19:03:14.053Z" },
+ { url = "https://files.pythonhosted.org/packages/be/7f/16f82bf359c28fb71ad4dc5ad386cce3483abbf1ce0ade087fe78c18daf3/opentelemetry_instrumentation_urllib3-0.66b1-py3-none-any.whl", hash = "sha256:e48b448fed4a127e54c584c37a443c91d80d34b9f9de324af9a00cead8b338b8", size = 13462, upload-time = "2026-10-06T17:36:02.242Z" },
]
[[package]]
@@ -6879,41 +6952,50 @@ wheels = [
[[package]]
name = "opentelemetry-proto"
-version = "1.33.1"
+version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f6/dc/791f3d60a1ad8235930de23eea735ae1084be1c6f96fdadf38710662a7e5/opentelemetry_proto-1.33.1.tar.gz", hash = "sha256:9627b0a5c90753bf3920c398908307063e4458b287bb890e5c1d6fa11ad50b68", size = 34363, upload-time = "2025-05-16T18:52:52.141Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/4b/7f/15f014fb195da6c2dbb6c71399b8e76824878718e94de6454038488eed28/opentelemetry_proto-1.45.1.tar.gz", hash = "sha256:79e0fb95e4616691a469439238aa9224d75779b3e108e895d1aa125ab29ca77c", size = 46488, upload-time = "2026-10-06T17:33:11.49Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c4/29/48609f4c875c2b6c80930073c82dd1cafd36b6782244c01394007b528960/opentelemetry_proto-1.33.1-py3-none-any.whl", hash = "sha256:243d285d9f29663fc7ea91a7171fcc1ccbbfff43b48df0774fd64a37d98eda70", size = 55854, upload-time = "2025-05-16T18:52:36.269Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/9a/42ec8180a769516ae757e893b69736826efceac7332553915b4528a91c6d/opentelemetry_proto-1.45.1-py3-none-any.whl", hash = "sha256:f38e2a8413053c180cd3d2637fbb279673ec2f6a6e09c995aafa2f452c52b46e", size = 72488, upload-time = "2026-10-06T17:32:53.057Z" },
+]
+
+[[package]]
+name = "opentelemetry-proto-json"
+version = "0.66b1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/90/25/a845cc4644123c6b29b2c21822341a55e7f1808703d4f557d24cbd841290/opentelemetry_proto_json-0.66b1.tar.gz", hash = "sha256:92bb70ff0f05815821e922ea628bfdf96f034b981747b0ba8e09db61e1a9cddd", size = 18708, upload-time = "2026-10-06T17:33:12.169Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/7d/bb6acc006c6c4042ef9f734f4901630a440992033f48b66a58fc8fd5e3f4/opentelemetry_proto_json-0.66b1-py3-none-any.whl", hash = "sha256:0d07f1922451cf3a8705bb46fa807f478a1164fbe9287bf7bd50e1ff43f75da3", size = 32061, upload-time = "2026-10-06T17:32:54.097Z" },
]
[[package]]
name = "opentelemetry-sdk"
-version = "1.33.1"
+version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/67/12/909b98a7d9b110cce4b28d49b2e311797cffdce180371f35eba13a72dd00/opentelemetry_sdk-1.33.1.tar.gz", hash = "sha256:85b9fcf7c3d23506fbc9692fd210b8b025a1920535feec50bd54ce203d57a531", size = 161885, upload-time = "2025-05-16T18:52:52.832Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/79/7392e21a1c8f0c61d90b223e31c7e48cb9d452e91a6b820ad24cca5f23c4/opentelemetry_sdk-1.45.1.tar.gz", hash = "sha256:63d24a6ca645019a631e6a51999c73e93adcac1196ca640b8ae78a7cc4762bf3", size = 218324, upload-time = "2026-10-06T17:33:13.26Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/df/8e/ae2d0742041e0bd7fe0d2dcc5e7cce51dcf7d3961a26072d5b43cc8fa2a7/opentelemetry_sdk-1.33.1-py3-none-any.whl", hash = "sha256:19ea73d9a01be29cacaa5d6c8ce0adc0b7f7b4d58cc52f923e4413609f670112", size = 118950, upload-time = "2025-05-16T18:52:37.297Z" },
+ { url = "https://files.pythonhosted.org/packages/95/3c/87c42b4bd6dd297536f04cd9383d212ac557ecd49f2cbdcd46da1c9ef5c8/opentelemetry_sdk-1.45.1-py3-none-any.whl", hash = "sha256:c604c11dc429810812348989115fa44bd558772a3d7442afc43d024f2c250ca4", size = 140063, upload-time = "2026-10-06T17:32:55.04Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
-version = "0.54b1"
+version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "deprecated" },
{ name = "opentelemetry-api" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5b/2c/d7990fc1ffc82889d466e7cd680788ace44a26789809924813b164344393/opentelemetry_semantic_conventions-0.54b1.tar.gz", hash = "sha256:d1cecedae15d19bdaafca1e56b29a66aa286f50b5d08f036a145c7f3e9ef9cee", size = 118642, upload-time = "2025-05-16T18:52:53.962Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/46/e4/dbbfb2a010c4db2224a5114638acede6fe563d33cc20fb1752cebcbe6298/opentelemetry_semantic_conventions-0.66b1.tar.gz", hash = "sha256:497ca63bf383723411e8eaf60c8779e9877633c936bb641080adab59d0eb6ec8", size = 150250, upload-time = "2026-10-06T17:33:14.073Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0a/80/08b1698c52ff76d96ba440bf15edc2f4bc0a279868778928e947c1004bdd/opentelemetry_semantic_conventions-0.54b1-py3-none-any.whl", hash = "sha256:29dab644a7e435b58d3a3918b58c333c92686236b30f7891d5e51f02933ca60d", size = 194938, upload-time = "2025-05-16T18:52:38.796Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/14/67f8aa798857f8cf686f515bf93d9bb877ce952ddc8efae0fa25b45ce0d6/opentelemetry_semantic_conventions-0.66b1-py3-none-any.whl", hash = "sha256:d4cddeb4315490b35213f55e2bdc9ac54bb1e4d318927475bed62b35545e581b", size = 206279, upload-time = "2026-10-06T17:32:56.103Z" },
]
[[package]]
@@ -6927,11 +7009,11 @@ wheels = [
[[package]]
name = "opentelemetry-util-http"
-version = "0.54b1"
+version = "0.66b1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a8/9f/1d8a1d1f34b9f62f2b940b388bf07b8167a8067e70870055bd05db354e5c/opentelemetry_util_http-0.54b1.tar.gz", hash = "sha256:f0b66868c19fbaf9c9d4e11f4a7599fa15d5ea50b884967a26ccd9d72c7c9d15", size = 8044, upload-time = "2025-05-16T19:04:10.79Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7c/b5/df4b61da899f6ebdffdbdf0c8b0f3189ee57151694ccd5b7d50ee2906241/opentelemetry_util_http-0.66b1.tar.gz", hash = "sha256:047dea1a628031f857a5a32261dc0e955bc162d39993ed1cffb8f2cff5ba8a62", size = 11586, upload-time = "2026-10-06T17:36:46.572Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a4/ef/c5aa08abca6894792beed4c0405e85205b35b8e73d653571c9ff13a8e34e/opentelemetry_util_http-0.54b1-py3-none-any.whl", hash = "sha256:b1c91883f980344a1c3c486cffd47ae5c9c1dd7323f9cbe9fdb7cadb401c87c9", size = 7301, upload-time = "2025-05-16T19:03:18.18Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/9b/c77ecaea79ba0de1a11e7f06a7f5eea7043ec23f1860dcf5f03536698e4c/opentelemetry_util_http-0.66b1-py3-none-any.whl", hash = "sha256:8f443d7abcaf29c4a07b373bbd31b5b39132c0ed3c27d015a59dc0323d5b1c58", size = 8387, upload-time = "2026-10-06T17:36:06.984Z" },
]
[[package]]
@@ -8157,14 +8239,14 @@ wheels = [
[[package]]
name = "pypdf"
-version = "6.19.0"
+version = "6.21.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/1f/ac/63d71aaedb59acbcdef491e6ca6469165e3771c9c74358204818fd9bc5a6/pypdf-6.19.0.tar.gz", hash = "sha256:bbc43aca292369ccc6cbc8a921991ecf2538a3587ab5a116eff06c321d647155", size = 7033266, upload-time = "2026-09-16T09:32:05.946Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/51/e1/32a9c4dbd94618145f8bab5c623bdb0a528f23fc8719afd77ba0278fd377/pypdf-6.21.1.tar.gz", hash = "sha256:2d62c383acd6e0dff0a96322ba8b1135229343b084d73c6f6cfd947fd291afcb", size = 7088597, upload-time = "2026-10-17T10:29:52.801Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3c/2c/c43c03eaf630435f023f1dc61ec4a4a78951ad5530a62c71cc89bde307b7/pypdf-6.19.0-py3-none-any.whl", hash = "sha256:7e5d6e730e7dae87d560a2cee218b852f6498c8be61966f3cd02ead971e48d14", size = 395480, upload-time = "2026-09-16T09:32:04.087Z" },
+ { url = "https://files.pythonhosted.org/packages/85/e6/24c944e473c34e818cf1b98085796fcd780465ecfe17663f2bf8fee1958b/pypdf-6.21.1-py3-none-any.whl", hash = "sha256:553e2d42f5bb3bb2a785a7d3de8ef90bb7df74b5413eff5a7b4388007ea9254b", size = 405134, upload-time = "2026-10-17T10:29:50.687Z" },
]
[[package]]
diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json
index 536d7a88f25..81e9425ef4e 100644
--- a/vscode-extension/package-lock.json
+++ b/vscode-extension/package-lock.json
@@ -17,7 +17,7 @@
"@vscode/vsce": "^4.0.0",
"esbuild": "^0.28.2",
"typescript": "^5.9.3",
- "vitest": "^4.1.11"
+ "vitest": "^4.1.12"
},
"engines": {
"vscode": "^1.115.0"
@@ -1323,16 +1323,16 @@
}
},
"node_modules/@vitest/expect": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
- "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.12.tgz",
+ "integrity": "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.11",
- "@vitest/utils": "4.1.11",
+ "@vitest/spy": "4.1.12",
+ "@vitest/utils": "4.1.12",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -1341,13 +1341,13 @@
}
},
"node_modules/@vitest/mocker": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
- "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz",
+ "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.1.11",
+ "@vitest/spy": "4.1.12",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -1368,9 +1368,9 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
- "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.12.tgz",
+ "integrity": "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1381,13 +1381,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
- "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz",
+ "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.11",
+ "@vitest/utils": "4.1.12",
"pathe": "^2.0.3"
},
"funding": {
@@ -1395,14 +1395,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
- "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.12.tgz",
+ "integrity": "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.11",
- "@vitest/utils": "4.1.11",
+ "@vitest/pretty-format": "4.1.12",
+ "@vitest/utils": "4.1.12",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -1411,9 +1411,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
- "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz",
+ "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1421,13 +1421,13 @@
}
},
"node_modules/@vitest/utils": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
- "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.12.tgz",
+ "integrity": "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.11",
+ "@vitest/pretty-format": "4.1.12",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -3390,19 +3390,19 @@
}
},
"node_modules/vitest": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
- "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz",
+ "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/expect": "4.1.11",
- "@vitest/mocker": "4.1.11",
- "@vitest/pretty-format": "4.1.11",
- "@vitest/runner": "4.1.11",
- "@vitest/snapshot": "4.1.11",
- "@vitest/spy": "4.1.11",
- "@vitest/utils": "4.1.11",
+ "@vitest/expect": "4.1.12",
+ "@vitest/mocker": "4.1.12",
+ "@vitest/pretty-format": "4.1.12",
+ "@vitest/runner": "4.1.12",
+ "@vitest/snapshot": "4.1.12",
+ "@vitest/spy": "4.1.12",
+ "@vitest/utils": "4.1.12",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -3430,12 +3430,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.11",
- "@vitest/browser-preview": "4.1.11",
- "@vitest/browser-webdriverio": "4.1.11",
- "@vitest/coverage-istanbul": "4.1.11",
- "@vitest/coverage-v8": "4.1.11",
- "@vitest/ui": "4.1.11",
+ "@vitest/browser-playwright": "4.1.12",
+ "@vitest/browser-preview": "4.1.12",
+ "@vitest/browser-webdriverio": "^4.1.11",
+ "@vitest/coverage-istanbul": "4.1.12",
+ "@vitest/coverage-v8": "4.1.12",
+ "@vitest/ui": "4.1.12",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
diff --git a/vscode-extension/package.json b/vscode-extension/package.json
index 431dad316a1..a3cf2dc7a5f 100644
--- a/vscode-extension/package.json
+++ b/vscode-extension/package.json
@@ -82,6 +82,6 @@
"@vscode/vsce": "^4.0.0",
"esbuild": "^0.28.2",
"typescript": "^5.9.3",
- "vitest": "^4.1.11"
+ "vitest": "^4.1.12"
}
}
From 6de935dfa98a7f5e4ccf9d16f7dbca3569bc063f Mon Sep 17 00:00:00 2001
From: J_Sinclair <56301946+E-R-Butch@users.noreply.github.com>
Date: Fri, 23 Oct 2026 16:48:17 +0800
Subject: [PATCH 5/6] fix(otel): preserve explicit collector certificate
precedence
Keep collector CA settings authoritative across telemetry and billing exports
Preserve mTLS, proxies and isolated tenant credentials
---
litellm/integrations/opentelemetry.py | 15 +--
.../integrations/otel/plumbing/otlp_json.py | 31 +-----
.../otel/plumbing/otlp_transport.py | 95 ++++++++++++++++++
.../integrations/otel/plumbing/providers.py | 15 +--
litellm/integrations/otel/presets/agentops.py | 11 ++-
.../enterprise_billing/billing_metrics.py | 12 ++-
.../otel/test_otel_v2_components.py | 2 +
.../otel/test_otel_v2_destinations.py | 98 +++++++++++++++++++
.../test_billing_metrics.py | 39 +++++---
9 files changed, 260 insertions(+), 58 deletions(-)
create mode 100644 litellm/integrations/otel/plumbing/otlp_transport.py
diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py
index 2226651227f..0f1212721d9 100644
--- a/litellm/integrations/opentelemetry.py
+++ b/litellm/integrations/opentelemetry.py
@@ -3161,13 +3161,14 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
otel_exporter,
)
normalized_endpoint = self._normalize_otel_endpoint(otel_endpoint, "traces")
+ from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
+
tls: Final = resolve_otlp_http_tls("TRACES", _split_otel_headers)
return BatchSpanProcessor(
OTLPSpanExporterHTTP(
endpoint=normalized_endpoint,
headers=_split_otel_headers,
- certificate_file=tls.certificate_file,
- session=tls.session,
+ _transport=build_otlp_http_transport("TRACES", tls),
),
)
elif otel_exporter == "otlp_grpc" or otel_exporter == "grpc":
@@ -3249,12 +3250,13 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
self.OTEL_EXPORTER,
normalized_endpoint,
)
+ from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
+
tls: Final = resolve_otlp_http_tls("LOGS", _split_otel_headers)
return OTLPLogExporter(
endpoint=normalized_endpoint,
headers=_split_otel_headers,
- certificate_file=tls.certificate_file,
- session=tls.session,
+ _transport=build_otlp_http_transport("LOGS", tls),
)
elif self.OTEL_EXPORTER == "otlp_grpc" or self.OTEL_EXPORTER == "grpc":
try:
@@ -3318,12 +3320,13 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
OTLPMetricExporter,
)
+ from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
+
tls: Final = resolve_otlp_http_tls("METRICS", _split_otel_headers)
exporter = OTLPMetricExporter(
endpoint=normalized_endpoint,
headers=_split_otel_headers,
- certificate_file=tls.certificate_file,
- session=tls.session,
+ _transport=build_otlp_http_transport("METRICS", tls),
)
return PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
diff --git a/litellm/integrations/otel/plumbing/otlp_json.py b/litellm/integrations/otel/plumbing/otlp_json.py
index 3c853c3ea8f..f3f4bb897b7 100644
--- a/litellm/integrations/otel/plumbing/otlp_json.py
+++ b/litellm/integrations/otel/plumbing/otlp_json.py
@@ -1,20 +1,13 @@
"""OTLP/JSON export with the same requests and TLS configuration as OTLP/protobuf."""
-import os
from collections.abc import Mapping
from typing import Final
import requests
-from opentelemetry.exporter.http.transport._requests import RequestsHTTPTransport
from opentelemetry.exporter.otlp.json.http.trace_exporter import OTLPSpanExporter
-from opentelemetry.sdk.environment_variables import (
- OTEL_EXPORTER_OTLP_CERTIFICATE,
- OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE,
- OTEL_EXPORTER_OTLP_CLIENT_KEY,
- OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE,
- OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE,
- OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY,
-)
+
+from litellm.integrations.otel.plumbing.otlp_tls import OtlpHttpTls
+from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
class OTLPJsonSpanExporter(OTLPSpanExporter):
@@ -25,22 +18,8 @@ class OTLPJsonSpanExporter(OTLPSpanExporter):
certificate_file: str | None = None,
session: requests.Session | None = None,
) -> None:
- verify: Final = (
- certificate_file
- or os.getenv(OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE)
- or os.getenv(OTEL_EXPORTER_OTLP_CERTIFICATE)
- or True
- )
- client_key: Final = os.getenv(OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY) or os.getenv(OTEL_EXPORTER_OTLP_CLIENT_KEY)
- client_certificate: Final = os.getenv(OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE) or os.getenv(
- OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE
- )
- # The JSON SDK has no session argument. Its transport injection retains
- # requests proxies, session authentication and the existing TLS adapters.
- transport: Final = RequestsHTTPTransport(
- verify=verify,
- cert=(client_certificate, client_key) if client_certificate and client_key else client_certificate,
- session=session,
+ transport: Final = build_otlp_http_transport(
+ "TRACES", OtlpHttpTls(certificate_file=certificate_file, session=session), use_credential_provider=False
)
super().__init__(
endpoint=endpoint, headers={**headers, "content-type": "application/json"}, _transport=transport
diff --git a/litellm/integrations/otel/plumbing/otlp_transport.py b/litellm/integrations/otel/plumbing/otlp_transport.py
new file mode 100644
index 00000000000..31ff3a85c17
--- /dev/null
+++ b/litellm/integrations/otel/plumbing/otlp_transport.py
@@ -0,0 +1,95 @@
+"""Keep explicit collector TLS settings authoritative at request time."""
+
+import os
+from collections.abc import Mapping
+from typing import Final, Literal
+
+import requests
+from opentelemetry.exporter.http.transport._requests import RequestsHTTPResult, RequestsHTTPTransport
+from opentelemetry.exporter.otlp.proto.http._common import (
+ _load_session_from_envvar, # pyright: ignore[reportPrivateUsage] # pinned SDK loader preserves operator auth
+)
+
+from litellm.integrations.otel.plumbing.otlp_tls import OtlpHttpTls
+
+
+class _OtlpHTTPTransport(RequestsHTTPTransport):
+ def __init__(
+ self,
+ verify: bool | str,
+ cert: str | tuple[str, str] | None,
+ session: requests.Session | None,
+ ) -> None:
+ self._request_session: Final = session if session is not None else requests.Session()
+ self._request_verify: Final = verify
+ super().__init__(verify=verify, cert=cert, session=self._request_session)
+
+ def request(
+ self,
+ method: str,
+ url: str,
+ *,
+ headers: Mapping[str, str] | None = None,
+ timeout: float | None = None,
+ data: bytes | None = None,
+ ) -> RequestsHTTPResult:
+ try:
+ response: Final = self._request_session.request(
+ method=method,
+ url=url,
+ headers=headers,
+ data=data,
+ timeout=timeout,
+ verify=self._request_verify,
+ allow_redirects=False,
+ )
+ except Exception as error: # noqa: BLE001 # SDK transport returns all session and auth failures as values
+ return RequestsHTTPResult(error=error)
+ return RequestsHTTPResult(status_code=response.status_code, reason=response.reason, response=response)
+
+
+def build_otlp_http_transport(
+ signal: Literal["TRACES", "LOGS", "METRICS"],
+ tls: OtlpHttpTls,
+ *,
+ use_credential_provider: bool = True,
+ client_key_file: str | None = None,
+ client_certificate_file: str | None = None,
+) -> RequestsHTTPTransport:
+ verify: Final = (
+ tls.certificate_file
+ or os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_CERTIFICATE")
+ or os.getenv("OTEL_EXPORTER_OTLP_CERTIFICATE")
+ or True
+ )
+ client_key: Final = (
+ client_key_file
+ or os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_CLIENT_KEY")
+ or os.getenv("OTEL_EXPORTER_OTLP_CLIENT_KEY")
+ )
+ client_certificate: Final = (
+ client_certificate_file
+ or os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_CLIENT_CERTIFICATE")
+ or os.getenv("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE")
+ )
+ credential_variable: Final[
+ Literal[
+ "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER",
+ "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER",
+ "OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER",
+ ]
+ ] = (
+ "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER"
+ if signal == "TRACES"
+ else "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER"
+ if signal == "LOGS"
+ else "OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER"
+ )
+ session: Final = tls.session or (
+ _load_session_from_envvar(credential_variable) if use_credential_provider else None
+ )
+ return _OtlpHTTPTransport(
+ verify=verify,
+ cert=(client_certificate, client_key) if client_certificate and client_key else client_certificate,
+ session=session,
+ )
diff --git a/litellm/integrations/otel/plumbing/providers.py b/litellm/integrations/otel/plumbing/providers.py
index 4aae13d822c..f5c1637ee63 100644
--- a/litellm/integrations/otel/plumbing/providers.py
+++ b/litellm/integrations/otel/plumbing/providers.py
@@ -218,12 +218,13 @@ def _exporter_from_spec(spec: ExporterSpec, *, isolate_headers: bool = False) ->
OTLPSpanExporter as HTTPExporter,
)
+ from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
+
http_tls: Final = resolve_otlp_http_tls("TRACES", parse_headers(spec.headers), isolate_headers=isolate_headers)
return HTTPExporter(
endpoint=spec.traces_endpoint or _otlp_traces_endpoint(spec.endpoint),
headers=parse_headers(spec.headers),
- certificate_file=http_tls.certificate_file,
- session=http_tls.session,
+ _transport=build_otlp_http_transport("TRACES", http_tls),
)
if kind in _OTLP_GRPC_KINDS:
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
@@ -1188,12 +1189,13 @@ def build_metric_reader(config: OpenTelemetryV2Config) -> "MetricReader":
OTLPMetricExporter as HTTPMetricExporter,
)
+ from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
+
tls: Final = resolve_otlp_http_tls("METRICS", parse_headers(config.headers))
exporter: object = HTTPMetricExporter(
endpoint=_otlp_metrics_endpoint(config.endpoint),
headers=parse_headers(config.headers),
- certificate_file=tls.certificate_file,
- session=tls.session,
+ _transport=build_otlp_http_transport("METRICS", tls),
)
elif kind in ("otlp_grpc", "grpc"):
try:
@@ -1251,12 +1253,13 @@ def build_log_exporter(config: OpenTelemetryV2Config) -> LogRecordExporter:
OTLPLogExporter as HTTPLogExporter,
)
+ from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
+
tls: Final = resolve_otlp_http_tls("LOGS", parse_headers(config.headers))
return HTTPLogExporter(
endpoint=_otlp_logs_endpoint(config.endpoint),
headers=parse_headers(config.headers),
- certificate_file=tls.certificate_file,
- session=tls.session,
+ _transport=build_otlp_http_transport("LOGS", tls),
)
if kind in ("otlp_grpc", "grpc"):
try:
diff --git a/litellm/integrations/otel/presets/agentops.py b/litellm/integrations/otel/presets/agentops.py
index 30758f7ed4b..3408adbfb5f 100644
--- a/litellm/integrations/otel/presets/agentops.py
+++ b/litellm/integrations/otel/presets/agentops.py
@@ -24,7 +24,7 @@ from litellm.integrations.otel.model.config import (
ExporterSpec,
OpenTelemetryV2Config,
)
-from litellm.integrations.otel.plumbing.otlp_tls import resolve_otlp_http_tls
+from litellm.integrations.otel.plumbing.otlp_tls import OtlpHttpTls, resolve_otlp_http_tls
from litellm.integrations.otel.plumbing.providers import register_exporter_factory
_AGENTOPS_ENDPOINT: Final = "https://otlp.agentops.ai/v1/traces"
@@ -101,11 +101,18 @@ def _build_agentops_exporter(spec: ExporterSpec) -> SpanExporter:
"""Authenticate lazily through requests' public session authentication API."""
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
+ from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
+
options: Final = spec.options or {}
tls: Final = resolve_otlp_http_tls("TRACES", isolate_headers=True)
session: Final = tls.session or requests.Session()
session.auth = _AgentOpsAuth(options.get("api_key"))
- return OTLPSpanExporter(endpoint=spec.endpoint, certificate_file=tls.certificate_file, session=session)
+ return OTLPSpanExporter(
+ endpoint=spec.endpoint,
+ _transport=build_otlp_http_transport(
+ "TRACES", OtlpHttpTls(certificate_file=tls.certificate_file, session=session)
+ ),
+ )
def _fetch_agentops_jwt(api_key: str) -> dict[str, object]:
diff --git a/litellm/proxy/enterprise_billing/billing_metrics.py b/litellm/proxy/enterprise_billing/billing_metrics.py
index 166ece28b83..90616e8acb1 100644
--- a/litellm/proxy/enterprise_billing/billing_metrics.py
+++ b/litellm/proxy/enterprise_billing/billing_metrics.py
@@ -25,6 +25,8 @@ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from litellm._logging import verbose_proxy_logger
+from litellm.integrations.otel.plumbing.otlp_tls import OtlpHttpTls
+from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
from litellm.proxy.middleware.billable_request_metrics_middleware import (
BillableCategory,
)
@@ -104,10 +106,12 @@ def build_mtls_meter_provider(config: BillingMetricsConfig) -> MeterProvider:
"""
exporter: Final = OTLPMetricExporter(
endpoint=_metrics_endpoint(config.endpoint),
- # None -> exporter falls back to the system trust store.
- certificate_file=config.ca_cert_path,
- client_certificate_file=config.client_cert_path,
- client_key_file=config.client_key_path,
+ _transport=build_otlp_http_transport(
+ "METRICS",
+ OtlpHttpTls(certificate_file=config.ca_cert_path, session=None),
+ client_certificate_file=config.client_cert_path,
+ client_key_file=config.client_key_path,
+ ),
)
reader: Final = PeriodicExportingMetricReader(exporter, export_interval_millis=config.export_interval_ms)
return MeterProvider(metric_readers=[reader], resource=Resource.create(_resource_attributes(config)))
diff --git a/tests/unit/integrations/otel/test_otel_v2_components.py b/tests/unit/integrations/otel/test_otel_v2_components.py
index 15ea206cf93..37392e3c77f 100644
--- a/tests/unit/integrations/otel/test_otel_v2_components.py
+++ b/tests/unit/integrations/otel/test_otel_v2_components.py
@@ -805,6 +805,8 @@ def test_http_json_exporter_preserves_session_tls_auth_and_compression(monkeypat
from opentelemetry.sdk.trace.export import SpanExportResult
from litellm.integrations.otel.plumbing.otlp_json import OTLPJsonSpanExporter
+ monkeypatch.setenv("REQUESTS_CA_BUNDLE", "/corporate/global-ca.pem")
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE", "/collector/environment-ca.pem")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", compression)
monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE", "/collector/client.crt")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY", "/collector/client.key")
diff --git a/tests/unit/integrations/otel/test_otel_v2_destinations.py b/tests/unit/integrations/otel/test_otel_v2_destinations.py
index e556cce6d00..a8d8da9b426 100644
--- a/tests/unit/integrations/otel/test_otel_v2_destinations.py
+++ b/tests/unit/integrations/otel/test_otel_v2_destinations.py
@@ -5235,3 +5235,101 @@ def test_session_defaults_cannot_restore_excluded_operator_headers(monkeypatch,
assert "x-operator-key" not in captured.last_request.headers
finally:
exporter.shutdown()
+
+
+@pytest.mark.parametrize(
+ ("api", "signal"),
+ (("v1", "traces"), ("v1", "logs"), ("v1", "metrics"),
+ ("v2", "traces"), ("v2", "logs"), ("v2", "metrics"), ("v2-json", "traces")),
+)
+@pytest.mark.parametrize("requests_ca", ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE"))
+@pytest.mark.parametrize("collector_ca", ("signal", "global", "SSL_CERT_FILE", "ssl_verify"))
+@pytest.mark.parametrize("tenant", (False, True))
+def test_explicit_collector_ca_precedes_requests_environment(
+ monkeypatch, requests_mock, tmp_path, api: str, signal: str, requests_ca: str, collector_ca: str, tenant: bool
+) -> None:
+ certificate_path: Final = tmp_path / "collector-ca.pem"
+ certificate_path.write_text("TEST_ONLY_CA")
+ certificate: Final = str(certificate_path)
+ for variable in ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "SSL_CERT_FILE", "SSL_VERIFY",
+ "OTEL_EXPORTER_OTLP_CERTIFICATE", f"OTEL_EXPORTER_OTLP_{signal.upper()}_CERTIFICATE"):
+ monkeypatch.delenv(variable, raising=False)
+ monkeypatch.setattr(litellm, "ssl_verify", True)
+ monkeypatch.setenv(requests_ca, "/corporate/global-ca.pem")
+ if collector_ca == "ssl_verify":
+ monkeypatch.setattr(litellm, "ssl_verify", certificate)
+ else:
+ variable: Final = (
+ f"OTEL_EXPORTER_OTLP_{signal.upper()}_CERTIFICATE" if collector_ca == "signal"
+ else "OTEL_EXPORTER_OTLP_CERTIFICATE" if collector_ca == "global" else collector_ca
+ )
+ monkeypatch.setenv(variable, certificate)
+ if collector_ca == "signal":
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_CERTIFICATE", "/collector/generic-ca.pem")
+ monkeypatch.setenv(f"OTEL_EXPORTER_OTLP_{signal.upper()}_CLIENT_CERTIFICATE", "/collector/client.pem")
+ monkeypatch.setenv(f"OTEL_EXPORTER_OTLP_{signal.upper()}_CLIENT_KEY", "/collector/client.key")
+ monkeypatch.setenv("HTTPS_PROXY", "http://proxy.example.invalid:8080")
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer%20TEST_ONLY_OPERATOR")
+ captured: Final = requests_mock.post(f"https://collector.example.invalid/v1/{signal}", content=b"")
+
+ _export_header_probe(api, signal, "https://collector.example.invalid", "x-tenant-key=TEST_ONLY" if tenant else None)
+
+ assert captured.request_history
+ for request in captured.request_history:
+ assert request.verify == certificate
+ assert request.cert == ("/collector/client.pem", "/collector/client.key")
+ assert request.proxies["https"] == "http://proxy.example.invalid:8080"
+ assert request.headers.get("Authorization") == (None if tenant else "Bearer TEST_ONLY_OPERATOR")
+ assert request.headers.get("x-tenant-key") == ("TEST_ONLY" if tenant else None)
+
+
+@pytest.mark.parametrize("api", ("v1", "v2"))
+@pytest.mark.parametrize("signal", ("traces", "logs", "metrics"))
+@pytest.mark.parametrize("global_provider", (False, True))
+@pytest.mark.parametrize("tenant", (False, True))
+def test_collector_tls_preserves_operator_credential_provider_without_tenant_leakage(
+ monkeypatch, requests_mock, api: str, signal: str, global_provider: bool, tenant: bool
+) -> None:
+ from types import SimpleNamespace
+
+ import requests
+ from opentelemetry.exporter.otlp.proto.http import _common
+
+ session: Final = requests.Session()
+ session.auth = requests.auth.HTTPBasicAuth("TEST_ONLY_USER", "TEST_ONLY_PASSWORD")
+ session.headers["x-session-marker"] = "TEST_ONLY_SESSION"
+ provider_variable: Final = (
+ "OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER" if global_provider
+ else f"OTEL_PYTHON_EXPORTER_OTLP_HTTP_{signal.upper()}_CREDENTIAL_PROVIDER"
+ )
+ monkeypatch.setenv(provider_variable, "TEST_ONLY_PROVIDER")
+ monkeypatch.setenv(f"OTEL_EXPORTER_OTLP_{signal.upper()}_CERTIFICATE", "/collector/collector-ca.pem")
+ monkeypatch.setenv("REQUESTS_CA_BUNDLE", "/corporate/global-ca.pem")
+ monkeypatch.setattr(_common, "entry_points", lambda **kwargs: (SimpleNamespace(load=lambda: lambda: session),))
+ captured: Final = requests_mock.post(f"https://collector.example.invalid/v1/{signal}", content=b"")
+
+ _export_header_probe(api, signal, "https://collector.example.invalid", "x-tenant-key=TEST_ONLY" if tenant else None)
+
+ assert captured.request_history
+ for request in captured.request_history:
+ assert request.verify == "/collector/collector-ca.pem"
+ assert request.headers.get("x-session-marker") == (None if tenant else "TEST_ONLY_SESSION")
+ assert request.headers.get("Authorization") == (
+ None if tenant else "Basic " + b64encode(b"TEST_ONLY_USER:TEST_ONLY_PASSWORD").decode()
+ )
+
+
+@pytest.mark.parametrize("api", ("v1", "v2", "v2-json"))
+@pytest.mark.parametrize("requests_ca", ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE"))
+def test_requests_environment_ca_remains_fallback(monkeypatch, requests_mock, api: str, requests_ca: str) -> None:
+ for variable in ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "SSL_CERT_FILE", "SSL_VERIFY",
+ "OTEL_EXPORTER_OTLP_CERTIFICATE", "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE"):
+ monkeypatch.delenv(variable, raising=False)
+ monkeypatch.setattr(litellm, "ssl_verify", True)
+ monkeypatch.setenv(requests_ca, "/corporate/global-ca.pem")
+ captured: Final = requests_mock.post("https://collector.example.invalid/v1/traces", content=b"")
+
+ _export_header_probe(api, "traces", "https://collector.example.invalid", None)
+
+ assert captured.request_history
+ assert captured.last_request.verify == "/corporate/global-ca.pem"
diff --git a/tests/unit/proxy/enterprise_billing/test_billing_metrics.py b/tests/unit/proxy/enterprise_billing/test_billing_metrics.py
index 0446cfeeab0..06f4e09697c 100644
--- a/tests/unit/proxy/enterprise_billing/test_billing_metrics.py
+++ b/tests/unit/proxy/enterprise_billing/test_billing_metrics.py
@@ -12,7 +12,7 @@ import os
import socket
import stat
from pathlib import Path
-from typing import Dict, List, Optional
+from typing import Dict, Final, List, Optional
import pytest
from opentelemetry.sdk.metrics import MeterProvider
@@ -350,19 +350,30 @@ def _fake_exporter_class(captured: Dict[str, object]) -> type:
return _FakeExporter
-def test_meter_provider_wires_client_cert_into_http_exporter(tmp_path, monkeypatch):
- """Client cert+key authenticate us at the collector's mTLS front end; CA override rides certificate_file."""
- captured: Dict[str, object] = {}
-
- monkeypatch.setattr(bm, "OTLPMetricExporter", _fake_exporter_class(captured))
- config = _config(tmp_path)
- provider = bm.build_mtls_meter_provider(config)
- provider.shutdown()
-
- assert captured["endpoint"] == "https://collector.example:4317/v1/metrics"
- assert captured["client_certificate_file"] == config.client_cert_path
- assert captured["client_key_file"] == config.client_key_path
- assert captured["certificate_file"] == config.ca_cert_path
+@pytest.mark.parametrize("requests_ca", ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE"))
+def test_meter_provider_preserves_explicit_tls_settings(tmp_path, monkeypatch, requests_mock, requests_ca: str) -> None:
+ monkeypatch.delenv("REQUESTS_CA_BUNDLE", raising=False)
+ monkeypatch.delenv("CURL_CA_BUNDLE", raising=False)
+ monkeypatch.setenv(requests_ca, "/corporate/global-ca.pem")
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE", "/operator/collector-ca.pem")
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE", "/operator/client.pem")
+ monkeypatch.setenv("OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY", "/operator/client.key")
+ monkeypatch.setenv("HTTPS_PROXY", "http://proxy.example.invalid:8080")
+ captured: Final = requests_mock.post("https://collector.example:4317/v1/metrics", content=b"")
+ config: Final = _config(tmp_path)
+ provider: Final = bm.build_mtls_meter_provider(config)
+ try:
+ bm.BillingMetricsRecorder(provider).record(
+ category=BillableCategory.LLM, route="/chat/completions", status_code=200, model_id=None
+ )
+ assert provider.force_flush()
+ assert captured.request_history
+ for request in captured.request_history:
+ assert request.verify == config.ca_cert_path
+ assert request.cert == (config.client_cert_path, config.client_key_path)
+ assert request.proxies["https"] == "http://proxy.example.invalid:8080"
+ finally:
+ provider.shutdown()
def test_load_config_without_ca_is_valid(monkeypatch, tmp_path):
From 490e1baad15b2043abc2cc376a2db0f6fdd5e7a1 Mon Sep 17 00:00:00 2001
From: J_Sinclair <56301946+E-R-Butch@users.noreply.github.com>
Date: Fri, 23 Oct 2026 18:00:35 +0800
Subject: [PATCH 6/6] revert: separate dependency fixes from Chinese
localization
Revert merge 3a3bc917fbe90bf376ad57e100c48a3d6d432b0a relative to its first parent.
Restore the exact premerge Chinese localization tree from 9baf20e312f2584ec98dedc3a342e6149b57edda. Preserve the reviewed security and dependency fixes on their separate branch for an independent PR.
---
litellm/integrations/langfuse/langfuse_sdk.py | 3 -
litellm/integrations/opentelemetry.py | 21 +-
litellm/integrations/otel/mount.py | 3 -
.../integrations/otel/plumbing/otlp_json.py | 79 +++++-
.../integrations/otel/plumbing/otlp_tls.py | 70 +----
.../otel/plumbing/otlp_transport.py | 95 -------
.../integrations/otel/plumbing/providers.py | 47 ++--
litellm/integrations/otel/presets/agentops.py | 78 ++---
.../enterprise_billing/billing_metrics.py | 12 +-
pyproject.toml | 31 +-
.../langfuse/test_langfuse_sdk.py | 24 --
.../otel/test_otel_v2_components.py | 76 +----
.../otel/test_otel_v2_destinations.py | 266 ------------------
.../integrations/otel/test_otel_v2_mount.py | 15 +-
.../integrations/otel/test_otel_v2_presets.py | 89 +++---
tests/unit/integrations/test_langfuse_otel.py | 26 +-
tests/unit/integrations/test_opentelemetry.py | 52 ++--
.../test_litellm_logging.py | 18 +-
.../test_billing_metrics.py | 39 +--
ui/litellm-dashboard/package-lock.json | 210 +++++++-------
ui/litellm-dashboard/package.json | 13 +-
uv.lock | 246 ++++++----------
vscode-extension/package-lock.json | 90 +++---
vscode-extension/package.json | 2 +-
24 files changed, 511 insertions(+), 1094 deletions(-)
delete mode 100644 litellm/integrations/otel/plumbing/otlp_transport.py
diff --git a/litellm/integrations/langfuse/langfuse_sdk.py b/litellm/integrations/langfuse/langfuse_sdk.py
index 1bbc18d5dd4..51b08cb72df 100644
--- a/litellm/integrations/langfuse/langfuse_sdk.py
+++ b/litellm/integrations/langfuse/langfuse_sdk.py
@@ -317,9 +317,6 @@ class _RequestedIdGenerator(RandomIdGenerator):
the tracer's id generator, so the request rides on a context variable instead.
"""
- def is_trace_id_random(self) -> bool:
- return _requested_trace_id.get() is None
-
def generate_trace_id(self) -> int:
requested: Final = _requested_trace_id.get()
return super().generate_trace_id() if requested is None else requested
diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py
index 0f1212721d9..0d0e309034a 100644
--- a/litellm/integrations/opentelemetry.py
+++ b/litellm/integrations/opentelemetry.py
@@ -3161,14 +3161,13 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
otel_exporter,
)
normalized_endpoint = self._normalize_otel_endpoint(otel_endpoint, "traces")
- from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
-
- tls: Final = resolve_otlp_http_tls("TRACES", _split_otel_headers)
+ tls: Final = resolve_otlp_http_tls("TRACES")
return BatchSpanProcessor(
OTLPSpanExporterHTTP(
endpoint=normalized_endpoint,
headers=_split_otel_headers,
- _transport=build_otlp_http_transport("TRACES", tls),
+ certificate_file=tls.certificate_file,
+ session=tls.session,
),
)
elif otel_exporter == "otlp_grpc" or otel_exporter == "grpc":
@@ -3250,13 +3249,12 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
self.OTEL_EXPORTER,
normalized_endpoint,
)
- from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
-
- tls: Final = resolve_otlp_http_tls("LOGS", _split_otel_headers)
+ tls: Final = resolve_otlp_http_tls("LOGS")
return OTLPLogExporter(
endpoint=normalized_endpoint,
headers=_split_otel_headers,
- _transport=build_otlp_http_transport("LOGS", tls),
+ certificate_file=tls.certificate_file,
+ session=tls.session,
)
elif self.OTEL_EXPORTER == "otlp_grpc" or self.OTEL_EXPORTER == "grpc":
try:
@@ -3320,13 +3318,12 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
OTLPMetricExporter,
)
- from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
-
- tls: Final = resolve_otlp_http_tls("METRICS", _split_otel_headers)
+ tls: Final = resolve_otlp_http_tls("METRICS")
exporter = OTLPMetricExporter(
endpoint=normalized_endpoint,
headers=_split_otel_headers,
- _transport=build_otlp_http_transport("METRICS", tls),
+ certificate_file=tls.certificate_file,
+ session=tls.session,
)
return PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
diff --git a/litellm/integrations/otel/mount.py b/litellm/integrations/otel/mount.py
index c033bc1935b..cb52ad24c8e 100644
--- a/litellm/integrations/otel/mount.py
+++ b/litellm/integrations/otel/mount.py
@@ -126,9 +126,6 @@ def instrument_fastapi_app(app: "FastAPI", tracer_provider: "TracerProvider | No
try:
if not is_otel_v2_enabled():
return
- if app.middleware_stack is not None:
- verbose_logger.debug("Skipping OTel V2 FastAPI instrumentation after the middleware stack was built")
- return
# Lazy: only the V2-enabled path needs the optional
# ``opentelemetry-instrumentation-fastapi`` package, which is not part of the
diff --git a/litellm/integrations/otel/plumbing/otlp_json.py b/litellm/integrations/otel/plumbing/otlp_json.py
index f3f4bb897b7..bc6d7d435b0 100644
--- a/litellm/integrations/otel/plumbing/otlp_json.py
+++ b/litellm/integrations/otel/plumbing/otlp_json.py
@@ -1,26 +1,77 @@
-"""OTLP/JSON export with the same requests and TLS configuration as OTLP/protobuf."""
+"""OTLP/HTTP span exporter that sends the OTLP/JSON encoding instead of protobuf.
-from collections.abc import Mapping
-from typing import Final
+The SDK only ships a protobuf OTLP/HTTP exporter; this reuses its transport and
+retry loop and swaps the payload for OTLP/JSON (enums as integers, ids as hex).
+"""
+
+import base64
+import json
+from collections.abc import Mapping, Sequence
+from types import MappingProxyType
+from typing import Final, TypeAlias
import requests
-from opentelemetry.exporter.otlp.json.http.trace_exporter import OTLPSpanExporter
+from google.protobuf.json_format import MessageToDict
+from opentelemetry.exporter.otlp.proto.common.trace_encoder import encode_spans
+from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
+from opentelemetry.sdk.trace import ReadableSpan
-from litellm.integrations.otel.plumbing.otlp_tls import OtlpHttpTls
-from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
+JSON_CONTENT_TYPE: Final = "application/json"
+_HEX_ID_KEYS: Final = frozenset({"traceId", "spanId", "parentSpanId"})
+
+_JsonValue: TypeAlias = "Mapping[str, _JsonValue] | Sequence[_JsonValue] | str | int | float | bool | None"
+_JsonObject: TypeAlias = Mapping[str, "_JsonValue"]
+
+
+def _objects(node: _JsonObject, key: str) -> tuple[_JsonObject, ...]:
+ items: Final = node.get(key)
+ if isinstance(items, str) or not isinstance(items, Sequence):
+ return ()
+ return tuple(item for item in items if isinstance(item, Mapping))
+
+
+def _hex_ids(node: _JsonObject) -> _JsonObject:
+ return MappingProxyType(
+ {
+ key: base64.b64decode(item).hex() if key in _HEX_ID_KEYS and isinstance(item, str) else item
+ for key, item in node.items()
+ }
+ )
+
+
+def _hex_span(span: _JsonObject) -> _JsonObject:
+ links: Final = _objects(span, "links")
+ if not links:
+ return _hex_ids(span)
+ return MappingProxyType({**_hex_ids(span), "links": tuple(_hex_ids(link) for link in links)})
+
+
+def _hex_scope_spans(scope: _JsonObject) -> _JsonObject:
+ return MappingProxyType({**scope, "spans": tuple(_hex_span(span) for span in _objects(scope, "spans"))})
+
+
+def _hex_resource_spans(resource: _JsonObject) -> _JsonObject:
+ scope_spans: Final = tuple(_hex_scope_spans(scope) for scope in _objects(resource, "scopeSpans"))
+ return MappingProxyType({**resource, "scopeSpans": scope_spans})
+
+
+def encode_spans_json(spans: Sequence[ReadableSpan]) -> bytes:
+ payload: Final[_JsonObject] = MessageToDict(encode_spans(spans), use_integers_for_enums=True)
+ resource_spans: Final = tuple(_hex_resource_spans(resource) for resource in _objects(payload, "resourceSpans"))
+ hexed: Final[_JsonObject] = MappingProxyType({**payload, "resourceSpans": resource_spans})
+ return json.dumps(hexed, default=dict, separators=(",", ":")).encode()
class OTLPJsonSpanExporter(OTLPSpanExporter):
def __init__(
self,
endpoint: str | None,
- headers: Mapping[str, str],
+ headers: dict[str, str], # mutable-ok: SDK __init__ takes Dict
certificate_file: str | None = None,
- session: requests.Session | None = None,
+ session: "requests.Session | None" = None,
) -> None:
- transport: Final = build_otlp_http_transport(
- "TRACES", OtlpHttpTls(certificate_file=certificate_file, session=session), use_credential_provider=False
- )
- super().__init__(
- endpoint=endpoint, headers={**headers, "content-type": "application/json"}, _transport=transport
- )
+ super().__init__(endpoint=endpoint, headers=headers, certificate_file=certificate_file, session=session)
+ self._session.headers["Content-Type"] = JSON_CONTENT_TYPE
+
+ def _serialize_spans(self, spans: Sequence[ReadableSpan]) -> bytes:
+ return encode_spans_json(spans)
diff --git a/litellm/integrations/otel/plumbing/otlp_tls.py b/litellm/integrations/otel/plumbing/otlp_tls.py
index 959e45e9bd7..9de97e0c77a 100644
--- a/litellm/integrations/otel/plumbing/otlp_tls.py
+++ b/litellm/integrations/otel/plumbing/otlp_tls.py
@@ -1,19 +1,11 @@
import os
-from collections.abc import Mapping
-from copy import copy
from dataclasses import dataclass
from typing import Final, Literal
import requests
-from pydantic import BaseModel, ConfigDict
from requests.adapters import HTTPAdapter
-class _RequestHeaders(BaseModel):
- model_config = ConfigDict(frozen=True, from_attributes=True)
- headers: Mapping[str, str] | None = None
-
-
@dataclass(frozen=True, slots=True)
class OtlpHttpTls:
certificate_file: str | None
@@ -33,65 +25,17 @@ class _NoVerifyAdapter(HTTPAdapter):
)
-class _IsolatedHeadersSession(requests.Session):
- def __init__(self, signal: str, headers: Mapping[str, str], content_type: str) -> None:
- from opentelemetry.util.re import parse_env_headers
-
- super().__init__()
- environment: Final = parse_env_headers(
- os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_HEADERS") or os.getenv("OTEL_EXPORTER_OTLP_HEADERS", ""),
- liberal=True,
- )
- explicit: Final = frozenset(key.lower() for key in headers)
- self._excluded_headers: Final = frozenset(key.lower() for key in environment) - explicit
- self._content_type: Final = content_type
- self._compression: Final = (
- (
- os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_COMPRESSION")
- or os.getenv("OTEL_EXPORTER_OTLP_COMPRESSION", "none")
- )
- .strip()
- .lower()
- )
-
- def prepare_request(self, request: requests.Request) -> requests.PreparedRequest:
- isolated: Final = copy(request)
- source: Final[Mapping[str, str]] = _RequestHeaders.model_validate(request).headers or {}
- kept: Final = {key: value for key, value in source.items() if key.lower() not in self._excluded_headers}
- content_type: Final = {"Content-Type": self._content_type} if "content-type" in self._excluded_headers else {}
- compression: Final = (
- {"Content-Encoding": self._compression}
- if "content-encoding" in self._excluded_headers and self._compression in ("gzip", "deflate")
- else {}
- )
- isolated.headers = {**kept, **content_type, **compression}
- session: Final = copy(self)
- session.headers = {
- key: value for key, value in self.headers.items() if key.lower() not in self._excluded_headers
- }
- return requests.Session.prepare_request(session, isolated)
-
-
-def resolve_otlp_http_tls(
- signal: Literal["TRACES", "METRICS", "LOGS"],
- headers: Mapping[str, str] | None = None,
- *,
- content_type: str = "application/x-protobuf",
- isolate_headers: bool = False,
-) -> OtlpHttpTls:
- session: Final = (
- _IsolatedHeadersSession(signal, headers or {}, content_type) if headers or isolate_headers else None
- )
+def resolve_otlp_http_tls(signal: Literal["TRACES", "METRICS", "LOGS"]) -> OtlpHttpTls:
if os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_CERTIFICATE") or os.getenv("OTEL_EXPORTER_OTLP_CERTIFICATE"):
- return OtlpHttpTls(certificate_file=None, session=session)
+ return OtlpHttpTls(certificate_file=None, session=None)
from litellm.llms.custom_httpx.http_handler import get_ssl_verify
verify: Final = get_ssl_verify()
if verify is False:
- unverified: Final = session or requests.Session()
- unverified.mount("https://", _NoVerifyAdapter())
- return OtlpHttpTls(certificate_file=None, session=unverified)
+ session: Final = requests.Session()
+ session.mount("https://", _NoVerifyAdapter())
+ return OtlpHttpTls(certificate_file=None, session=session)
if isinstance(verify, str):
- return OtlpHttpTls(certificate_file=verify, session=session)
- return OtlpHttpTls(certificate_file=None, session=session)
+ return OtlpHttpTls(certificate_file=verify, session=None)
+ return OtlpHttpTls(certificate_file=None, session=None)
diff --git a/litellm/integrations/otel/plumbing/otlp_transport.py b/litellm/integrations/otel/plumbing/otlp_transport.py
deleted file mode 100644
index 31ff3a85c17..00000000000
--- a/litellm/integrations/otel/plumbing/otlp_transport.py
+++ /dev/null
@@ -1,95 +0,0 @@
-"""Keep explicit collector TLS settings authoritative at request time."""
-
-import os
-from collections.abc import Mapping
-from typing import Final, Literal
-
-import requests
-from opentelemetry.exporter.http.transport._requests import RequestsHTTPResult, RequestsHTTPTransport
-from opentelemetry.exporter.otlp.proto.http._common import (
- _load_session_from_envvar, # pyright: ignore[reportPrivateUsage] # pinned SDK loader preserves operator auth
-)
-
-from litellm.integrations.otel.plumbing.otlp_tls import OtlpHttpTls
-
-
-class _OtlpHTTPTransport(RequestsHTTPTransport):
- def __init__(
- self,
- verify: bool | str,
- cert: str | tuple[str, str] | None,
- session: requests.Session | None,
- ) -> None:
- self._request_session: Final = session if session is not None else requests.Session()
- self._request_verify: Final = verify
- super().__init__(verify=verify, cert=cert, session=self._request_session)
-
- def request(
- self,
- method: str,
- url: str,
- *,
- headers: Mapping[str, str] | None = None,
- timeout: float | None = None,
- data: bytes | None = None,
- ) -> RequestsHTTPResult:
- try:
- response: Final = self._request_session.request(
- method=method,
- url=url,
- headers=headers,
- data=data,
- timeout=timeout,
- verify=self._request_verify,
- allow_redirects=False,
- )
- except Exception as error: # noqa: BLE001 # SDK transport returns all session and auth failures as values
- return RequestsHTTPResult(error=error)
- return RequestsHTTPResult(status_code=response.status_code, reason=response.reason, response=response)
-
-
-def build_otlp_http_transport(
- signal: Literal["TRACES", "LOGS", "METRICS"],
- tls: OtlpHttpTls,
- *,
- use_credential_provider: bool = True,
- client_key_file: str | None = None,
- client_certificate_file: str | None = None,
-) -> RequestsHTTPTransport:
- verify: Final = (
- tls.certificate_file
- or os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_CERTIFICATE")
- or os.getenv("OTEL_EXPORTER_OTLP_CERTIFICATE")
- or True
- )
- client_key: Final = (
- client_key_file
- or os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_CLIENT_KEY")
- or os.getenv("OTEL_EXPORTER_OTLP_CLIENT_KEY")
- )
- client_certificate: Final = (
- client_certificate_file
- or os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_CLIENT_CERTIFICATE")
- or os.getenv("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE")
- )
- credential_variable: Final[
- Literal[
- "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER",
- "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER",
- "OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER",
- ]
- ] = (
- "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER"
- if signal == "TRACES"
- else "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER"
- if signal == "LOGS"
- else "OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER"
- )
- session: Final = tls.session or (
- _load_session_from_envvar(credential_variable) if use_credential_provider else None
- )
- return _OtlpHTTPTransport(
- verify=verify,
- cert=(client_certificate, client_key) if client_certificate and client_key else client_certificate,
- session=session,
- )
diff --git a/litellm/integrations/otel/plumbing/providers.py b/litellm/integrations/otel/plumbing/providers.py
index f5c1637ee63..898eab5715b 100644
--- a/litellm/integrations/otel/plumbing/providers.py
+++ b/litellm/integrations/otel/plumbing/providers.py
@@ -16,9 +16,9 @@ from opentelemetry.metrics import MeterProvider, NoOpMeterProvider
from opentelemetry.sdk._logs import LoggerProvider as SDKLoggerProvider
from opentelemetry.sdk._logs.export import (
BatchLogRecordProcessor,
- ConsoleLogRecordExporter,
- InMemoryLogRecordExporter,
- LogRecordExporter,
+ ConsoleLogExporter,
+ InMemoryLogExporter,
+ LogExporter,
SimpleLogRecordProcessor,
)
from opentelemetry.sdk.metrics import MeterProvider as SDKMeterProvider
@@ -194,7 +194,7 @@ def exporter_transport(kind: str) -> Literal["http", "grpc", "headerless"]:
return "headerless"
-def _exporter_from_spec(spec: ExporterSpec, *, isolate_headers: bool = False) -> SpanExporter:
+def _exporter_from_spec(spec: ExporterSpec) -> SpanExporter:
kind: Final = (spec.kind or "console").lower()
factory: Final = _EXPORTER_FACTORIES.get(kind)
if factory is not None:
@@ -204,9 +204,7 @@ def _exporter_from_spec(spec: ExporterSpec, *, isolate_headers: bool = False) ->
if kind in _OTLP_HTTP_JSON_KINDS:
from litellm.integrations.otel.plumbing.otlp_json import OTLPJsonSpanExporter
- tls: Final = resolve_otlp_http_tls(
- "TRACES", parse_headers(spec.headers), content_type="application/json", isolate_headers=isolate_headers
- )
+ tls: Final = resolve_otlp_http_tls("TRACES")
return OTLPJsonSpanExporter(
endpoint=spec.traces_endpoint or _otlp_traces_endpoint(spec.endpoint),
headers=parse_headers(spec.headers),
@@ -218,13 +216,12 @@ def _exporter_from_spec(spec: ExporterSpec, *, isolate_headers: bool = False) ->
OTLPSpanExporter as HTTPExporter,
)
- from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
-
- http_tls: Final = resolve_otlp_http_tls("TRACES", parse_headers(spec.headers), isolate_headers=isolate_headers)
+ http_tls: Final = resolve_otlp_http_tls("TRACES")
return HTTPExporter(
endpoint=spec.traces_endpoint or _otlp_traces_endpoint(spec.endpoint),
headers=parse_headers(spec.headers),
- _transport=build_otlp_http_transport("TRACES", http_tls),
+ certificate_file=http_tls.certificate_file,
+ session=http_tls.session,
)
if kind in _OTLP_GRPC_KINDS:
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
@@ -1016,7 +1013,7 @@ def _destination_processor(destination: "OtelDestination") -> SpanProcessor | No
headers=destination.header_string(),
owner=None,
)
- return _processor_for(_exporter_from_spec(spec, isolate_headers=True), use_simple=False)
+ return _processor_for(_exporter_from_spec(spec), use_simple=False)
except Exception as exc: # noqa: BLE001 # a malformed destination must not break the request or the other destinations
verbose_logger.debug("OTel V2 fan-out: no processor for %s: %s", destination.endpoint, exc)
return None
@@ -1189,13 +1186,12 @@ def build_metric_reader(config: OpenTelemetryV2Config) -> "MetricReader":
OTLPMetricExporter as HTTPMetricExporter,
)
- from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
-
- tls: Final = resolve_otlp_http_tls("METRICS", parse_headers(config.headers))
+ tls: Final = resolve_otlp_http_tls("METRICS")
exporter: object = HTTPMetricExporter(
endpoint=_otlp_metrics_endpoint(config.endpoint),
headers=parse_headers(config.headers),
- _transport=build_otlp_http_transport("METRICS", tls),
+ certificate_file=tls.certificate_file,
+ session=tls.session,
)
elif kind in ("otlp_grpc", "grpc"):
try:
@@ -1237,7 +1233,7 @@ def _otlp_logs_endpoint(endpoint: str | None) -> str | None:
return endpoint + "/v1/logs"
-def build_log_exporter(config: OpenTelemetryV2Config) -> LogRecordExporter:
+def build_log_exporter(config: OpenTelemetryV2Config) -> LogExporter:
"""Build a log exporter mirroring the exporter selection of the other signals.
``console`` (and any unrecognized kind) exports to the console; ``otlp_http``
@@ -1247,19 +1243,18 @@ def build_log_exporter(config: OpenTelemetryV2Config) -> LogRecordExporter:
"""
kind: Final = (config.exporter or "console").lower()
if kind in ("in_memory", "inmemory", "memory"):
- return InMemoryLogRecordExporter()
+ return InMemoryLogExporter()
if kind in ("otlp_http", "http", "http/protobuf", "http/json"):
from opentelemetry.exporter.otlp.proto.http._log_exporter import (
OTLPLogExporter as HTTPLogExporter,
)
- from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
-
- tls: Final = resolve_otlp_http_tls("LOGS", parse_headers(config.headers))
+ tls: Final = resolve_otlp_http_tls("LOGS")
return HTTPLogExporter(
endpoint=_otlp_logs_endpoint(config.endpoint),
headers=parse_headers(config.headers),
- _transport=build_otlp_http_transport("LOGS", tls),
+ certificate_file=tls.certificate_file,
+ session=tls.session,
)
if kind in ("otlp_grpc", "grpc"):
try:
@@ -1273,24 +1268,24 @@ def build_log_exporter(config: OpenTelemetryV2Config) -> LogRecordExporter:
) from exc
return GRPCLogExporter(endpoint=config.endpoint, headers=parse_headers(config.headers))
- return ConsoleLogRecordExporter()
+ return ConsoleLogExporter()
def build_logger_provider(
config: OpenTelemetryV2Config,
- log_exporter: LogRecordExporter | None = None,
+ log_exporter: LogExporter | None = None,
) -> SDKLoggerProvider:
"""Build the :class:`LoggerProvider` GenAI events export through.
``log_exporter`` is an explicit override (tests inject an
- ``InMemoryLogRecordExporter``); otherwise the exporter is selected from the config's
+ ``InMemoryLogExporter``); otherwise the exporter is selected from the config's
exporter kind via :func:`build_log_exporter`. Console and in-memory exporters
get a Simple processor (synchronous export, which tests rely on), everything
else a Batch processor — the same split as span processing.
"""
exporter: Final = log_exporter if log_exporter is not None else build_log_exporter(config)
provider: Final = SDKLoggerProvider(resource=build_resource(config))
- use_simple: Final = isinstance(exporter, (ConsoleLogRecordExporter, InMemoryLogRecordExporter))
+ use_simple: Final = isinstance(exporter, (ConsoleLogExporter, InMemoryLogExporter))
provider.add_log_record_processor(
SimpleLogRecordProcessor(exporter) if use_simple else BatchLogRecordProcessor(exporter)
)
diff --git a/litellm/integrations/otel/presets/agentops.py b/litellm/integrations/otel/presets/agentops.py
index 3408adbfb5f..3befb73b0cc 100644
--- a/litellm/integrations/otel/presets/agentops.py
+++ b/litellm/integrations/otel/presets/agentops.py
@@ -9,14 +9,14 @@ this preset registers a custom exporter (``kind="agentops"``) that mints the JWT
worker thread, off any event loop — and caches it for the process lifetime.
"""
+from collections.abc import Sequence
from typing import Final
import httpx
-import requests
-from opentelemetry.sdk.trace.export import SpanExporter
+from opentelemetry.sdk.trace import ReadableSpan
+from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
-from requests.auth import AuthBase
from litellm._logging import verbose_logger
from litellm.integrations.otel.model.config import (
@@ -24,7 +24,6 @@ from litellm.integrations.otel.model.config import (
ExporterSpec,
OpenTelemetryV2Config,
)
-from litellm.integrations.otel.plumbing.otlp_tls import OtlpHttpTls, resolve_otlp_http_tls
from litellm.integrations.otel.plumbing.providers import register_exporter_factory
_AGENTOPS_ENDPOINT: Final = "https://otlp.agentops.ai/v1/traces"
@@ -75,44 +74,47 @@ def agentops_preset(
)
-class _AgentOpsAuth(AuthBase):
- def __init__(self, api_key: str | None) -> None:
- self._api_key = api_key
- self._resolved = False
- self._token: str | None = None
-
- def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
- if not self._resolved:
- self._resolved = True
- if self._api_key:
- try:
- token: Final = _fetch_agentops_jwt(self._api_key).get("token")
- if isinstance(token, str) and token:
- self._token = token
- except Exception as error:
- verbose_logger.debug("AgentOps JWT fetch failed: %s", error)
- authenticated: Final = request.copy()
- if self._token:
- authenticated.headers["Authorization"] = f"Bearer {self._token}"
- return authenticated
-
-
def _build_agentops_exporter(spec: ExporterSpec) -> SpanExporter:
- """Authenticate lazily through requests' public session authentication API."""
- from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
+ """Factory for the ``agentops`` exporter kind: a lazy-auth OTLP/HTTP exporter."""
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
+ OTLPSpanExporter,
+ )
- from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
+ class _LazyAuthAgentOpsExporter(OTLPSpanExporter):
+ """OTLP/HTTP exporter that mints the AgentOps JWT on its first export.
+
+ ``export`` runs in the ``BatchSpanProcessor`` worker thread, so the
+ blocking token fetch never touches an event loop. The result is cached
+ after the first attempt (success or failure) so it runs at most once.
+ """
+
+ def __init__(self, *, endpoint: str | None, api_key: str | None) -> None:
+ super().__init__(endpoint=endpoint)
+ self._agentops_api_key = api_key
+ self._auth_resolved = False
+
+ def _ensure_authenticated(self) -> None:
+ if self._auth_resolved:
+ return
+ self._auth_resolved = True
+ if not self._agentops_api_key:
+ return
+ try:
+ token: Final = _fetch_agentops_jwt(self._agentops_api_key).get("token")
+ if token:
+ # ``_session`` is the requests.Session the base exporter
+ # POSTs through; updating its Authorization header is how the
+ # minted JWT reaches every subsequent export.
+ self._session.headers["Authorization"] = f"Bearer {token}"
+ except Exception as e:
+ verbose_logger.debug("AgentOps JWT fetch failed: %s", e)
+
+ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
+ self._ensure_authenticated()
+ return super().export(spans)
options: Final = spec.options or {}
- tls: Final = resolve_otlp_http_tls("TRACES", isolate_headers=True)
- session: Final = tls.session or requests.Session()
- session.auth = _AgentOpsAuth(options.get("api_key"))
- return OTLPSpanExporter(
- endpoint=spec.endpoint,
- _transport=build_otlp_http_transport(
- "TRACES", OtlpHttpTls(certificate_file=tls.certificate_file, session=session)
- ),
- )
+ return _LazyAuthAgentOpsExporter(endpoint=spec.endpoint, api_key=options.get("api_key"))
def _fetch_agentops_jwt(api_key: str) -> dict[str, object]:
diff --git a/litellm/proxy/enterprise_billing/billing_metrics.py b/litellm/proxy/enterprise_billing/billing_metrics.py
index 90616e8acb1..166ece28b83 100644
--- a/litellm/proxy/enterprise_billing/billing_metrics.py
+++ b/litellm/proxy/enterprise_billing/billing_metrics.py
@@ -25,8 +25,6 @@ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from litellm._logging import verbose_proxy_logger
-from litellm.integrations.otel.plumbing.otlp_tls import OtlpHttpTls
-from litellm.integrations.otel.plumbing.otlp_transport import build_otlp_http_transport
from litellm.proxy.middleware.billable_request_metrics_middleware import (
BillableCategory,
)
@@ -106,12 +104,10 @@ def build_mtls_meter_provider(config: BillingMetricsConfig) -> MeterProvider:
"""
exporter: Final = OTLPMetricExporter(
endpoint=_metrics_endpoint(config.endpoint),
- _transport=build_otlp_http_transport(
- "METRICS",
- OtlpHttpTls(certificate_file=config.ca_cert_path, session=None),
- client_certificate_file=config.client_cert_path,
- client_key_file=config.client_key_path,
- ),
+ # None -> exporter falls back to the system trust store.
+ certificate_file=config.ca_cert_path,
+ client_certificate_file=config.client_cert_path,
+ client_key_file=config.client_key_path,
)
reader: Final = PeriodicExportingMetricReader(exporter, export_interval_millis=config.export_interval_ms)
return MeterProvider(metric_readers=[reader], resource=Resource.create(_resource_attributes(config)))
diff --git a/pyproject.toml b/pyproject.toml
index 1789d5503c0..3aefd911937 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -177,17 +177,16 @@ proxy-runtime = [
"grpcio==1.78.0",
"prometheus-client>=0.20.0,<1.0",
"langfuse>=4.7,<5.0",
- "opentelemetry-api==1.45.1",
- "opentelemetry-sdk==1.45.1",
- "opentelemetry-exporter-otlp==1.45.1",
- "opentelemetry-exporter-otlp-json-http==0.66b1",
- "opentelemetry-instrumentation-fastapi==0.66b1",
+ "opentelemetry-api==1.33.1",
+ "opentelemetry-sdk==1.33.1",
+ "opentelemetry-exporter-otlp==1.33.1",
+ "opentelemetry-instrumentation-fastapi==0.54b1",
"ddtrace>=4.8.2,<5.0",
"sentry-sdk>=2.21.0,<3.0",
"mangum>=0.17.0,<1.0",
"azure-ai-contentsafety>=1.0.0,<2.0",
"azure-storage-file-datalake>=12.20.0,<13.0",
- "pypdf>=6.21.1,<7.0",
+ "pypdf>=6.16.1,<7.0",
"llm-sandbox>=0.3.39,<1.0",
"detect-secrets>=1.5.0,<2.0",
]
@@ -224,11 +223,10 @@ dev = [
"types-PyYAML==6.0.12.20250915",
"botocore-stubs==1.43.14",
"types-boto3[bedrock,bedrock-agent,bedrock-runtime,kms,s3,sagemaker-runtime,sts]==1.43.30",
- "opentelemetry-api==1.45.1",
- "opentelemetry-sdk==1.45.1",
- "opentelemetry-exporter-otlp==1.45.1",
- "opentelemetry-exporter-otlp-json-http==0.66b1",
- "opentelemetry-instrumentation-fastapi==0.66b1",
+ "opentelemetry-api==1.33.1",
+ "opentelemetry-sdk==1.33.1",
+ "opentelemetry-exporter-otlp==1.33.1",
+ "opentelemetry-instrumentation-fastapi==0.54b1",
"langfuse>=4.7,<5.0",
"fastapi-offline==1.7.6",
"fakeredis==2.34.1",
@@ -257,11 +255,10 @@ proxy-dev = [
"hypercorn==0.17.3",
"prometheus-client==0.20.0",
"sentry-sdk==2.21.0",
- "opentelemetry-api==1.45.1",
- "opentelemetry-sdk==1.45.1",
- "opentelemetry-exporter-otlp==1.45.1",
- "opentelemetry-exporter-otlp-json-http==0.66b1",
- "opentelemetry-instrumentation-fastapi==0.66b1",
+ "opentelemetry-api==1.33.1",
+ "opentelemetry-sdk==1.33.1",
+ "opentelemetry-exporter-otlp==1.33.1",
+ "opentelemetry-instrumentation-fastapi==0.54b1",
"azure-identity==1.25.2",
"a2a-sdk==1.1.0",
"backports-zstd==1.8.0; python_version < '3.14'",
@@ -283,7 +280,7 @@ ci = [
"langchain==1.3.9",
"lunary==1.4.36; python_version == '3.10'",
"lunary==1.4.37; python_version >= '3.11'",
- "logfire==5.2.0",
+ "logfire==4.6.0",
"traceloop-sdk==0.34.0",
"detect-secrets==1.5.0",
"PyGithub==2.8.1",
diff --git a/tests/unit/integrations/langfuse/test_langfuse_sdk.py b/tests/unit/integrations/langfuse/test_langfuse_sdk.py
index 8f5a8fc603b..cef2b13f0fd 100644
--- a/tests/unit/integrations/langfuse/test_langfuse_sdk.py
+++ b/tests/unit/integrations/langfuse/test_langfuse_sdk.py
@@ -165,30 +165,6 @@ def test_requested_trace_id_is_the_exported_trace_id_and_the_generation_is_its_r
assert span.parent is None
-
-@pytest.mark.parametrize("trace_id", ["0" * 31 + "1", None])
-def test_trace_randomness_flag_matches_the_id_source(channel, trace_id):
- from opentelemetry.context import Context
- from litellm.integrations.langfuse.langfuse_sdk import _start_span
-
- tracing, exporter = channel
- span = _start_span(
- tracing.tracer,
- name="id-source",
- context=Context(),
- start_time=CALL_START,
- trace_id=trace_id,
- observation_id=None,
- attributes={},
- )
- span.end(end_time=to_unix_nanos(CALL_END))
- tracing.flush()
- exported = _only_span(exporter, "id-source")
- assert exported.context.trace_flags.random_trace_id is (trace_id is None)
- assert exported.context.trace_flags.sampled
- if trace_id is not None:
- assert exported.context.trace_id == int(trace_id, 16)
-
def test_parent_observation_id_nests_the_generation_under_the_callers_observation(channel):
tracing, exporter = channel
_generation(tracing, name="child-gen", parent=PARENT_C).end(CALL_END)
diff --git a/tests/unit/integrations/otel/test_otel_v2_components.py b/tests/unit/integrations/otel/test_otel_v2_components.py
index 37392e3c77f..3f4389765d3 100644
--- a/tests/unit/integrations/otel/test_otel_v2_components.py
+++ b/tests/unit/integrations/otel/test_otel_v2_components.py
@@ -23,9 +23,8 @@ from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ( # noqa:
from opentelemetry import baggage # noqa: E402
from opentelemetry.context import attach, detach # noqa: E402
from opentelemetry._logs.severity import SeverityNumber # noqa: E402
-from opentelemetry._logs import LogRecord
-from opentelemetry.sdk._logs import ReadableLogRecord # noqa: E402
-from opentelemetry.sdk._logs.export import LogRecordExportResult # noqa: E402
+from opentelemetry.sdk._logs import LogData, LogRecord # noqa: E402
+from opentelemetry.sdk._logs.export import LogExportResult # noqa: E402
from opentelemetry.sdk.metrics import MeterProvider # noqa: E402
from opentelemetry.sdk.metrics.export import InMemoryMetricReader # noqa: E402
from opentelemetry.sdk.trace import TracerProvider # noqa: E402
@@ -677,9 +676,7 @@ def test_parse_headers_percent_decodes_values():
documents ``Authorization=Basic%20``. Forwarding the literal ``%20``
makes the backend reject the export as a malformed credential.
"""
- from base64 import b64encode
-
- token: Final = b64encode(b"TEST_ONLY_USER:TEST_ONLY_PASSWORD").decode()
+ token = "MTMzNzc4MzpnbGNfZXlKdklqb2lNVEl6TkNJPQ=="
assert providers.parse_headers(f"Authorization=Basic%20{token}") == {"authorization": f"Basic {token}"}
assert providers.parse_headers("x-scope-orgid=team%20a") == {"x-scope-orgid": "team a"}
@@ -736,7 +733,7 @@ def _export_one_trace_to_local_collector(exporter_kind: str) -> tuple[list[dict]
class Collector(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers["Content-Length"]))
- received.append({"path": self.path, "headers": requests.structures.CaseInsensitiveDict(self.headers), "body": body})
+ received.append({"path": self.path, "headers": dict(self.headers), "body": body})
self.send_response(200)
self.end_headers()
@@ -789,60 +786,13 @@ def test_http_json_exporter_posts_otlp_json_to_traces_endpoint():
assert parent["kind"] == 2
assert parent["traceId"] == format(trace_id, "032x")
assert parent["spanId"] == format(parent_id, "016x")
- assert parent.get("parentSpanId", "") == ""
+ assert "parentSpanId" not in parent
child = _only_span(child_request)
assert child["traceId"] == format(trace_id, "032x")
assert child["spanId"] == format(child_id, "016x")
assert child["parentSpanId"] == format(parent_id, "016x")
-
-@pytest.mark.parametrize("compression", ["none", "gzip", "deflate"])
-def test_http_json_exporter_preserves_session_tls_auth_and_compression(monkeypatch, requests_mock, compression):
- import gzip
- import zlib
-
- from opentelemetry.sdk.trace.export import SpanExportResult
- from litellm.integrations.otel.plumbing.otlp_json import OTLPJsonSpanExporter
-
- monkeypatch.setenv("REQUESTS_CA_BUNDLE", "/corporate/global-ca.pem")
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE", "/collector/environment-ca.pem")
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", compression)
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE", "/collector/client.crt")
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY", "/collector/client.key")
- monkeypatch.setenv("HTTPS_PROXY", "http://proxy.example:8080")
- collector = requests_mock.post("https://collector.example/v1/traces", status_code=200)
- session = requests.Session()
- session.headers["x-session-marker"] = "retained"
- session.auth = requests.auth.HTTPBasicAuth("session-user", "synthetic-password")
- exporter = OTLPJsonSpanExporter(
- endpoint="https://collector.example/v1/traces",
- headers={"x-exporter-marker": "configured"},
- certificate_file="/collector/ca.crt",
- session=session,
- )
- provider = TracerProvider()
- span = provider.get_tracer(__name__).start_span("session-export")
- span.end()
- try:
- assert exporter.export([span]) is SpanExportResult.SUCCESS
- request = collector.last_request
- assert request.headers["Content-Type"] == "application/json"
- assert request.headers["x-session-marker"] == "retained"
- assert request.headers["x-exporter-marker"] == "configured"
- assert request.headers["Authorization"] == "Basic c2Vzc2lvbi11c2VyOnN5bnRoZXRpYy1wYXNzd29yZA=="
- assert request.verify == "/collector/ca.crt"
- assert request.cert == ("/collector/client.crt", "/collector/client.key")
- assert request.proxies["https"] == "http://proxy.example:8080"
- decompress = {"none": lambda body: body, "gzip": gzip.decompress, "deflate": zlib.decompress}[compression]
- payload = json.loads(decompress(request.body))
- exported = payload["resourceSpans"][0]["scopeSpans"][0]["spans"][0]
- assert exported["name"] == "session-export"
- assert exported["traceId"] == f"{span.get_span_context().trace_id:032x}"
- finally:
- exporter.shutdown()
- provider.shutdown()
-
def test_http_protobuf_exporter_still_posts_protobuf():
(_child_request, parent_request), (trace_id, _parent_id, _child_id) = _export_one_trace_to_local_collector(
"http/protobuf"
@@ -974,20 +924,20 @@ def test_otlp_logs_endpoint_normalization():
def test_build_log_exporter_variants():
- from opentelemetry.sdk._logs.export import ConsoleLogRecordExporter, InMemoryLogRecordExporter
+ from opentelemetry.sdk._logs.export import ConsoleLogExporter, InMemoryLogExporter
assert isinstance(
providers.build_log_exporter(OpenTelemetryV2Config(exporter="console")),
- ConsoleLogRecordExporter,
+ ConsoleLogExporter,
)
assert isinstance(
providers.build_log_exporter(OpenTelemetryV2Config(exporter="in_memory")),
- InMemoryLogRecordExporter,
+ InMemoryLogExporter,
)
# An unrecognized kind falls back to console rather than dropping events.
assert isinstance(
providers.build_log_exporter(OpenTelemetryV2Config(exporter="unknown")),
- ConsoleLogRecordExporter,
+ ConsoleLogExporter,
)
http_exporter = providers.build_log_exporter(OpenTelemetryV2Config(exporter="otlp_http", endpoint="http://h:4318"))
assert "OTLPLogExporter" in type(http_exporter).__name__
@@ -1680,13 +1630,9 @@ def test_v2_otlp_http_log_export_trusts_ssl_cert_file(monkeypatch: pytest.Monkey
severity_number=SeverityNumber.INFO,
body="v2-tls-test",
)
- log_data = ReadableLogRecord(
- log_record=record,
- resource=providers.build_resource(cfg),
- instrumentation_scope=InstrumentationScope("v2-tls-test"),
- )
+ log_data = LogData(log_record=record, instrumentation_scope=InstrumentationScope("v2-tls-test"))
result = exporter.export([log_data])
- assert result is LogRecordExportResult.SUCCESS, f"log export failed: {result}"
+ assert result is LogExportResult.SUCCESS, f"log export failed: {result}"
assert tls_sink.received.get(timeout=5) == "/v1/logs"
finally:
exporter.shutdown()
diff --git a/tests/unit/integrations/otel/test_otel_v2_destinations.py b/tests/unit/integrations/otel/test_otel_v2_destinations.py
index a8d8da9b426..c7a057cca5c 100644
--- a/tests/unit/integrations/otel/test_otel_v2_destinations.py
+++ b/tests/unit/integrations/otel/test_otel_v2_destinations.py
@@ -2,8 +2,6 @@
import asyncio
import contextvars
-import gzip
-import os
import threading
import time
from base64 import b64encode
@@ -5069,267 +5067,3 @@ class TestArizeProjectRouting:
monkeypatch.setenv("ARIZE_PROJECT_NAME", "gateway-prod")
assert self._forwarded_projects() == {"gateway-prod"}
-
-
-def _export_header_probe(api: str, signal: str, endpoint: str, headers: str | None) -> None:
- from opentelemetry.sdk.metrics import MeterProvider
-
- from litellm.integrations.opentelemetry import OpenTelemetry, OpenTelemetryConfig
-
- config: Final = OpenTelemetryV2Config(
- exporter="http/json" if api == "v2-json" else "otlp_http", endpoint=endpoint, headers=headers
- )
- tracing: Final = TracerProvider()
- legacy: Final = (
- OpenTelemetry(
- config=OpenTelemetryConfig(exporter="otlp_http", endpoint=endpoint, headers=headers),
- tracer_provider=tracing,
- )
- if api == "v1"
- else None
- )
- try:
- if signal == "metrics":
- reader: Final = legacy._get_metric_reader() if legacy else otel_providers.build_metric_reader(config)
- metrics: Final = MeterProvider(metric_readers=(reader,))
- try:
- metrics.get_meter("header-isolation").create_counter("header_probe").add(1)
- assert metrics.force_flush()
- finally:
- metrics.shutdown()
- elif signal == "logs":
- logs: Final = legacy._get_log_exporter() if legacy else otel_providers.build_log_exporter(config)
- try:
- assert logs.export(()).name == "SUCCESS"
- finally:
- logs.shutdown()
- elif legacy:
- processor: Final = legacy._get_span_processor()
- try:
- assert processor.span_exporter.export(()).name == "SUCCESS"
- finally:
- processor.shutdown()
- else:
- spans: Final = otel_providers.build_span_exporter(config)
- try:
- assert spans.export(()).name == "SUCCESS"
- finally:
- spans.shutdown()
- finally:
- tracing.shutdown()
-
-
-@pytest.mark.parametrize(
- ("api", "signal"),
- (
- ("v1", "traces"),
- ("v1", "metrics"),
- ("v1", "logs"),
- ("v2", "traces"),
- ("v2", "metrics"),
- ("v2", "logs"),
- ("v2-json", "traces"),
- ),
-)
-@pytest.mark.parametrize("signal_headers", (False, True))
-def test_http_destinations_do_not_inherit_other_accounts_headers(
- monkeypatch, requests_mock, api: str, signal: str, signal_headers: bool
-) -> None:
- variable: Final = f"OTEL_EXPORTER_OTLP_{signal.upper()}_HEADERS" if signal_headers else "OTEL_EXPORTER_OTLP_HEADERS"
- operator_headers: Final = "Authorization=Bearer%20operator-key,x-operator-key=operator-only"
- monkeypatch.setenv(variable, operator_headers)
- for account, configured in (("tenant-a", "a-key"), ("tenant-b", "b-key"), ("operator", None)):
- endpoint: Final = f"https://{account}.example.test"
- captured: Final = requests_mock.post(f"{endpoint}/v1/{signal}", content=b"")
-
- _export_header_probe(api, signal, endpoint, f"x-tenant-key={configured}" if configured else None)
-
- assert captured.request_history, f"{account} never exported {signal}"
- for request in captured.request_history:
- credentials: Final = {
- name.lower(): value
- for name, value in request.headers.items()
- if name.lower() in {"authorization", "x-operator-key", "x-tenant-key"}
- }
- assert credentials == (
- {"x-tenant-key": configured}
- if configured
- else {"authorization": "Bearer operator-key", "x-operator-key": "operator-only"}
- )
- assert os.environ[variable] == operator_headers
-
-
-@pytest.mark.parametrize("protocol", ("otlp_http", "http/json"))
-def test_headerless_tenant_destination_does_not_receive_operator_credentials(monkeypatch, requests_mock, protocol):
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer%20operator-key")
- captured: Final = requests_mock.post("https://tenant.example.test/v1/traces", content=b"")
- processor: Final = otel_providers._destination_processor(
- OtelDestination(endpoint="https://tenant.example.test", headers={}, protocol=protocol)
- )
- assert processor is not None
- provider: Final = TracerProvider()
- provider.add_span_processor(processor)
- try:
- emit(provider, "headerless-tenant")
- assert provider.force_flush()
- assert captured.request_history
- assert all("Authorization" not in request.headers for request in captured.request_history)
- finally:
- provider.shutdown()
-
-
-@pytest.mark.parametrize("api", ("v2", "v2-json"))
-def test_isolated_headers_keep_the_exporters_content_type_and_compression(monkeypatch, requests_mock, api):
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "Content-Type=operator-only,Content-Encoding=operator-only")
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", "gzip")
- captured: Final = requests_mock.post("https://tenant.example.test/v1/traces", content=b"")
-
- _export_header_probe(api, "traces", "https://tenant.example.test", "x-tenant-key=tenant-key")
-
- assert len(captured.request_history) == 1
- request: Final = captured.last_request
- assert request.headers["Content-Type"] == ("application/json" if api == "v2-json" else "application/x-protobuf")
- assert request.headers["Content-Encoding"] == "gzip"
- monkeypatch.delenv("OTEL_EXPORTER_OTLP_TRACES_COMPRESSION")
-
- _export_header_probe(api, "traces", "https://tenant.example.test", "x-tenant-key=tenant-key")
-
- assert gzip.decompress(request.body) == (captured.last_request.body or b"")
-
-
-@pytest.mark.parametrize("api", ("v2", "v2-json"))
-@pytest.mark.parametrize("header", ("Authorization", "AUTHORIZATION", "authorization"))
-def test_tenant_authorization_replaces_operator_authorization(monkeypatch, requests_mock, api, header):
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer%20operator-key")
- captured: Final = requests_mock.post("https://tenant.example.test/v1/traces", content=b"")
-
- _export_header_probe(api, "traces", "https://tenant.example.test", f"{header}=Bearer tenant-key")
-
- assert len(captured.request_history) == 1
- assert captured.last_request.headers["Authorization"] == "Bearer tenant-key"
- assert "Bearer operator-key" not in captured.last_request.headers.values()
-
-
-@pytest.mark.parametrize("protocol", ("otlp_http", "http/json"))
-def test_session_defaults_cannot_restore_excluded_operator_headers(monkeypatch, requests_mock, protocol):
- from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
-
- from litellm.integrations.otel.plumbing.otlp_json import OTLPJsonSpanExporter
- from litellm.integrations.otel.plumbing.otlp_tls import resolve_otlp_http_tls
-
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer%20operator-key,x-operator-key=operator-only")
- captured: Final = requests_mock.post("https://tenant.example.test/v1/traces", content=b"")
- headers: Final = {"x-tenant-key": "tenant-key"}
- tls: Final = resolve_otlp_http_tls("TRACES", headers)
- assert tls.session is not None
- tls.session.headers.update({"AUTHORIZATION": "Bearer operator-key", "X-Operator-Key": "operator-only"})
- exporter_type: Final = OTLPJsonSpanExporter if protocol == "http/json" else OTLPSpanExporter
- exporter: Final = exporter_type(
- endpoint="https://tenant.example.test/v1/traces", headers=headers, session=tls.session
- )
- try:
- assert exporter.export(()).name == "SUCCESS"
- assert len(captured.request_history) == 1
- assert captured.last_request.headers["x-tenant-key"] == "tenant-key"
- assert "Authorization" not in captured.last_request.headers
- assert "x-operator-key" not in captured.last_request.headers
- finally:
- exporter.shutdown()
-
-
-@pytest.mark.parametrize(
- ("api", "signal"),
- (("v1", "traces"), ("v1", "logs"), ("v1", "metrics"),
- ("v2", "traces"), ("v2", "logs"), ("v2", "metrics"), ("v2-json", "traces")),
-)
-@pytest.mark.parametrize("requests_ca", ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE"))
-@pytest.mark.parametrize("collector_ca", ("signal", "global", "SSL_CERT_FILE", "ssl_verify"))
-@pytest.mark.parametrize("tenant", (False, True))
-def test_explicit_collector_ca_precedes_requests_environment(
- monkeypatch, requests_mock, tmp_path, api: str, signal: str, requests_ca: str, collector_ca: str, tenant: bool
-) -> None:
- certificate_path: Final = tmp_path / "collector-ca.pem"
- certificate_path.write_text("TEST_ONLY_CA")
- certificate: Final = str(certificate_path)
- for variable in ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "SSL_CERT_FILE", "SSL_VERIFY",
- "OTEL_EXPORTER_OTLP_CERTIFICATE", f"OTEL_EXPORTER_OTLP_{signal.upper()}_CERTIFICATE"):
- monkeypatch.delenv(variable, raising=False)
- monkeypatch.setattr(litellm, "ssl_verify", True)
- monkeypatch.setenv(requests_ca, "/corporate/global-ca.pem")
- if collector_ca == "ssl_verify":
- monkeypatch.setattr(litellm, "ssl_verify", certificate)
- else:
- variable: Final = (
- f"OTEL_EXPORTER_OTLP_{signal.upper()}_CERTIFICATE" if collector_ca == "signal"
- else "OTEL_EXPORTER_OTLP_CERTIFICATE" if collector_ca == "global" else collector_ca
- )
- monkeypatch.setenv(variable, certificate)
- if collector_ca == "signal":
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_CERTIFICATE", "/collector/generic-ca.pem")
- monkeypatch.setenv(f"OTEL_EXPORTER_OTLP_{signal.upper()}_CLIENT_CERTIFICATE", "/collector/client.pem")
- monkeypatch.setenv(f"OTEL_EXPORTER_OTLP_{signal.upper()}_CLIENT_KEY", "/collector/client.key")
- monkeypatch.setenv("HTTPS_PROXY", "http://proxy.example.invalid:8080")
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer%20TEST_ONLY_OPERATOR")
- captured: Final = requests_mock.post(f"https://collector.example.invalid/v1/{signal}", content=b"")
-
- _export_header_probe(api, signal, "https://collector.example.invalid", "x-tenant-key=TEST_ONLY" if tenant else None)
-
- assert captured.request_history
- for request in captured.request_history:
- assert request.verify == certificate
- assert request.cert == ("/collector/client.pem", "/collector/client.key")
- assert request.proxies["https"] == "http://proxy.example.invalid:8080"
- assert request.headers.get("Authorization") == (None if tenant else "Bearer TEST_ONLY_OPERATOR")
- assert request.headers.get("x-tenant-key") == ("TEST_ONLY" if tenant else None)
-
-
-@pytest.mark.parametrize("api", ("v1", "v2"))
-@pytest.mark.parametrize("signal", ("traces", "logs", "metrics"))
-@pytest.mark.parametrize("global_provider", (False, True))
-@pytest.mark.parametrize("tenant", (False, True))
-def test_collector_tls_preserves_operator_credential_provider_without_tenant_leakage(
- monkeypatch, requests_mock, api: str, signal: str, global_provider: bool, tenant: bool
-) -> None:
- from types import SimpleNamespace
-
- import requests
- from opentelemetry.exporter.otlp.proto.http import _common
-
- session: Final = requests.Session()
- session.auth = requests.auth.HTTPBasicAuth("TEST_ONLY_USER", "TEST_ONLY_PASSWORD")
- session.headers["x-session-marker"] = "TEST_ONLY_SESSION"
- provider_variable: Final = (
- "OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER" if global_provider
- else f"OTEL_PYTHON_EXPORTER_OTLP_HTTP_{signal.upper()}_CREDENTIAL_PROVIDER"
- )
- monkeypatch.setenv(provider_variable, "TEST_ONLY_PROVIDER")
- monkeypatch.setenv(f"OTEL_EXPORTER_OTLP_{signal.upper()}_CERTIFICATE", "/collector/collector-ca.pem")
- monkeypatch.setenv("REQUESTS_CA_BUNDLE", "/corporate/global-ca.pem")
- monkeypatch.setattr(_common, "entry_points", lambda **kwargs: (SimpleNamespace(load=lambda: lambda: session),))
- captured: Final = requests_mock.post(f"https://collector.example.invalid/v1/{signal}", content=b"")
-
- _export_header_probe(api, signal, "https://collector.example.invalid", "x-tenant-key=TEST_ONLY" if tenant else None)
-
- assert captured.request_history
- for request in captured.request_history:
- assert request.verify == "/collector/collector-ca.pem"
- assert request.headers.get("x-session-marker") == (None if tenant else "TEST_ONLY_SESSION")
- assert request.headers.get("Authorization") == (
- None if tenant else "Basic " + b64encode(b"TEST_ONLY_USER:TEST_ONLY_PASSWORD").decode()
- )
-
-
-@pytest.mark.parametrize("api", ("v1", "v2", "v2-json"))
-@pytest.mark.parametrize("requests_ca", ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE"))
-def test_requests_environment_ca_remains_fallback(monkeypatch, requests_mock, api: str, requests_ca: str) -> None:
- for variable in ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "SSL_CERT_FILE", "SSL_VERIFY",
- "OTEL_EXPORTER_OTLP_CERTIFICATE", "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE"):
- monkeypatch.delenv(variable, raising=False)
- monkeypatch.setattr(litellm, "ssl_verify", True)
- monkeypatch.setenv(requests_ca, "/corporate/global-ca.pem")
- captured: Final = requests_mock.post("https://collector.example.invalid/v1/traces", content=b"")
-
- _export_header_probe(api, "traces", "https://collector.example.invalid", None)
-
- assert captured.request_history
- assert captured.last_request.verify == "/corporate/global-ca.pem"
diff --git a/tests/unit/integrations/otel/test_otel_v2_mount.py b/tests/unit/integrations/otel/test_otel_v2_mount.py
index 22d7e4cd317..3a27c3ec4e4 100644
--- a/tests/unit/integrations/otel/test_otel_v2_mount.py
+++ b/tests/unit/integrations/otel/test_otel_v2_mount.py
@@ -382,7 +382,7 @@ def test_every_traced_response_names_its_own_server_span(monkeypatch):
assert responses["/stream"].text == "data: first\n\ndata: [DONE]\n\n"
for path, response in responses.items():
context = server_spans[path].context
- assert response.headers.get_list("traceresponse") == [f"00-{context.trace_id:032x}-{context.span_id:016x}-{context.trace_flags:02x}"]
+ assert response.headers.get_list("traceresponse") == [f"00-{context.trace_id:032x}-{context.span_id:016x}-01"]
@pytest.mark.parametrize("path", ["/relayed", "/relayed/health"])
@@ -472,20 +472,13 @@ def test_gate_off_responses_carry_no_traceresponse(monkeypatch):
assert "traceresponse" not in TestClient(app).get("/ping").headers
-def test_started_app_keeps_serving_without_registering_a_response_propagator(monkeypatch):
+def test_failed_instrumentation_registers_no_response_propagator(monkeypatch):
monkeypatch.setenv("LITELLM_OTEL_V2", "1")
app = fastapi.FastAPI()
- @app.get("/running")
- def running():
- return {"ok": True}
-
- with TestClient(app) as client:
+ with TestClient(app):
instrument_fastapi_app(app)
- response = client.get("/running")
- assert response.json() == {"ok": True}
- assert "traceresponse" not in response.headers
assert get_global_response_propagator() is None
@@ -552,4 +545,4 @@ def test_the_unhandled_error_helper_names_the_span_it_is_given_in_any_context(mo
headers = contextvars.Context().run(trace_response_headers, span)
- assert headers["traceresponse"] == f"00-{context.trace_id:032x}-{context.span_id:016x}-{context.trace_flags:02x}"
+ assert headers["traceresponse"] == f"00-{context.trace_id:032x}-{context.span_id:016x}-01"
diff --git a/tests/unit/integrations/otel/test_otel_v2_presets.py b/tests/unit/integrations/otel/test_otel_v2_presets.py
index 60207a2b5bc..64366626784 100644
--- a/tests/unit/integrations/otel/test_otel_v2_presets.py
+++ b/tests/unit/integrations/otel/test_otel_v2_presets.py
@@ -2,8 +2,6 @@
event loop: the preset does no network I/O, and a custom exporter mints the JWT
lazily on its first export (in the BatchSpanProcessor worker thread)."""
-import json
-
import httpx
import pytest
@@ -77,53 +75,50 @@ def test_dynamic_cred_presets_tag_exporter_with_matching_owner(monkeypatch):
)
-def test_agentops_exporter_mints_jwt_lazily(monkeypatch, requests_mock, respx_mock):
- from opentelemetry.sdk.trace import TracerProvider
- from opentelemetry.sdk.trace.export import SpanExportResult
-
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Bearer%20operator,x-operator-key=operator-secret")
- auth = respx_mock.post(agentops_mod._AGENTOPS_AUTH_ENDPOINT).respond(200, json={"token": "jwt-xyz"})
- collector = requests_mock.post(_AGENTOPS_ENDPOINT, status_code=200)
- exporter = _build_agentops_exporter(
- ExporterSpec(kind=_AGENTOPS_EXPORTER_KIND, endpoint=_AGENTOPS_ENDPOINT, options={"api_key": "ak"})
+def test_agentops_exporter_mints_jwt_lazily(monkeypatch):
+ pytest.importorskip("opentelemetry.exporter.otlp.proto.http.trace_exporter")
+ monkeypatch.setattr(
+ agentops_mod, "_fetch_agentops_jwt", lambda _k: {"token": "jwt-xyz"}
)
- provider = TracerProvider()
- span = provider.get_tracer(__name__).start_span("authenticated")
- span.end()
- try:
- assert auth.call_count == 0
- assert exporter.export([span]) is SpanExportResult.SUCCESS
- assert exporter.export([]) is SpanExportResult.SUCCESS
- assert auth.call_count == 1
- assert json.loads(auth.calls[0].request.content) == {"api_key": "ak"}
- assert [request.headers["Authorization"] for request in collector.request_history] == [
- "Bearer jwt-xyz",
- "Bearer jwt-xyz",
- ]
- assert all("x-operator-key" not in request.headers for request in collector.request_history)
- finally:
- exporter.shutdown()
- provider.shutdown()
-
-
-def test_agentops_exporter_tolerates_fetch_failure(monkeypatch, requests_mock, respx_mock):
- from opentelemetry.sdk.trace.export import SpanExportResult
-
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "Authorization=Bearer%20operator,x-operator-key=operator-secret")
- auth = respx_mock.post(agentops_mod._AGENTOPS_AUTH_ENDPOINT).respond(503, text="auth down")
- collector = requests_mock.post(_AGENTOPS_ENDPOINT, status_code=200)
- exporter = _build_agentops_exporter(
- ExporterSpec(kind=_AGENTOPS_EXPORTER_KIND, endpoint=_AGENTOPS_ENDPOINT, options={"api_key": "ak"})
+ spec = ExporterSpec(
+ kind=_AGENTOPS_EXPORTER_KIND,
+ endpoint=_AGENTOPS_ENDPOINT,
+ options={"api_key": "ak"},
)
- try:
- assert exporter.export([]) is SpanExportResult.SUCCESS
- assert exporter.export([]) is SpanExportResult.SUCCESS
- assert auth.call_count == 1
- assert collector.call_count == 2
- assert all("Authorization" not in request.headers for request in collector.request_history)
- assert all("x-operator-key" not in request.headers for request in collector.request_history)
- finally:
- exporter.shutdown()
+ exporter = _build_agentops_exporter(spec)
+
+ # No auth header until the first export triggers the (off-loop) fetch.
+ assert "Authorization" not in exporter._session.headers
+ exporter._ensure_authenticated()
+ assert exporter._session.headers["Authorization"] == "Bearer jwt-xyz"
+
+ # Cached: a second resolution does not re-fetch.
+ calls = []
+ monkeypatch.setattr(
+ agentops_mod,
+ "_fetch_agentops_jwt",
+ lambda k: calls.append(k) or {"token": "again"},
+ )
+ exporter._ensure_authenticated()
+ assert calls == []
+
+
+def test_agentops_exporter_tolerates_fetch_failure(monkeypatch):
+ pytest.importorskip("opentelemetry.exporter.otlp.proto.http.trace_exporter")
+
+ def _raise(_k):
+ raise RuntimeError("auth down")
+
+ monkeypatch.setattr(agentops_mod, "_fetch_agentops_jwt", _raise)
+ exporter = _build_agentops_exporter(
+ ExporterSpec(
+ kind=_AGENTOPS_EXPORTER_KIND,
+ endpoint=_AGENTOPS_ENDPOINT,
+ options={"api_key": "ak"},
+ )
+ )
+ exporter._ensure_authenticated() # must not raise
+ assert "Authorization" not in exporter._session.headers
def test_fetch_jwt_uses_owned_client_not_shared_pool(monkeypatch):
diff --git a/tests/unit/integrations/test_langfuse_otel.py b/tests/unit/integrations/test_langfuse_otel.py
index cc31e2788da..9f536ab0dac 100644
--- a/tests/unit/integrations/test_langfuse_otel.py
+++ b/tests/unit/integrations/test_langfuse_otel.py
@@ -591,7 +591,7 @@ class TestLangfuseOtelKeyDynamicConfig:
assert logger.construct_dynamic_otel_config(self._dynamic_params(langfuse_secret_key=None)) is None
assert logger.construct_dynamic_otel_config(self._dynamic_params(langfuse_public_key=None)) is None
- def test_key_dynamic_params_create_otlp_exporter_without_global_env(self, requests_mock):
+ def test_key_dynamic_params_create_otlp_exporter_without_global_env(self):
"""Without global LANGFUSE_* env vars, a request carrying key-scoped Langfuse
credentials must get a tracer exporting via OTLP HTTP to that key's host."""
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
@@ -617,17 +617,15 @@ class TestLangfuseOtelKeyDynamicConfig:
exporter = span_processors[0].span_exporter
assert isinstance(exporter, OTLPSpanExporter)
+ assert exporter._endpoint == "https://langfuse.example.com/api/public/otel/v1/traces"
import base64
expected_auth = base64.b64encode(b"key_public:key_secret").decode()
- collector = requests_mock.post("https://langfuse.example.com/api/public/otel/v1/traces", status_code=200)
- with tracer.start_as_current_span("dynamic-credentials"):
- pass
- assert provider.force_flush()
- assert collector.call_count == 1
- assert collector.last_request.headers["Authorization"] == f"Basic {expected_auth}"
- assert collector.last_request.headers["x-langfuse-ingestion-version"] == "4"
+ assert exporter._headers == {
+ "Authorization": f"Basic {expected_auth}",
+ "x-langfuse-ingestion-version": "4",
+ }
def test_key_dynamic_params_reuse_cached_provider(self):
with self._clean_env():
@@ -646,7 +644,7 @@ class TestLangfuseOtelKeyDynamicConfig:
assert tracer is logger.tracer
assert logger._tracer_provider_cache == {}
- def test_key_credentials_never_passed_to_debug_logger(self, requests_mock):
+ def test_key_credentials_never_passed_to_debug_logger(self):
"""The span-processor debug logs must receive a redacted header value, so the
key-scoped Langfuse secret never enters a log record regardless of downstream
handler configuration, while the exporter still gets the real header."""
@@ -680,12 +678,10 @@ class TestLangfuseOtelKeyDynamicConfig:
provider = next(iter(logger._tracer_provider_cache.values())).provider
exporter = provider._active_span_processor._span_processors[0].span_exporter
assert isinstance(exporter, OTLPSpanExporter)
- from opentelemetry.sdk.trace.export import SpanExportResult
-
- collector = requests_mock.post("https://langfuse.example.com/api/public/otel/v1/traces", status_code=200)
- assert exporter.export([]) is SpanExportResult.SUCCESS
- assert collector.last_request.headers["Authorization"] == f"Basic {secret}"
- assert collector.last_request.headers["x-langfuse-ingestion-version"] == "4"
+ assert exporter._headers == {
+ "Authorization": f"Basic {secret}",
+ "x-langfuse-ingestion-version": "4",
+ }
class TestLangfuseOtelResponsesAPI:
diff --git a/tests/unit/integrations/test_opentelemetry.py b/tests/unit/integrations/test_opentelemetry.py
index 8b14fbddaf8..d14703e48a1 100644
--- a/tests/unit/integrations/test_opentelemetry.py
+++ b/tests/unit/integrations/test_opentelemetry.py
@@ -12,23 +12,21 @@ import weakref
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import MappingProxyType
-from typing import ClassVar, Final
+from typing import Final
from unittest.mock import MagicMock, patch
import pytest
# Adds the grandparent directory to sys.path to allow importing project modules
from opentelemetry import trace
-from opentelemetry._logs import LogRecord
from opentelemetry._logs.severity import SeverityNumber
-from opentelemetry.sdk._logs import ReadableLogRecord
+from opentelemetry.sdk._logs import LogData, LogRecord
from opentelemetry.sdk._logs import LoggerProvider as OTLoggerProvider
-from opentelemetry.sdk._logs.export import InMemoryLogExporter, LogRecordExportResult, SimpleLogRecordProcessor
+from opentelemetry.sdk._logs.export import InMemoryLogExporter, LogExportResult, SimpleLogRecordProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader, MetricsData
-from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import ReadableSpan, SpanLimits, TracerProvider
-from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor, SpanExportResult
+from opentelemetry.sdk.trace.export import SimpleSpanProcessor, SpanExportResult
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from parameterized import parameterized
@@ -2323,13 +2321,9 @@ def test_otlp_http_log_export_trusts_ssl_cert_file(monkeypatch: pytest.MonkeyPat
severity_number=SeverityNumber.INFO,
body="tls-export-test",
)
- log_data: Final = ReadableLogRecord(
- log_record=record,
- resource=Resource.create({}),
- instrumentation_scope=InstrumentationScope("tls-export-test"),
- )
+ log_data: Final = LogData(log_record=record, instrumentation_scope=InstrumentationScope("tls-export-test"))
result: Final = exporter.export([log_data])
- assert result is LogRecordExportResult.SUCCESS, f"log export failed: {result}"
+ assert result is LogExportResult.SUCCESS, f"log export failed: {result}"
assert tls_sink.received.get(timeout=5) == "/v1/logs"
finally:
exporter.shutdown()
@@ -6645,7 +6639,7 @@ class TestOpenTelemetryProviderlessCallAttributes(unittest.TestCase):
time.sleep(self.POLL_INTERVAL)
return None
- def _emitted_log_records(self, semconv_opt_in: str) -> tuple[ReadableLogRecord, ...]:
+ def _emitted_log_records(self, semconv_opt_in: str) -> tuple[LogData, ...]:
log_exporter = InMemoryLogExporter()
logger_provider = OTLoggerProvider()
logger_provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter))
@@ -6719,19 +6713,7 @@ class TestDynamicTracerProviderCache(unittest.TestCase):
must be bounded and must shut down whatever it drops (LIT-5437: threads accumulated
until pods were OOMKilled)."""
- BSP_THREAD_NAME: ClassVar[str]
-
- @classmethod
- def setUpClass(cls) -> None:
- before: Final = frozenset(threading.enumerate())
- processor: Final = BatchSpanProcessor(InMemorySpanExporter())
- try:
- workers: Final = tuple(thread for thread in threading.enumerate() if thread not in before)
- assert len(workers) == 1
- assert workers[0].is_alive()
- cls.BSP_THREAD_NAME = workers[0].name
- finally:
- processor.shutdown()
+ BSP_THREAD_NAME = "OtelBatchSpanProcessor"
def _logger(self, cap=3, exporter="console"):
logger = OpenTelemetry(
@@ -6769,6 +6751,8 @@ class TestDynamicTracerProviderCache(unittest.TestCase):
logger._get_tracer_with_dynamic_headers({"authorization": f"Basic tenant-{i}"})
self.assertEqual(len(logger._tracer_provider_cache), 3)
+ # Guards the thread-name constant: a rename upstream would make this read 0 and the
+ # bound assertion below would pass while measuring nothing.
self.assertGreaterEqual(len(self._live_exporter_threads()), 1)
self.assertLessEqual(self._wait_for_exporter_threads(before + 3) - before, 3)
@@ -6939,8 +6923,8 @@ class TestOpenTelemetryDatabaseSemconvAttributes(unittest.TestCase):
database's own metrics.
"""
- DSN = "postgresql://TEST_ONLY_USER:TEST_ONLY_PASSWORD@primary-db.example.invalid:6432/TEST_ONLY_DATABASE?schema=reporting"
- REPLICA_DSN = "postgresql://TEST_ONLY_REPLICA_USER:TEST_ONLY_REPLICA_PASSWORD@replica-db.example.invalid/TEST_ONLY_DATABASE"
+ DSN = "postgresql://llmproxy:dbpassword9090@litellm-prod.abc123.us-east-1.rds.amazonaws.com:6432/litellm?schema=reporting"
+ REPLICA_DSN = "postgresql://reader:r3ad0nly@litellm-prod-ro.abc123.us-east-1.rds.amazonaws.com/litellm"
def _service_span(self, service, call_type, dsn, error=None, replica_dsn=None):
exporter = InMemorySpanExporter()
@@ -6981,10 +6965,10 @@ class TestOpenTelemetryDatabaseSemconvAttributes(unittest.TestCase):
self.assertEqual(span.attributes["db.operation.name"], "get_data")
self.assertEqual(
span.attributes["server.address"],
- "primary-db.example.invalid",
+ "litellm-prod.abc123.us-east-1.rds.amazonaws.com",
)
self.assertEqual(span.attributes["server.port"], 6432)
- self.assertEqual(span.attributes["db.namespace"], "TEST_ONLY_DATABASE|reporting")
+ self.assertEqual(span.attributes["db.namespace"], "litellm|reporting")
def test_datastore_span_is_a_client_span_carrying_the_legacy_db_system(self):
"""Datadog types a span as a database call from CLIENT kind plus
@@ -7010,16 +6994,16 @@ class TestOpenTelemetryDatabaseSemconvAttributes(unittest.TestCase):
self.assertEqual(span.kind, trace.SpanKind.CLIENT)
self.assertEqual(
span.attributes["server.address"],
- "primary-db.example.invalid",
+ "litellm-prod.abc123.us-east-1.rds.amazonaws.com",
)
self.assertEqual(span.attributes["error"], "connection refused")
def test_no_credential_from_the_dsn_lands_on_the_span(self):
span = self._service_span(ServiceTypes.DB, "get_data", self.DSN)
exported = " ".join(str(value) for value in span.attributes.values())
- self.assertIn("primary-db.example.invalid", exported)
- self.assertNotIn("TEST_ONLY_PASSWORD", exported)
- self.assertNotIn("TEST_ONLY_USER", exported)
+ self.assertIn("litellm-prod.abc123.us-east-1.rds.amazonaws.com", exported)
+ self.assertNotIn("dbpassword9090", exported)
+ self.assertNotIn("llmproxy", exported)
def test_redis_span_does_not_borrow_the_postgres_endpoint(self):
span = self._service_span(ServiceTypes.REDIS, "async_set_cache", self.DSN)
diff --git a/tests/unit/litellm_core_utils/test_litellm_logging.py b/tests/unit/litellm_core_utils/test_litellm_logging.py
index 5b67250ad00..bbfae2229ce 100644
--- a/tests/unit/litellm_core_utils/test_litellm_logging.py
+++ b/tests/unit/litellm_core_utils/test_litellm_logging.py
@@ -2043,7 +2043,7 @@ async def test_logfire_logger_accepts_env_vars_for_base_url(monkeypatch):
],
)
def test_langtrace_callback_exports_to_api_trace_with_x_api_key(
- monkeypatch: pytest.MonkeyPatch, api_host: str | None, expected_endpoint: str, requests_mock
+ monkeypatch: pytest.MonkeyPatch, api_host: str | None, expected_endpoint: str
) -> None:
"""The exporter must post to Langtrace's complete /api/trace path with the key in x-api-key,
without leaking it into the process-wide OTEL_EXPORTER_OTLP_TRACES_HEADERS."""
@@ -2069,12 +2069,8 @@ def test_langtrace_callback_exports_to_api_trace_with_x_api_key(
assert type(logger) is OpenTelemetry and logger.callback_name == "langtrace"
exporter: Final = logger._get_span_processor().span_exporter
assert isinstance(exporter, OTLPSpanExporter)
- from opentelemetry.sdk.trace.export import SpanExportResult
-
- collector: Final = requests_mock.post(expected_endpoint, status_code=200)
- assert exporter.export([]) is SpanExportResult.SUCCESS
- assert collector.call_count == 1
- assert collector.last_request.headers["x-api-key"] == api_key
+ assert exporter._endpoint == expected_endpoint
+ assert exporter._headers == {"x-api-key": api_key}
assert "OTEL_EXPORTER_OTLP_TRACES_HEADERS" not in os.environ
finally:
logging_module._in_memory_loggers.clear()
@@ -8023,14 +8019,14 @@ def test_payload_without_guardrail_cost_is_unchanged(logging_obj):
assert payload["cost_breakdown"] is None
-_AWS_SECRET = "TEST_ONLY_AWS_SECRET_NOT_A_CREDENTIAL"
-_GEMINI_KEY = "TEST_ONLY_GEMINI_KEY_NOT_A_CREDENTIAL"
+_AWS_SECRET = "wJalrXUtnFEMIK7MDENGbPxRfiCYEXAMPLEKEY"
+_GEMINI_KEY = "AIzaSyC0000000000000000000000000000000"
def test_empty_api_base_does_not_dump_call_state(logging_obj):
"""Direct (non-HTTP) providers pass api_base='', which used to echo model_call_details."""
logging_obj.model_call_details["litellm_params"] = {
- "api_key": "sk-TEST_ONLY_API_KEY_NOT_A_CREDENTIAL",
+ "api_key": "sk-proj-hunter2hunter2hunter2hunter2",
"aws_secret_access_key": _AWS_SECRET,
}
@@ -8043,7 +8039,7 @@ def test_empty_api_base_does_not_dump_call_state(logging_obj):
assert "litellm_call_id" not in curl_command
assert _AWS_SECRET not in curl_command
- assert "sk-TEST_ONLY_API_KEY_NOT_A_CREDENTIAL" not in curl_command
+ assert "hunter2" not in curl_command
def test_pre_call_redacts_and_masks_raw_request(logging_obj):
diff --git a/tests/unit/proxy/enterprise_billing/test_billing_metrics.py b/tests/unit/proxy/enterprise_billing/test_billing_metrics.py
index 06f4e09697c..0446cfeeab0 100644
--- a/tests/unit/proxy/enterprise_billing/test_billing_metrics.py
+++ b/tests/unit/proxy/enterprise_billing/test_billing_metrics.py
@@ -12,7 +12,7 @@ import os
import socket
import stat
from pathlib import Path
-from typing import Dict, Final, List, Optional
+from typing import Dict, List, Optional
import pytest
from opentelemetry.sdk.metrics import MeterProvider
@@ -350,30 +350,19 @@ def _fake_exporter_class(captured: Dict[str, object]) -> type:
return _FakeExporter
-@pytest.mark.parametrize("requests_ca", ("REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE"))
-def test_meter_provider_preserves_explicit_tls_settings(tmp_path, monkeypatch, requests_mock, requests_ca: str) -> None:
- monkeypatch.delenv("REQUESTS_CA_BUNDLE", raising=False)
- monkeypatch.delenv("CURL_CA_BUNDLE", raising=False)
- monkeypatch.setenv(requests_ca, "/corporate/global-ca.pem")
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE", "/operator/collector-ca.pem")
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE", "/operator/client.pem")
- monkeypatch.setenv("OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY", "/operator/client.key")
- monkeypatch.setenv("HTTPS_PROXY", "http://proxy.example.invalid:8080")
- captured: Final = requests_mock.post("https://collector.example:4317/v1/metrics", content=b"")
- config: Final = _config(tmp_path)
- provider: Final = bm.build_mtls_meter_provider(config)
- try:
- bm.BillingMetricsRecorder(provider).record(
- category=BillableCategory.LLM, route="/chat/completions", status_code=200, model_id=None
- )
- assert provider.force_flush()
- assert captured.request_history
- for request in captured.request_history:
- assert request.verify == config.ca_cert_path
- assert request.cert == (config.client_cert_path, config.client_key_path)
- assert request.proxies["https"] == "http://proxy.example.invalid:8080"
- finally:
- provider.shutdown()
+def test_meter_provider_wires_client_cert_into_http_exporter(tmp_path, monkeypatch):
+ """Client cert+key authenticate us at the collector's mTLS front end; CA override rides certificate_file."""
+ captured: Dict[str, object] = {}
+
+ monkeypatch.setattr(bm, "OTLPMetricExporter", _fake_exporter_class(captured))
+ config = _config(tmp_path)
+ provider = bm.build_mtls_meter_provider(config)
+ provider.shutdown()
+
+ assert captured["endpoint"] == "https://collector.example:4317/v1/metrics"
+ assert captured["client_certificate_file"] == config.client_cert_path
+ assert captured["client_key_file"] == config.client_key_path
+ assert captured["certificate_file"] == config.ca_cert_path
def test_load_config_without_ca_is_valid(monkeypatch, tmp_path):
diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json
index c6f45fd19f5..d6b98d9be80 100644
--- a/ui/litellm-dashboard/package-lock.json
+++ b/ui/litellm-dashboard/package-lock.json
@@ -29,7 +29,7 @@
"jwt-decode": "4.0.0",
"lucide-react": "0.513.0",
"moment": "2.31.0",
- "next": "16.4.1",
+ "next": "16.3.8",
"next-themes": "^0.4.6",
"nuqs": "^2.9.4",
"openai": "6.49.0",
@@ -71,8 +71,8 @@
"@types/react-copy-to-clipboard": "5.0.7",
"@types/react-dom": "19.2.4",
"@types/react-syntax-highlighter": "15.5.13",
- "@vitest/coverage-v8": "4.1.12",
- "@vitest/ui": "4.1.12",
+ "@vitest/coverage-v8": "4.1.11",
+ "@vitest/ui": "4.1.11",
"eslint": "9.39.2",
"eslint-config-next": "16.3.3",
"eslint-config-prettier": "10.1.8",
@@ -89,7 +89,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.60.1",
"vite": "7.3.7",
- "vitest": "4.1.12"
+ "vitest": "4.1.11"
},
"engines": {
"node": ">=24.14.1",
@@ -2155,9 +2155,9 @@
}
},
"node_modules/@next/env": {
- "version": "16.4.1",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.1.tgz",
- "integrity": "sha512-5sUUrrSvs/2/2LnLoesybMGx12EtPB46fdZ6RhAw0XDGSPxU2LFyQBB7yuEiOFFJPKmBY6+q2jTr1/AMbnp0/g==",
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.8.tgz",
+ "integrity": "sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -2172,9 +2172,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "16.4.1",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.1.tgz",
- "integrity": "sha512-U+hQ5Gj3fpzf4fveTIF3dpUFYhaSW8vHhdAnbiIomzXzg7boreYFgBqprepXFtW4sw+aTttiNmg0gew/iFNhbg==",
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.8.tgz",
+ "integrity": "sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==",
"cpu": [
"arm64"
],
@@ -2188,9 +2188,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "16.4.1",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.1.tgz",
- "integrity": "sha512-/hqC3V1XLUjhMASf/WridKdO9FXoEgN3HBWmtKbj00MPgnltutfraWJ3bYTpCRIgEIpbjJyOVLpYgAhh/BkdcQ==",
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.8.tgz",
+ "integrity": "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==",
"cpu": [
"x64"
],
@@ -2204,12 +2204,15 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.4.1",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.1.tgz",
- "integrity": "sha512-GHzncHbcaS9g95kcvTNtKQW3aCLoR+xc3Jm7qrkpk1R+D2BjMHIPyvQGRPKCdMX95BsH/421jBUK06q9tEwzjw==",
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.8.tgz",
+ "integrity": "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==",
"cpu": [
"arm64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2220,12 +2223,15 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.4.1",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.1.tgz",
- "integrity": "sha512-1SuJyDxW/PR9dHJat/ZRgcYLhuSZ+1psP4DlKb2tKIuXbjygqpLrJWRSq+onZQBv8LcmU67EIrmb6c3XTaJLjw==",
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.8.tgz",
+ "integrity": "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==",
"cpu": [
"arm64"
],
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2236,12 +2242,15 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.4.1",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.1.tgz",
- "integrity": "sha512-Istng1hMGp9TGO4lUqeKLd70MPN/OOkucVTP2Nmj+gLkwBgmPqUw8CJYBCC7nbl/B/23bBCcjCI5Hsh6hazC1A==",
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.8.tgz",
+ "integrity": "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==",
"cpu": [
"x64"
],
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2252,12 +2261,15 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "16.4.1",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.1.tgz",
- "integrity": "sha512-aSC9wMNiZRRFVpPkPWBzRwLQxIGm/CiQcXyFF1XNcgodb+uMxdk7aSSGGpDXCbnvZdf4itjHxpOd5h7i7zaiYw==",
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.8.tgz",
+ "integrity": "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==",
"cpu": [
"x64"
],
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -2268,9 +2280,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.4.1",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.1.tgz",
- "integrity": "sha512-t/myvwFDlypMUjRvbBf8qLPyuaB0iXYYdBtwMt6DM6f+60wtGr4bSYholdPqNeV5VUXZhrpMwfza829kmhS3iA==",
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.8.tgz",
+ "integrity": "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==",
"cpu": [
"arm64"
],
@@ -2284,9 +2296,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.4.1",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.1.tgz",
- "integrity": "sha512-6ICqPo9awCkrtYNPhwkx1uU7rlFWrZRrIboDHS2LU+jv088UBYQtmJ25h6pCp27/+FZjF6BQ4Te4ErugyMeuBg==",
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.8.tgz",
+ "integrity": "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==",
"cpu": [
"x64"
],
@@ -4480,14 +4492,14 @@
]
},
"node_modules/@vitest/coverage-v8": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.12.tgz",
- "integrity": "sha512-6S7/iVknKglLAxMFxQGNppBI0iqJS2NgM6FBxavitj7KaDq/vywOWGf6dGu7F2L1Qy0G6S3uTp7aGshHrzK8JA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz",
+ "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
- "@vitest/utils": "4.1.12",
+ "@vitest/utils": "4.1.11",
"ast-v8-to-istanbul": "^1.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
@@ -4501,8 +4513,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "@vitest/browser": "4.1.12",
- "vitest": "4.1.12"
+ "@vitest/browser": "4.1.11",
+ "vitest": "4.1.11"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -4511,16 +4523,16 @@
}
},
"node_modules/@vitest/expect": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.12.tgz",
- "integrity": "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
+ "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.12",
- "@vitest/utils": "4.1.12",
+ "@vitest/spy": "4.1.11",
+ "@vitest/utils": "4.1.11",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -4529,13 +4541,13 @@
}
},
"node_modules/@vitest/mocker": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz",
- "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
+ "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.1.12",
+ "@vitest/spy": "4.1.11",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -4556,9 +4568,9 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.12.tgz",
- "integrity": "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
+ "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4569,13 +4581,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz",
- "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
+ "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.12",
+ "@vitest/utils": "4.1.11",
"pathe": "^2.0.3"
},
"funding": {
@@ -4583,14 +4595,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.12.tgz",
- "integrity": "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
+ "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.12",
- "@vitest/utils": "4.1.12",
+ "@vitest/pretty-format": "4.1.11",
+ "@vitest/utils": "4.1.11",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -4599,9 +4611,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz",
- "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
+ "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -4609,13 +4621,13 @@
}
},
"node_modules/@vitest/ui": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.12.tgz",
- "integrity": "sha512-eYUTHtsp52Fu6YGUw9T9173bba7YUr9j/Xj9CdhUaOb8AppLKcknHUaiZQeqJ8MiJ8C2VyKiZuUGSKdvPIKQKA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.11.tgz",
+ "integrity": "sha512-r/rwyKoev21mWdRGSEkZOqkQ2BYy68mwjihg9M90nNRbf4NGrgzZ4cj6JNCEwlOGJkbKeMgsjlykvwKUbRr7gw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.12",
+ "@vitest/utils": "4.1.11",
"fflate": "^0.8.2",
"flatted": "^3.4.2",
"pathe": "^2.0.3",
@@ -4627,17 +4639,17 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "vitest": "4.1.12"
+ "vitest": "4.1.11"
}
},
"node_modules/@vitest/utils": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.12.tgz",
- "integrity": "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
+ "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.12",
+ "@vitest/pretty-format": "4.1.11",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -9737,12 +9749,12 @@
"license": "MIT"
},
"node_modules/next": {
- "version": "16.4.1",
- "resolved": "https://registry.npmjs.org/next/-/next-16.4.1.tgz",
- "integrity": "sha512-uc657V62E34fOI6W5NtG9IzwsPcms0Z60grz2MJGtglgD+pTFnNt4Yw2PzaU622tfX4vhrDgLZzicE2YlhI20Q==",
+ "version": "16.3.8",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.3.8.tgz",
+ "integrity": "sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==",
"license": "MIT",
"dependencies": {
- "@next/env": "16.4.1",
+ "@next/env": "16.3.8",
"@swc/helpers": "0.5.23",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
@@ -9756,15 +9768,15 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "16.4.1",
- "@next/swc-darwin-x64": "16.4.1",
- "@next/swc-linux-arm64-gnu": "16.4.1",
- "@next/swc-linux-arm64-musl": "16.4.1",
- "@next/swc-linux-x64-gnu": "16.4.1",
- "@next/swc-linux-x64-musl": "16.4.1",
- "@next/swc-win32-arm64-msvc": "16.4.1",
- "@next/swc-win32-x64-msvc": "16.4.1",
- "sharp": "^0.35.5"
+ "@next/swc-darwin-arm64": "16.3.8",
+ "@next/swc-darwin-x64": "16.3.8",
+ "@next/swc-linux-arm64-gnu": "16.3.8",
+ "@next/swc-linux-arm64-musl": "16.3.8",
+ "@next/swc-linux-x64-gnu": "16.3.8",
+ "@next/swc-linux-x64-musl": "16.3.8",
+ "@next/swc-win32-arm64-msvc": "16.3.8",
+ "@next/swc-win32-x64-msvc": "16.3.8",
+ "sharp": "^0.35.4"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@@ -12566,19 +12578,19 @@
}
},
"node_modules/vitest": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz",
- "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
+ "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/expect": "4.1.12",
- "@vitest/mocker": "4.1.12",
- "@vitest/pretty-format": "4.1.12",
- "@vitest/runner": "4.1.12",
- "@vitest/snapshot": "4.1.12",
- "@vitest/spy": "4.1.12",
- "@vitest/utils": "4.1.12",
+ "@vitest/expect": "4.1.11",
+ "@vitest/mocker": "4.1.11",
+ "@vitest/pretty-format": "4.1.11",
+ "@vitest/runner": "4.1.11",
+ "@vitest/snapshot": "4.1.11",
+ "@vitest/spy": "4.1.11",
+ "@vitest/utils": "4.1.11",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -12606,12 +12618,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.12",
- "@vitest/browser-preview": "4.1.12",
- "@vitest/browser-webdriverio": "^4.1.11",
- "@vitest/coverage-istanbul": "4.1.12",
- "@vitest/coverage-v8": "4.1.12",
- "@vitest/ui": "4.1.12",
+ "@vitest/browser-playwright": "4.1.11",
+ "@vitest/browser-preview": "4.1.11",
+ "@vitest/browser-webdriverio": "4.1.11",
+ "@vitest/coverage-istanbul": "4.1.11",
+ "@vitest/coverage-v8": "4.1.11",
+ "@vitest/ui": "4.1.11",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json
index 432e327ec5a..8d1b7752b25 100644
--- a/ui/litellm-dashboard/package.json
+++ b/ui/litellm-dashboard/package.json
@@ -46,7 +46,7 @@
"jwt-decode": "4.0.0",
"lucide-react": "0.513.0",
"moment": "2.31.0",
- "next": "16.4.1",
+ "next": "16.3.8",
"next-themes": "^0.4.6",
"nuqs": "^2.9.4",
"openai": "6.49.0",
@@ -88,8 +88,8 @@
"@types/react-copy-to-clipboard": "5.0.7",
"@types/react-dom": "19.2.4",
"@types/react-syntax-highlighter": "15.5.13",
- "@vitest/coverage-v8": "4.1.12",
- "@vitest/ui": "4.1.12",
+ "@vitest/coverage-v8": "4.1.11",
+ "@vitest/ui": "4.1.11",
"eslint": "9.39.2",
"eslint-config-next": "16.3.3",
"eslint-config-prettier": "10.1.8",
@@ -106,7 +106,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.60.1",
"vite": "7.3.7",
- "vitest": "4.1.12"
+ "vitest": "4.1.11"
},
"overrides": {
"prismjs": "1.30.0",
@@ -119,10 +119,7 @@
"axios": "1.13.6",
"postcss": "8.5.23",
"esbuild": "0.28.1",
- "sharp": "^0.35.5",
- "@litellm/lens-ui": {
- "next": "$next"
- }
+ "sharp": "^0.35.5"
},
"engines": {
"node": ">=24.14.1",
diff --git a/uv.lock b/uv.lock
index 17247935ebc..8d2a38c1806 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2482,14 +2482,14 @@ wheels = [
[[package]]
name = "gitpython"
-version = "3.2.1"
+version = "3.1.62"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gitdb" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c0/ff/fee293f1ec3e1b64db6eb270e00964f29c086d769201474cedb8d1608930/gitpython-3.2.1.tar.gz", hash = "sha256:0f1229ec5c057bdabdeeda8bda4f17bda679ab33dbccc00f24e6a480553c8613", size = 241688, upload-time = "2026-10-14T12:20:20.589Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/db/3ca813cbacb23ab6fe46ff38a9b5ef8e73e970c8051f2ce903aacafe0446/gitpython-3.1.62.tar.gz", hash = "sha256:1791de66309bc0c7cfca40bf8d2e3de7ca091cbf94e6051be1ad0722c61062af", size = 231728, upload-time = "2026-09-07T02:57:21.155Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6b/50/2a923c4dc02522ff9567f07c67ed1deca6d5fa9474f4708168b8088dfa37/gitpython-3.2.1-py3-none-any.whl", hash = "sha256:d66b0832aa8755c1a84892937282df0befb16c97e5a64aeebd68be9c5e323f53", size = 231548, upload-time = "2026-10-14T12:20:18.844Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/0b/29d7965215f8ef830a7ca1f42997fe13e5693d85e9edb18f938d063ef5f2/gitpython-3.1.62-py3-none-any.whl", hash = "sha256:7002251225e10e29d2e1f49e6532613fe5d5d9f0b6f1f02997a52b38fe56899e", size = 222753, upload-time = "2026-09-07T02:57:19.762Z" },
]
[[package]]
@@ -2987,11 +2987,11 @@ wheels = [
[[package]]
name = "graphql-core"
-version = "3.2.13"
+version = "3.2.8"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4e/5e/aa0d4e701b50db0bab71b125dd19ddb0f98c638d008d6f7e3d8ce9cbc92e/graphql_core-3.2.13.tar.gz", hash = "sha256:bb81dd266d4ab7b591bd976f1b23639d97776cb9ac1a896b4a93c271e11ed618", size = 577471, upload-time = "2026-09-27T12:22:57.136Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/68/c5/36aa96205c3ecbb3d34c7c24189e4553c7ca2ebc7e1dd07432339b980272/graphql_core-3.2.8.tar.gz", hash = "sha256:015457da5d996c924ddf57a43f4e959b0b94fb695b85ed4c29446e508ed65cf3", size = 513181, upload-time = "2026-03-05T19:55:37.332Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1f/b4/b52a324b0b297ddc49b8ea31a51d39fae63132f694a267b9381d94e2e0af/graphql_core-3.2.13-py3-none-any.whl", hash = "sha256:b0eb04f2c31556b2310a77c8fb53c74e8b56570f8fea594d89c6ef7827dbb497", size = 268191, upload-time = "2026-09-27T12:22:55.661Z" },
+ { url = "https://files.pythonhosted.org/packages/86/41/cb887d9afc5dabd78feefe6ccbaf83ff423c206a7a1b7aeeac05120b2125/graphql_core-3.2.8-py3-none-any.whl", hash = "sha256:cbee07bee1b3ed5e531723685369039f32ff815ef60166686e0162f540f1520c", size = 207349, upload-time = "2026-03-05T19:55:35.911Z" },
]
[[package]]
@@ -4727,7 +4727,6 @@ proxy-runtime = [
{ name = "mangum" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp" },
- { name = "opentelemetry-exporter-otlp-json-http" },
{ name = "opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-sdk" },
{ name = "prometheus-client" },
@@ -4811,7 +4810,6 @@ dev = [
{ name = "openapi-core" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp" },
- { name = "opentelemetry-exporter-otlp-json-http" },
{ name = "opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-sdk" },
{ name = "parameterized" },
@@ -4859,7 +4857,6 @@ proxy-dev = [
{ name = "litellm-admin-mcp", marker = "python_full_version >= '3.12'" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp" },
- { name = "opentelemetry-exporter-otlp-json-http" },
{ name = "opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-sdk" },
{ name = "prisma" },
@@ -4933,11 +4930,10 @@ requires-dist = [
{ name = "numpydoc", marker = "extra == 'utils'", specifier = ">=1.8.0,<2.0" },
{ name = "nvidia-riva-client", marker = "extra == 'stt-nvidia-riva'", specifier = ">=2.15.0" },
{ name = "openai", specifier = ">=2.20.0,<4.0.0" },
- { name = "opentelemetry-api", marker = "extra == 'proxy-runtime'", specifier = "==1.45.1" },
- { name = "opentelemetry-exporter-otlp", marker = "extra == 'proxy-runtime'", specifier = "==1.45.1" },
- { name = "opentelemetry-exporter-otlp-json-http", marker = "extra == 'proxy-runtime'", specifier = "==0.66b1" },
- { name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'proxy-runtime'", specifier = "==0.66b1" },
- { name = "opentelemetry-sdk", marker = "extra == 'proxy-runtime'", specifier = "==1.45.1" },
+ { name = "opentelemetry-api", marker = "extra == 'proxy-runtime'", specifier = "==1.33.1" },
+ { name = "opentelemetry-exporter-otlp", marker = "extra == 'proxy-runtime'", specifier = "==1.33.1" },
+ { name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'proxy-runtime'", specifier = "==0.54b1" },
+ { name = "opentelemetry-sdk", marker = "extra == 'proxy-runtime'", specifier = "==1.33.1" },
{ name = "orjson", marker = "extra == 'proxy'", specifier = ">=3.11.6,<4.0" },
{ name = "packaging", specifier = ">=24.0" },
{ name = "polars", marker = "extra == 'proxy'", specifier = ">=1.38.1,<2.0" },
@@ -4952,7 +4948,7 @@ requires-dist = [
{ name = "pydantic-settings", specifier = ">=2.14.1,<3.0" },
{ name = "pyjwt", marker = "extra == 'proxy'", specifier = ">=2.13.0,<3.0" },
{ name = "pynacl", marker = "extra == 'proxy'", specifier = ">=1.6.2,<2.0" },
- { name = "pypdf", marker = "extra == 'proxy-runtime'", specifier = ">=6.21.1,<7.0" },
+ { name = "pypdf", marker = "extra == 'proxy-runtime'", specifier = ">=6.16.1,<7.0" },
{ name = "pyroscope-io", marker = "sys_platform != 'win32' and extra == 'proxy'", specifier = ">=0.8.16,<1.0" },
{ name = "python-dotenv", specifier = ">=1.0.0,<2.0" },
{ name = "python-multipart", marker = "extra == 'proxy'", specifier = ">=0.0.27,<1.0" },
@@ -5008,7 +5004,7 @@ ci = [
{ name = "langchain-openai", specifier = "==1.1.14" },
{ name = "langgraph", specifier = ">=1.2.4,<1.3.0" },
{ name = "langgraph-prebuilt", specifier = ">=1.1.0,<1.3.0" },
- { name = "logfire", specifier = "==5.2.0" },
+ { name = "logfire", specifier = "==4.6.0" },
{ name = "lunary", marker = "python_full_version == '3.10.*'", specifier = "==1.4.36" },
{ name = "lunary", marker = "python_full_version >= '3.11'", specifier = "==1.4.37" },
{ name = "pillow", marker = "python_full_version == '3.10.*'", specifier = "==12.3.0" },
@@ -5035,11 +5031,10 @@ dev = [
{ name = "mypy", specifier = "==1.20.1" },
{ name = "numpy", specifier = ">=1.26.0,<3.0" },
{ name = "openapi-core", specifier = "==0.22.0" },
- { name = "opentelemetry-api", specifier = "==1.45.1" },
- { name = "opentelemetry-exporter-otlp", specifier = "==1.45.1" },
- { name = "opentelemetry-exporter-otlp-json-http", specifier = "==0.66b1" },
- { name = "opentelemetry-instrumentation-fastapi", specifier = "==0.66b1" },
- { name = "opentelemetry-sdk", specifier = "==1.45.1" },
+ { name = "opentelemetry-api", specifier = "==1.33.1" },
+ { name = "opentelemetry-exporter-otlp", specifier = "==1.33.1" },
+ { name = "opentelemetry-instrumentation-fastapi", specifier = "==0.54b1" },
+ { name = "opentelemetry-sdk", specifier = "==1.33.1" },
{ name = "parameterized", specifier = "==0.9.0" },
{ name = "psycopg", specifier = "==3.3.3" },
{ name = "psycopg-binary", specifier = "==3.3.3" },
@@ -5083,11 +5078,10 @@ proxy-dev = [
{ name = "backports-zstd", marker = "python_full_version < '3.14'", specifier = "==1.8.0" },
{ name = "hypercorn", specifier = "==0.17.3" },
{ name = "litellm-admin-mcp", marker = "python_full_version >= '3.12'", url = "https://github.com/BerriAI/liteadmin-mcp/archive/d35ec9c19c117d4c50cc1eccf6ce1296aac25a1a.tar.gz" },
- { name = "opentelemetry-api", specifier = "==1.45.1" },
- { name = "opentelemetry-exporter-otlp", specifier = "==1.45.1" },
- { name = "opentelemetry-exporter-otlp-json-http", specifier = "==0.66b1" },
- { name = "opentelemetry-instrumentation-fastapi", specifier = "==0.66b1" },
- { name = "opentelemetry-sdk", specifier = "==1.45.1" },
+ { name = "opentelemetry-api", specifier = "==1.33.1" },
+ { name = "opentelemetry-exporter-otlp", specifier = "==1.33.1" },
+ { name = "opentelemetry-instrumentation-fastapi", specifier = "==0.54b1" },
+ { name = "opentelemetry-sdk", specifier = "==1.33.1" },
{ name = "prisma", specifier = "==0.11.0" },
{ name = "prometheus-client", specifier = "==0.20.0" },
{ name = "sentry-sdk", specifier = "==2.21.0" },
@@ -5179,24 +5173,21 @@ wheels = [
[[package]]
name = "logfire"
-version = "5.2.0"
+version = "4.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "executing" },
- { name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-sdk" },
- { name = "opentelemetry-semantic-conventions" },
{ name = "protobuf" },
- { name = "requests" },
{ name = "rich" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/56/5a/e6cccc98d30358b8beee47c61c2a1ca66ac7a76a07de9ba333aa5219b9a6/logfire-5.2.0.tar.gz", hash = "sha256:5fb9ddda303759766e86f5704a1ae96f3a0072906d52e21fbaad2e3c3b03f823", size = 1404990, upload-time = "2026-10-13T17:36:30.746Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/fa/14cbd976e8ff77290c0c87f93c45029785537ffb84e2f5215416786cda11/logfire-4.6.0.tar.gz", hash = "sha256:974bc8f4efd3aa9df2d0c7b1d53b35dc4612ff5cee2be6c40dd65e5c26eab694", size = 533174, upload-time = "2025-09-10T14:56:42.168Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/73/cb/bfcd484c16eeac8cdeb2defe1a2ee39bb1b4b6e69324b85515e7818eb678/logfire-5.2.0-py3-none-any.whl", hash = "sha256:c73dd02cfe048f2254e803962aa992d7f53217d121f3488e797926c30442342c", size = 495128, upload-time = "2026-10-13T17:36:27.305Z" },
+ { url = "https://files.pythonhosted.org/packages/04/9c/6384dc69601bfba19c1f5909a472b00cc4b5863f411ecf85be308beacf8f/logfire-4.6.0-py3-none-any.whl", hash = "sha256:d322844db7a2f1935976f2852a07e3753489ecc4e83fa288173b05a453bb1cbf", size = 219556, upload-time = "2025-09-10T14:56:37.278Z" },
]
[[package]]
@@ -6315,144 +6306,81 @@ wheels = [
[[package]]
name = "opentelemetry-api"
-version = "1.45.1"
+version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions" },
+ { name = "deprecated" },
+ { name = "importlib-metadata" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8d/1f5a45fbcb9a7d87809d460f09dc3399e3fbd31d7f3e14888345e9d29951/opentelemetry_api-1.33.1.tar.gz", hash = "sha256:1c6055fc0a2d3f23a50c7e17e16ef75ad489345fd3df1f8b8af7c0bbf8a109e8", size = 65002, upload-time = "2025-05-16T18:52:41.146Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" },
-]
-
-[[package]]
-name = "opentelemetry-exporter-http-transport"
-version = "0.66b1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/62/0c/e3ebdb4b507f66afcc905e6885a4946969bd75b45988492643356fbbdc63/opentelemetry_exporter_http_transport-0.66b1.tar.gz", hash = "sha256:443080203bf52586ce0b2ad901e8951c61833eab1aa539ae6f1f16fe9e8e7952", size = 11693, upload-time = "2026-10-06T17:32:59.65Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/69/6af86ff66492b481c6a4c05dcfd68beb47ed8ba046440a26a2aac76b95c7/opentelemetry_exporter_http_transport-0.66b1-py3-none-any.whl", hash = "sha256:2f95404bdee7f9d2d529c7de56c7bd86d014d774d8fbf137810e0167f8a492bf", size = 12155, upload-time = "2026-10-06T17:32:35.454Z" },
-]
-
-[package.optional-dependencies]
-requests = [
- { name = "requests" },
-]
-urllib3 = [
- { name = "urllib3" },
+ { url = "https://files.pythonhosted.org/packages/05/44/4c45a34def3506122ae61ad684139f0bbc4e00c39555d4f7e20e0e001c8a/opentelemetry_api-1.33.1-py3-none-any.whl", hash = "sha256:4db83ebcf7ea93e64637ec6ee6fabee45c5cbe4abd9cf3da95c43828ddb50b83", size = 65771, upload-time = "2025-05-16T18:52:17.419Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp"
-version = "1.45.1"
+version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-exporter-otlp-proto-grpc" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e3/6f/5a561048ea372894f22f58f70e8478e4cd97d9a4e2d6f014559584a29bc5/opentelemetry_exporter_otlp-1.45.1.tar.gz", hash = "sha256:d0ac35592e77663a9fabf2740b4818c57196b1b764e0c8449d0d7bb2c7b2bc67", size = 6094, upload-time = "2026-10-06T17:33:00.966Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b1/3f/c8ad4f1c3aaadcea2b0f1b4d7970e7b7898c145699769a789f3435143f69/opentelemetry_exporter_otlp-1.33.1.tar.gz", hash = "sha256:4d050311ea9486e3994575aa237e32932aad58330a31fba24fdba5c0d531cf04", size = 6189, upload-time = "2025-05-16T18:52:43.176Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/36/e1/68c28d7da4482ce882a640b2c61405a78284c0a3a17a4db125647189b0b7/opentelemetry_exporter_otlp-1.45.1-py3-none-any.whl", hash = "sha256:ef3910d32b36ccbaf62390189759bd43a8109885e2d50b738ed9c9b255bf5cb5", size = 6730, upload-time = "2026-10-06T17:32:37.236Z" },
-]
-
-[[package]]
-name = "opentelemetry-exporter-otlp-common"
-version = "0.66b1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-sdk" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/cb/19/41de712173f43057e4532d42ece7d0c6d4210d353e5752433cb14987643f/opentelemetry_exporter_otlp_common-0.66b1.tar.gz", hash = "sha256:6b1403487a2185ac1feb45fd5546fdf8630ce71c36bcefaadf51e2130e9e23f9", size = 14325, upload-time = "2026-10-06T17:33:01.725Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fc/39/8c23d67665c762aa51840fa06f86e902e8f6f1693bc8d7e3d98cd6e2f753/opentelemetry_exporter_otlp_common-0.66b1-py3-none-any.whl", hash = "sha256:00ff8592c3a7cb729ff3fdc7ffa12372c243bdf2163e80c180994d0c7bd83ee9", size = 12385, upload-time = "2026-10-06T17:32:38.177Z" },
-]
-
-[[package]]
-name = "opentelemetry-exporter-otlp-json-common"
-version = "0.66b1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-proto-json" },
- { name = "opentelemetry-sdk" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ca/27/fd51023a4916ad00ef0c970577e148fba36e3682716d15a1495f74f830fb/opentelemetry_exporter_otlp_json_common-0.66b1.tar.gz", hash = "sha256:d8bd656ee03be5d2f54b533e03e297792c5017501417d132c3c67eb2ac95b3de", size = 24519, upload-time = "2026-10-06T17:33:02.52Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0f/34/6e7a4794ff9995042c8372af5c7e4e5127aea10dfeec64402fbec93a6d43/opentelemetry_exporter_otlp_json_common-0.66b1-py3-none-any.whl", hash = "sha256:1ae23eb3dfcd756607367c1ff5df2b4120fe481a523e7868b26b61720c558875", size = 14826, upload-time = "2026-10-06T17:32:39.052Z" },
-]
-
-[[package]]
-name = "opentelemetry-exporter-otlp-json-http"
-version = "0.66b1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-exporter-http-transport", extra = ["urllib3"] },
- { name = "opentelemetry-exporter-otlp-common" },
- { name = "opentelemetry-exporter-otlp-json-common" },
- { name = "opentelemetry-proto-json" },
- { name = "opentelemetry-sdk" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/44/30/47c00f9329242b71724df87536613cb21596b41f294cdc37d8b216c5afaa/opentelemetry_exporter_otlp_json_http-0.66b1.tar.gz", hash = "sha256:940ca96fff3509ca06b3ce8733d4818ee022620aa9329cb7025c63e43c2d224d", size = 17583, upload-time = "2026-10-06T17:33:03.868Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e2/2d/469689a34b17ed83d4ddee480db8322246dcffeb95ef5fc5050bb8a16c8c/opentelemetry_exporter_otlp_json_http-0.66b1-py3-none-any.whl", hash = "sha256:4bd7607120fb7cc77aae88d5c7f629c40515d225b3ca5db293d20d091a248156", size = 14523, upload-time = "2026-10-06T17:32:40.837Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/32/b9add70dd4e845654fc9fcd1401a705477743880be6c3e62acb1ad0d8662/opentelemetry_exporter_otlp-1.33.1-py3-none-any.whl", hash = "sha256:9bcf1def35b880b55a49e31ebd63910edac14b294fd2ab884953c4deaff5b300", size = 7045, upload-time = "2025-05-16T18:52:21.022Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
-version = "1.45.1"
+version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c1/8e/65e85e5137991a3c493b11682151d198638a5bc1dd4b4c5f67e013c57d7c/opentelemetry_exporter_otlp_proto_common-1.45.1.tar.gz", hash = "sha256:2e4adcc3a67bcf57804fc49514f0ef64974ca7590aa3491da389852b4a0628f6", size = 18873, upload-time = "2026-10-06T17:33:04.471Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7a/18/a1ec9dcb6713a48b4bdd10f1c1e4d5d2489d3912b80d2bcc059a9a842836/opentelemetry_exporter_otlp_proto_common-1.33.1.tar.gz", hash = "sha256:c57b3fa2d0595a21c4ed586f74f948d259d9949b58258f11edb398f246bec131", size = 20828, upload-time = "2025-05-16T18:52:43.795Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/84/aa/92f225d353904e7f70b8b3e3c1b02db0cf56f744c2e83c581dc372e78873/opentelemetry_exporter_otlp_proto_common-1.45.1-py3-none-any.whl", hash = "sha256:2f446183ae7047b036226f1d846c41a834b0e8755ad13b51a51dd38952eb466c", size = 15393, upload-time = "2026-10-06T17:32:41.911Z" },
+ { url = "https://files.pythonhosted.org/packages/09/52/9bcb17e2c29c1194a28e521b9d3f2ced09028934c3c52a8205884c94b2df/opentelemetry_exporter_otlp_proto_common-1.33.1-py3-none-any.whl", hash = "sha256:b81c1de1ad349785e601d02715b2d29d6818aed2c809c20219f3d1f20b038c36", size = 18839, upload-time = "2025-05-16T18:52:22.447Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-grpc"
-version = "1.45.1"
+version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "deprecated" },
{ name = "googleapis-common-protos" },
{ name = "grpcio" },
{ name = "opentelemetry-api" },
- { name = "opentelemetry-exporter-otlp-common" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
- { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d6/00/a82af0be959dc58495740b169c6669a86e0811f6cd353a01eda34d255db3/opentelemetry_exporter_otlp_proto_grpc-1.45.1.tar.gz", hash = "sha256:3b3dcfbfdcb4e35149fcf309972282054b45228f5c10547d0095d6578510a9a0", size = 25997, upload-time = "2026-10-06T17:33:05.114Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/5f/75ef5a2a917bd0e6e7b83d3fb04c99236ee958f6352ba3019ea9109ae1a6/opentelemetry_exporter_otlp_proto_grpc-1.33.1.tar.gz", hash = "sha256:345696af8dc19785fac268c8063f3dc3d5e274c774b308c634f39d9c21955728", size = 22556, upload-time = "2025-05-16T18:52:44.76Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4d/46/2d1da202f1e17c81aae7efcf702898d524b46709e4d3e2bf1f7f8ca8fbc6/opentelemetry_exporter_otlp_proto_grpc-1.45.1-py3-none-any.whl", hash = "sha256:e42ecb789d2fc5d8145e3dadc3e2991c9f18cd166d7c7514e234702540274b76", size = 19492, upload-time = "2026-10-06T17:32:42.838Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/ec/6047e230bb6d092c304511315b13893b1c9d9260044dd1228c9d48b6ae0e/opentelemetry_exporter_otlp_proto_grpc-1.33.1-py3-none-any.whl", hash = "sha256:7e8da32c7552b756e75b4f9e9c768a61eb47dee60b6550b37af541858d669ce1", size = 18591, upload-time = "2025-05-16T18:52:23.772Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
-version = "1.45.1"
+version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "deprecated" },
{ name = "googleapis-common-protos" },
{ name = "opentelemetry-api" },
- { name = "opentelemetry-exporter-http-transport", extra = ["requests"] },
- { name = "opentelemetry-exporter-otlp-common" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "requests" },
- { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/1b/17/26487707ea4caa97b17e6e4b5fa72133a53512ffa2f5cf7a49ef284b29cb/opentelemetry_exporter_otlp_proto_http-1.45.1.tar.gz", hash = "sha256:45c218405ce3fd879596924b1874bf9a8f6880206d61065c5a912c8e5c297fb7", size = 28839, upload-time = "2026-10-06T17:33:05.713Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/60/48/e4314ac0ed2ad043c07693d08c9c4bf5633857f5b72f2fefc64fd2b114f6/opentelemetry_exporter_otlp_proto_http-1.33.1.tar.gz", hash = "sha256:46622d964a441acb46f463ebdc26929d9dec9efb2e54ef06acdc7305e8593c38", size = 15353, upload-time = "2025-05-16T18:52:45.522Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/aa/1f/517eaa0187ba106a9da97160ce2add3a371812681dc440930b267f714e42/opentelemetry_exporter_otlp_proto_http-1.45.1-py3-none-any.whl", hash = "sha256:24a97cf3753c7fb52fad44a696e452ff371686339e2acf3309e2eda3d0230700", size = 22180, upload-time = "2026-10-06T17:32:43.946Z" },
+ { url = "https://files.pythonhosted.org/packages/63/ba/5a4ad007588016fe37f8d36bf08f325fe684494cc1e88ca8fa064a4c8f57/opentelemetry_exporter_otlp_proto_http-1.33.1-py3-none-any.whl", hash = "sha256:ebd6c523b89a2ecba0549adb92537cc2bf647b4ee61afbbd5a4c6535aa3da7cf", size = 17733, upload-time = "2025-05-16T18:52:25.137Z" },
]
[[package]]
name = "opentelemetry-instrumentation"
-version = "0.66b1"
+version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@@ -6460,9 +6388,9 @@ dependencies = [
{ name = "packaging" },
{ name = "wrapt" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a5/03/89e47ff8d52a4f83b343e6eb9ef1698ff45357216e5b6b2b21e0da5c5c7d/opentelemetry_instrumentation-0.66b1.tar.gz", hash = "sha256:e79a510f7d87c72d95e964ddb42193a0d9a75668c027d980eab032ea1322a5ce", size = 43308, upload-time = "2026-10-06T17:36:10.703Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/fd/5756aea3fdc5651b572d8aef7d94d22a0a36e49c8b12fcb78cb905ba8896/opentelemetry_instrumentation-0.54b1.tar.gz", hash = "sha256:7658bf2ff914b02f246ec14779b66671508125c0e4227361e56b5ebf6cef0aec", size = 28436, upload-time = "2025-05-16T19:03:22.223Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/da/b2/d1413681ff43e13ac9860df27e1226d3199ab0b97b352ceea41abcc660a5/opentelemetry_instrumentation-0.66b1-py3-none-any.whl", hash = "sha256:4c4aa14dc9a24a02325a9d4c42c4d0208dbb1374c2b1b8fe6c9392d59f3e1008", size = 36904, upload-time = "2026-10-06T17:35:11.663Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/89/0790abc5d9c4fc74bd3e03cb87afe2c820b1d1a112a723c1163ef32453ee/opentelemetry_instrumentation-0.54b1-py3-none-any.whl", hash = "sha256:a4ae45f4a90c78d7006c51524f57cd5aa1231aef031eae905ee34d5423f5b198", size = 31019, upload-time = "2025-05-16T19:02:15.611Z" },
]
[[package]]
@@ -6497,7 +6425,7 @@ wheels = [
[[package]]
name = "opentelemetry-instrumentation-asgi"
-version = "0.66b1"
+version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
@@ -6506,9 +6434,9 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5a/d9/ff522f5c3e340e9007554923b1a4d2ac451676f8757bafb3d0057f68b5c3/opentelemetry_instrumentation_asgi-0.66b1.tar.gz", hash = "sha256:78cdc5e45e897e16a8dac9d282e8d5bdf9af2d58e1313fa0bdd4a134c6f9dafc", size = 25992, upload-time = "2026-10-06T17:36:14.593Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/f7/a3377f9771947f4d3d59c96841d3909274f446c030dbe8e4af871695ddee/opentelemetry_instrumentation_asgi-0.54b1.tar.gz", hash = "sha256:ab4df9776b5f6d56a78413c2e8bbe44c90694c67c844a1297865dc1bd926ed3c", size = 24230, upload-time = "2025-05-16T19:03:30.234Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/67/ea/10ba99110bf3c9fb736af39c96ca8f3668b988cabb6b59309e058c44461c/opentelemetry_instrumentation_asgi-0.66b1-py3-none-any.whl", hash = "sha256:78b3f9bdf0fa38c65935a2ab46d59e0f9de873a51e0c95b0329f106e2ccb5274", size = 15812, upload-time = "2026-10-06T17:35:17.638Z" },
+ { url = "https://files.pythonhosted.org/packages/20/24/7a6f0ae79cae49927f528ecee2db55a5bddd87b550e310ce03451eae7491/opentelemetry_instrumentation_asgi-0.54b1-py3-none-any.whl", hash = "sha256:84674e822b89af563b283a5283c2ebb9ed585d1b80a1c27fb3ac20b562e9f9fc", size = 16338, upload-time = "2025-05-16T19:02:22.808Z" },
]
[[package]]
@@ -6559,7 +6487,7 @@ wheels = [
[[package]]
name = "opentelemetry-instrumentation-fastapi"
-version = "0.66b1"
+version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@@ -6568,9 +6496,9 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5d/2a/cd4125b7acbea2ed17f1d31b58c184cb0a79fcb5541ceb4de90ffc6d8c01/opentelemetry_instrumentation_fastapi-0.66b1.tar.gz", hash = "sha256:584cf9d2c4417ff8b2d6ff2bc606bfe13c8b3456018bf94f50f2cf658492505b", size = 26634, upload-time = "2026-10-06T17:36:25.157Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/3b/9a262cdc1a4defef0e52afebdde3e8add658cc6f922e39e9dcee0da98349/opentelemetry_instrumentation_fastapi-0.54b1.tar.gz", hash = "sha256:1fcad19cef0db7092339b571a59e6f3045c9b58b7fd4670183f7addc459d78df", size = 19325, upload-time = "2025-05-16T19:03:45.359Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/75/70/676928d537978acc7bff2ac8657bd0836ba608ffc8f65455238f1fa2bd0f/opentelemetry_instrumentation_fastapi-0.66b1-py3-none-any.whl", hash = "sha256:97f8ac8fd7537517f9e6988bd0aca04bfa5aad564bcd46c245530739e2be72d1", size = 14216, upload-time = "2026-10-06T17:35:32.827Z" },
+ { url = "https://files.pythonhosted.org/packages/df/9c/6b2b0f9d6c5dea7528ae0bf4e461dd765b0ae35f13919cd452970bb0d0b3/opentelemetry_instrumentation_fastapi-0.54b1-py3-none-any.whl", hash = "sha256:fb247781cfa75fd09d3d8713c65e4a02bd1e869b00e2c322cc516d4b5429860c", size = 12125, upload-time = "2025-05-16T19:02:41.172Z" },
]
[[package]]
@@ -6666,16 +6594,15 @@ wheels = [
[[package]]
name = "opentelemetry-instrumentation-logging"
-version = "0.66b1"
+version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
- { name = "opentelemetry-semantic-conventions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/28/ca/0fcf525f238f4ab9df3a6d0423fb057085ea39a3e79ac912fef3d5fa16b8/opentelemetry_instrumentation_logging-0.66b1.tar.gz", hash = "sha256:2250240ecb1fce36939f64eafd988832338684564fb4c7b401c9b9af0ee11d46", size = 20353, upload-time = "2026-10-06T17:36:29.546Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d9/5b/88ed39f22e8c6eb4f6192ab9a62adaa115579fcbcadb3f0241ee645eea56/opentelemetry_instrumentation_logging-0.54b1.tar.gz", hash = "sha256:893a3cbfda893b64ff71b81991894e2fd6a9267ba85bb6c251f51c0419fbe8fa", size = 9976, upload-time = "2025-05-16T19:03:49.976Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2e/7b/76bea1e80787848a9ac9882a598cbf1e761f16cebdb399d0e675221e34a0/opentelemetry_instrumentation_logging-0.66b1-py3-none-any.whl", hash = "sha256:f13761110c52eecae9d7debedb0f88b46acc248c20437e97c2a5b3a2b4f96a02", size = 15928, upload-time = "2026-10-06T17:35:39.915Z" },
+ { url = "https://files.pythonhosted.org/packages/96/0c/b441fb30d860f25040eaed61e89d68f4d9ee31873159ed18cbc1b92eba56/opentelemetry_instrumentation_logging-0.54b1-py3-none-any.whl", hash = "sha256:01a4cec54348f13941707d857b850b0febf9d49f45d0fcf0673866e079d7357b", size = 12579, upload-time = "2025-05-16T19:02:49.039Z" },
]
[[package]]
@@ -6801,7 +6728,7 @@ wheels = [
[[package]]
name = "opentelemetry-instrumentation-requests"
-version = "0.66b1"
+version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@@ -6809,9 +6736,9 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/08/62/dcca0b7a2008675056040c61b70d37b5a24439613c7822068908ad3255ae/opentelemetry_instrumentation_requests-0.66b1.tar.gz", hash = "sha256:28578f72e68e3a5be3226c618ac9570ed360ef1dd22d5d6e0721c6905a4ccc67", size = 17985, upload-time = "2026-10-06T17:36:37.478Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/45/116da84930d3dc2f5cdd876283ca96e9b96547bccee7eaa0bd01ce6bf046/opentelemetry_instrumentation_requests-0.54b1.tar.gz", hash = "sha256:3eca5d697c5564af04c6a1dd23b6a3ffbaf11e64887c6051655cee03998f4654", size = 15148, upload-time = "2025-05-16T19:04:00.488Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/88/01/98e9bb7fef12784cd19811e454cf25bce4d774fd3e46b3f5fd2bce237075/opentelemetry_instrumentation_requests-0.66b1-py3-none-any.whl", hash = "sha256:7ba17d984a2bd876b88bf0aafcca27b5e3ff4c1821e0fe07641e380a3cf3a3a2", size = 13320, upload-time = "2026-10-06T17:35:52.477Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/b1/6e33d2c3d3cc9e3ae20a9a77625ec81a509a0e5d7fa87e09e7f879468990/opentelemetry_instrumentation_requests-0.54b1-py3-none-any.whl", hash = "sha256:a0c4cd5d946224f336d6bd73cdabdecc6f80d5c39208f84eb96eb15f16cd41a0", size = 12968, upload-time = "2025-05-16T19:03:03.131Z" },
]
[[package]]
@@ -6831,7 +6758,7 @@ wheels = [
[[package]]
name = "opentelemetry-instrumentation-sqlalchemy"
-version = "0.66b1"
+version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@@ -6840,23 +6767,23 @@ dependencies = [
{ name = "packaging" },
{ name = "wrapt" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d4/3e/d69fb08dacc4c248daedf7357732ddca7a0aaff7072a55e311d3da3ea51c/opentelemetry_instrumentation_sqlalchemy-0.66b1.tar.gz", hash = "sha256:a10043953fcba71911bf29a024f8cc337260c1ef0b4fc844b96cae0de0947baa", size = 18181, upload-time = "2026-10-06T17:36:38.111Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ac/33/78a25ae4233d42058bb0b363ba4fea7d7210e53c24e5e31f16d5cf6cf957/opentelemetry_instrumentation_sqlalchemy-0.54b1.tar.gz", hash = "sha256:97839acf1c9b96ded857fca57a09b86a56cf8d9eb6d706b7ceaee9352a460e03", size = 14620, upload-time = "2025-05-16T19:04:01.215Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/08/05/f8cff0c68a7f9ab8fab01904be5a49c5214435109c2a6b6d173a974c10d9/opentelemetry_instrumentation_sqlalchemy-0.66b1-py3-none-any.whl", hash = "sha256:aa30b10d880d7e91cf94b23a92ac85cec09ffddd8f0d40256d7c510e3dd33971", size = 14425, upload-time = "2026-10-06T17:35:53.436Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/2b/1c954885815614ef5c1e8c7bbf57a5275e64cd6fb5946b65e17162a34037/opentelemetry_instrumentation_sqlalchemy-0.54b1-py3-none-any.whl", hash = "sha256:d2ca5edb4c7ecef120d51aad6793b7da1cc80207ccfd31c437ee18f098e7c4c4", size = 14169, upload-time = "2025-05-16T19:03:04.119Z" },
]
[[package]]
name = "opentelemetry-instrumentation-threading"
-version = "0.66b1"
+version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "wrapt" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/55/4f/5fa05266cf24cae93fc32d557d0d2d2f605f81cca1df90614231bbad3cd1/opentelemetry_instrumentation_threading-0.66b1.tar.gz", hash = "sha256:2c217359439d09ca4a4be524db368c05a3cc7f218b85e8086f22664db0a03538", size = 9031, upload-time = "2026-10-06T17:36:41.147Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a0/bd/561245292e7cc78ac7a0a75537873aea87440cb9493d41371421b3308c2b/opentelemetry_instrumentation_threading-0.54b1.tar.gz", hash = "sha256:3a081085b59675baf7bd93126a681903e6304a5f283df5eaecdd44bcb66df578", size = 8774, upload-time = "2025-05-16T19:04:04.482Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/86/da/1cd52b8be7a521958ccdb9a1c4e5ae743aa2be295ba21ec0b620dc87842a/opentelemetry_instrumentation_threading-0.66b1-py3-none-any.whl", hash = "sha256:94039c60603060d26524e31aed1181a5d153e43f58462d110dbeb4830687d7d7", size = 8493, upload-time = "2026-10-06T17:35:57.958Z" },
+ { url = "https://files.pythonhosted.org/packages/81/10/d87ec07d69546adaad525ba5d40d27324a45cba29097d9854a53d9af5047/opentelemetry_instrumentation_threading-0.54b1-py3-none-any.whl", hash = "sha256:bc229e6cd3f2b29fafe0a8dd3141f452e16fcb4906bca4fbf52609f99fb1eb42", size = 9314, upload-time = "2025-05-16T19:03:09.527Z" },
]
[[package]]
@@ -6891,7 +6818,7 @@ wheels = [
[[package]]
name = "opentelemetry-instrumentation-urllib3"
-version = "0.66b1"
+version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@@ -6900,9 +6827,9 @@ dependencies = [
{ name = "opentelemetry-util-http" },
{ name = "wrapt" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/08/ab/2dd6b3a69df31ae12714bf24fcc2772956d7cb8f9082432c9e63a37e6930/opentelemetry_instrumentation_urllib3-0.66b1.tar.gz", hash = "sha256:31e84a2ccfa86ca1d4f1209ea66c1f709c335348d5c487477d3843172efcc408", size = 18826, upload-time = "2026-10-06T17:36:43.547Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ed/6f/76a46806cd21002cac1bfd087f5e4674b195ab31ab44c773ca534b6bb546/opentelemetry_instrumentation_urllib3-0.54b1.tar.gz", hash = "sha256:0d30ba3b230e4100cfadaad29174bf7bceac70e812e4f5204e681e4b55a74cd9", size = 15697, upload-time = "2025-05-16T19:04:07.709Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/be/7f/16f82bf359c28fb71ad4dc5ad386cce3483abbf1ce0ade087fe78c18daf3/opentelemetry_instrumentation_urllib3-0.66b1-py3-none-any.whl", hash = "sha256:e48b448fed4a127e54c584c37a443c91d80d34b9f9de324af9a00cead8b338b8", size = 13462, upload-time = "2026-10-06T17:36:02.242Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/7a/d75bec41edb6deaf1d2859bab66a84c8ba03e822e7eafdb245da205e53f6/opentelemetry_instrumentation_urllib3-0.54b1-py3-none-any.whl", hash = "sha256:e87958c297ddd36d30e1c9069f34a9690e845e4ccc2662dd80e99ed976d4c03e", size = 13123, upload-time = "2025-05-16T19:03:14.053Z" },
]
[[package]]
@@ -6952,50 +6879,41 @@ wheels = [
[[package]]
name = "opentelemetry-proto"
-version = "1.45.1"
+version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/4b/7f/15f014fb195da6c2dbb6c71399b8e76824878718e94de6454038488eed28/opentelemetry_proto-1.45.1.tar.gz", hash = "sha256:79e0fb95e4616691a469439238aa9224d75779b3e108e895d1aa125ab29ca77c", size = 46488, upload-time = "2026-10-06T17:33:11.49Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/dc/791f3d60a1ad8235930de23eea735ae1084be1c6f96fdadf38710662a7e5/opentelemetry_proto-1.33.1.tar.gz", hash = "sha256:9627b0a5c90753bf3920c398908307063e4458b287bb890e5c1d6fa11ad50b68", size = 34363, upload-time = "2025-05-16T18:52:52.141Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ab/9a/42ec8180a769516ae757e893b69736826efceac7332553915b4528a91c6d/opentelemetry_proto-1.45.1-py3-none-any.whl", hash = "sha256:f38e2a8413053c180cd3d2637fbb279673ec2f6a6e09c995aafa2f452c52b46e", size = 72488, upload-time = "2026-10-06T17:32:53.057Z" },
-]
-
-[[package]]
-name = "opentelemetry-proto-json"
-version = "0.66b1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/90/25/a845cc4644123c6b29b2c21822341a55e7f1808703d4f557d24cbd841290/opentelemetry_proto_json-0.66b1.tar.gz", hash = "sha256:92bb70ff0f05815821e922ea628bfdf96f034b981747b0ba8e09db61e1a9cddd", size = 18708, upload-time = "2026-10-06T17:33:12.169Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/93/7d/bb6acc006c6c4042ef9f734f4901630a440992033f48b66a58fc8fd5e3f4/opentelemetry_proto_json-0.66b1-py3-none-any.whl", hash = "sha256:0d07f1922451cf3a8705bb46fa807f478a1164fbe9287bf7bd50e1ff43f75da3", size = 32061, upload-time = "2026-10-06T17:32:54.097Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/29/48609f4c875c2b6c80930073c82dd1cafd36b6782244c01394007b528960/opentelemetry_proto-1.33.1-py3-none-any.whl", hash = "sha256:243d285d9f29663fc7ea91a7171fcc1ccbbfff43b48df0774fd64a37d98eda70", size = 55854, upload-time = "2025-05-16T18:52:36.269Z" },
]
[[package]]
name = "opentelemetry-sdk"
-version = "1.45.1"
+version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a1/79/7392e21a1c8f0c61d90b223e31c7e48cb9d452e91a6b820ad24cca5f23c4/opentelemetry_sdk-1.45.1.tar.gz", hash = "sha256:63d24a6ca645019a631e6a51999c73e93adcac1196ca640b8ae78a7cc4762bf3", size = 218324, upload-time = "2026-10-06T17:33:13.26Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/67/12/909b98a7d9b110cce4b28d49b2e311797cffdce180371f35eba13a72dd00/opentelemetry_sdk-1.33.1.tar.gz", hash = "sha256:85b9fcf7c3d23506fbc9692fd210b8b025a1920535feec50bd54ce203d57a531", size = 161885, upload-time = "2025-05-16T18:52:52.832Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/95/3c/87c42b4bd6dd297536f04cd9383d212ac557ecd49f2cbdcd46da1c9ef5c8/opentelemetry_sdk-1.45.1-py3-none-any.whl", hash = "sha256:c604c11dc429810812348989115fa44bd558772a3d7442afc43d024f2c250ca4", size = 140063, upload-time = "2026-10-06T17:32:55.04Z" },
+ { url = "https://files.pythonhosted.org/packages/df/8e/ae2d0742041e0bd7fe0d2dcc5e7cce51dcf7d3961a26072d5b43cc8fa2a7/opentelemetry_sdk-1.33.1-py3-none-any.whl", hash = "sha256:19ea73d9a01be29cacaa5d6c8ce0adc0b7f7b4d58cc52f923e4413609f670112", size = 118950, upload-time = "2025-05-16T18:52:37.297Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
-version = "0.66b1"
+version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "deprecated" },
{ name = "opentelemetry-api" },
- { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/46/e4/dbbfb2a010c4db2224a5114638acede6fe563d33cc20fb1752cebcbe6298/opentelemetry_semantic_conventions-0.66b1.tar.gz", hash = "sha256:497ca63bf383723411e8eaf60c8779e9877633c936bb641080adab59d0eb6ec8", size = 150250, upload-time = "2026-10-06T17:33:14.073Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5b/2c/d7990fc1ffc82889d466e7cd680788ace44a26789809924813b164344393/opentelemetry_semantic_conventions-0.54b1.tar.gz", hash = "sha256:d1cecedae15d19bdaafca1e56b29a66aa286f50b5d08f036a145c7f3e9ef9cee", size = 118642, upload-time = "2025-05-16T18:52:53.962Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/bc/14/67f8aa798857f8cf686f515bf93d9bb877ce952ddc8efae0fa25b45ce0d6/opentelemetry_semantic_conventions-0.66b1-py3-none-any.whl", hash = "sha256:d4cddeb4315490b35213f55e2bdc9ac54bb1e4d318927475bed62b35545e581b", size = 206279, upload-time = "2026-10-06T17:32:56.103Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/80/08b1698c52ff76d96ba440bf15edc2f4bc0a279868778928e947c1004bdd/opentelemetry_semantic_conventions-0.54b1-py3-none-any.whl", hash = "sha256:29dab644a7e435b58d3a3918b58c333c92686236b30f7891d5e51f02933ca60d", size = 194938, upload-time = "2025-05-16T18:52:38.796Z" },
]
[[package]]
@@ -7009,11 +6927,11 @@ wheels = [
[[package]]
name = "opentelemetry-util-http"
-version = "0.66b1"
+version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7c/b5/df4b61da899f6ebdffdbdf0c8b0f3189ee57151694ccd5b7d50ee2906241/opentelemetry_util_http-0.66b1.tar.gz", hash = "sha256:047dea1a628031f857a5a32261dc0e955bc162d39993ed1cffb8f2cff5ba8a62", size = 11586, upload-time = "2026-10-06T17:36:46.572Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a8/9f/1d8a1d1f34b9f62f2b940b388bf07b8167a8067e70870055bd05db354e5c/opentelemetry_util_http-0.54b1.tar.gz", hash = "sha256:f0b66868c19fbaf9c9d4e11f4a7599fa15d5ea50b884967a26ccd9d72c7c9d15", size = 8044, upload-time = "2025-05-16T19:04:10.79Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/eb/9b/c77ecaea79ba0de1a11e7f06a7f5eea7043ec23f1860dcf5f03536698e4c/opentelemetry_util_http-0.66b1-py3-none-any.whl", hash = "sha256:8f443d7abcaf29c4a07b373bbd31b5b39132c0ed3c27d015a59dc0323d5b1c58", size = 8387, upload-time = "2026-10-06T17:36:06.984Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ef/c5aa08abca6894792beed4c0405e85205b35b8e73d653571c9ff13a8e34e/opentelemetry_util_http-0.54b1-py3-none-any.whl", hash = "sha256:b1c91883f980344a1c3c486cffd47ae5c9c1dd7323f9cbe9fdb7cadb401c87c9", size = 7301, upload-time = "2025-05-16T19:03:18.18Z" },
]
[[package]]
@@ -8239,14 +8157,14 @@ wheels = [
[[package]]
name = "pypdf"
-version = "6.21.1"
+version = "6.19.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/51/e1/32a9c4dbd94618145f8bab5c623bdb0a528f23fc8719afd77ba0278fd377/pypdf-6.21.1.tar.gz", hash = "sha256:2d62c383acd6e0dff0a96322ba8b1135229343b084d73c6f6cfd947fd291afcb", size = 7088597, upload-time = "2026-10-17T10:29:52.801Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1f/ac/63d71aaedb59acbcdef491e6ca6469165e3771c9c74358204818fd9bc5a6/pypdf-6.19.0.tar.gz", hash = "sha256:bbc43aca292369ccc6cbc8a921991ecf2538a3587ab5a116eff06c321d647155", size = 7033266, upload-time = "2026-09-16T09:32:05.946Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/85/e6/24c944e473c34e818cf1b98085796fcd780465ecfe17663f2bf8fee1958b/pypdf-6.21.1-py3-none-any.whl", hash = "sha256:553e2d42f5bb3bb2a785a7d3de8ef90bb7df74b5413eff5a7b4388007ea9254b", size = 405134, upload-time = "2026-10-17T10:29:50.687Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2c/c43c03eaf630435f023f1dc61ec4a4a78951ad5530a62c71cc89bde307b7/pypdf-6.19.0-py3-none-any.whl", hash = "sha256:7e5d6e730e7dae87d560a2cee218b852f6498c8be61966f3cd02ead971e48d14", size = 395480, upload-time = "2026-09-16T09:32:04.087Z" },
]
[[package]]
diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json
index 81e9425ef4e..536d7a88f25 100644
--- a/vscode-extension/package-lock.json
+++ b/vscode-extension/package-lock.json
@@ -17,7 +17,7 @@
"@vscode/vsce": "^4.0.0",
"esbuild": "^0.28.2",
"typescript": "^5.9.3",
- "vitest": "^4.1.12"
+ "vitest": "^4.1.11"
},
"engines": {
"vscode": "^1.115.0"
@@ -1323,16 +1323,16 @@
}
},
"node_modules/@vitest/expect": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.12.tgz",
- "integrity": "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
+ "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.12",
- "@vitest/utils": "4.1.12",
+ "@vitest/spy": "4.1.11",
+ "@vitest/utils": "4.1.11",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -1341,13 +1341,13 @@
}
},
"node_modules/@vitest/mocker": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz",
- "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
+ "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.1.12",
+ "@vitest/spy": "4.1.11",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -1368,9 +1368,9 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.12.tgz",
- "integrity": "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
+ "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1381,13 +1381,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz",
- "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
+ "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.12",
+ "@vitest/utils": "4.1.11",
"pathe": "^2.0.3"
},
"funding": {
@@ -1395,14 +1395,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.12.tgz",
- "integrity": "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
+ "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.12",
- "@vitest/utils": "4.1.12",
+ "@vitest/pretty-format": "4.1.11",
+ "@vitest/utils": "4.1.11",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -1411,9 +1411,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz",
- "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
+ "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1421,13 +1421,13 @@
}
},
"node_modules/@vitest/utils": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.12.tgz",
- "integrity": "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
+ "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.12",
+ "@vitest/pretty-format": "4.1.11",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -3390,19 +3390,19 @@
}
},
"node_modules/vitest": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz",
- "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
+ "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/expect": "4.1.12",
- "@vitest/mocker": "4.1.12",
- "@vitest/pretty-format": "4.1.12",
- "@vitest/runner": "4.1.12",
- "@vitest/snapshot": "4.1.12",
- "@vitest/spy": "4.1.12",
- "@vitest/utils": "4.1.12",
+ "@vitest/expect": "4.1.11",
+ "@vitest/mocker": "4.1.11",
+ "@vitest/pretty-format": "4.1.11",
+ "@vitest/runner": "4.1.11",
+ "@vitest/snapshot": "4.1.11",
+ "@vitest/spy": "4.1.11",
+ "@vitest/utils": "4.1.11",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -3430,12 +3430,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.12",
- "@vitest/browser-preview": "4.1.12",
- "@vitest/browser-webdriverio": "^4.1.11",
- "@vitest/coverage-istanbul": "4.1.12",
- "@vitest/coverage-v8": "4.1.12",
- "@vitest/ui": "4.1.12",
+ "@vitest/browser-playwright": "4.1.11",
+ "@vitest/browser-preview": "4.1.11",
+ "@vitest/browser-webdriverio": "4.1.11",
+ "@vitest/coverage-istanbul": "4.1.11",
+ "@vitest/coverage-v8": "4.1.11",
+ "@vitest/ui": "4.1.11",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
diff --git a/vscode-extension/package.json b/vscode-extension/package.json
index a3cf2dc7a5f..431dad316a1 100644
--- a/vscode-extension/package.json
+++ b/vscode-extension/package.json
@@ -82,6 +82,6 @@
"@vscode/vsce": "^4.0.0",
"esbuild": "^0.28.2",
"typescript": "^5.9.3",
- "vitest": "^4.1.12"
+ "vitest": "^4.1.11"
}
}