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. + }} + /> - @@ -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")} - + {({ ref, ...field }) => ( {!uiConfig?.sso_configured ? ( }> - Please configure SSO to log in with SSO. + {t("Please configure SSO to log in 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")} @@ -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={ ) : null}
@@ -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 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 (
); 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)} = ({
- + LiteLLM = ({ 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 = ({
); @@ -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" } }