feat(proxy): forward spend logs metadata to an upstream LiteLLM proxy

In a proxy-to-proxy setup the upstream proxy recorded only its own key's
metadata, so per-user attribution set on a downstream virtual key never
reached the upstream SpendLogs. `forward_client_headers_to_llm_api` only
relays headers the caller sent, and `spend_logs_metadata` resolved from
the downstream key or team is never a caller header, so it had no way out.

Adds `general_settings.forward_spend_logs_metadata_to_llm_api` (off by
default, with an Admin UI toggle). When on, the downstream emits the
request's resolved `spend_logs_metadata` as `x-litellm-spend-logs-metadata`
on the outbound call, which the upstream already reads into its own
SpendLogs. The emit runs after the key and team merges, so both proxies
log the same values, and caller-supplied values still win over key and
team ones. Values above 4KB once encoded are dropped with a warning rather
than sent, since an oversized header would make the upstream reject the
request.

Resolves LIT-3371
This commit is contained in:
Yucheng He 2026-09-03 00:51:53 -07:00
parent 658f50663d
commit 9b6742b6d8
11 changed files with 424 additions and 2 deletions

View file

@ -1514,6 +1514,14 @@ ALLOWED_VERTEX_AI_PASSTHROUGH_HEADERS: Final = {
# Works for all LLM pass-through endpoints (Vertex AI, Anthropic, Bedrock, etc.)
PASS_THROUGH_HEADER_PREFIX: Final = "x-pass-"
# Request header an upstream LiteLLM proxy reads to populate SpendLogs `spend_logs_metadata`.
SPEND_LOGS_METADATA_HEADER_NAME: Final = "x-litellm-spend-logs-metadata"
# Cap on the JSON-encoded `x-litellm-spend-logs-metadata` value a downstream proxy emits.
# Kept well under the 8KB per-header limit common to uvicorn/nginx/ALB so an oversized
# key or team `spend_logs_metadata` is dropped with a warning instead of making the
# upstream reject the whole request.
MAX_SPEND_LOGS_METADATA_HEADER_BYTES: Final = 4096
BASE_MCP_ROUTE: Final = "/mcp"
BATCH_STATUS_POLL_INTERVAL_SECONDS: Final = int(os.getenv("BATCH_STATUS_POLL_INTERVAL_SECONDS", 3600)) # 1 hour

View file

@ -2704,6 +2704,15 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
None,
description="If True, forwards client headers (e.g. Authorization) to the LLM API. Required for Claude Code with Max subscription.",
)
forward_spend_logs_metadata_to_llm_api: bool | None = Field(
None,
description=(
"If True, sends the request's resolved spend_logs_metadata (key, team and caller values merged) to the "
"LLM API as the x-litellm-spend-logs-metadata header, so an upstream LiteLLM proxy records it in its own "
"SpendLogs. Proxy-wide: only enable it when the configured upstreams are LiteLLM proxies you trust with "
"those identifiers. Default is False."
),
)
mcp_required_fields: list[str] | None = Field(
None,
description="List of MCP server fields that must be filled in for a submission to pass standards checks (e.g. ['description', 'source_url', 'alias']).",

View file

@ -21,10 +21,12 @@ from litellm.constants import (
CONSUMED_REQUEST_TAGS_METADATA_KEY,
INTERNAL_CALL_ORIGIN_METADATA_KEY,
LITELLM_PROXY_MASTER_KEY_ALIAS,
MAX_SPEND_LOGS_METADATA_HEADER_BYTES,
OTEL_SERVICE_NAME_METADATA_KEYS,
PRE_CALL_EXECUTED_GUARDRAILS_KEY,
SESSION_DEPLOYMENT_AFFINITY_TTL_METADATA_KEY,
SESSION_ID_GENERATED_METADATA_KEY,
SPEND_LOGS_METADATA_HEADER_NAME,
)
from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
from litellm.litellm_core_utils.initialize_dynamic_callback_params import (
@ -1092,7 +1094,7 @@ class LiteLLMProxyRequestSetup:
"""
from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
spend_logs_metadata_header: Final = headers.get("x-litellm-spend-logs-metadata", None)
spend_logs_metadata_header: Final = headers.get(SPEND_LOGS_METADATA_HEADER_NAME, None)
if spend_logs_metadata_header is not None:
return safe_json_loads(spend_logs_metadata_header)
return None
@ -1213,6 +1215,79 @@ class LiteLLMProxyRequestSetup:
return returned_headers
@staticmethod
def add_spend_logs_metadata_to_llm_call_headers(
data: MutableMapping[str, object], # mutable-ok: this helper writes the outbound header into it
_metadata_variable_name: str,
general_settings: Mapping[str, object] | None,
) -> None:
"""
Emit the request's resolved ``spend_logs_metadata`` as the
``x-litellm-spend-logs-metadata`` header on the outbound LLM call.
Proxy-to-proxy attribution: an upstream LiteLLM proxy reads that header in
``_get_spend_logs_metadata_from_request_headers`` and stores the values in its
own SpendLogs row. ``forward_client_headers_to_llm_api`` only relays headers
the client itself sent, so ``spend_logs_metadata`` the downstream resolved from
the virtual key or the team never reached the upstream.
Must run after every key/team ``spend_logs_metadata`` merge so the header
carries the same values the downstream writes to its own SpendLogs. The
resolved dict already merges caller, key and team values with key/team losing
to the caller, so the upstream sees one deterministic namespace.
Opt-in via ``general_settings.forward_spend_logs_metadata_to_llm_api``: the
values are customer identifiers and the header is sent to every configured
provider, not only to LiteLLM upstreams.
"""
if not general_settings or general_settings.get("forward_spend_logs_metadata_to_llm_api") is not True:
return
metadata: Final = data.get(_metadata_variable_name)
if not isinstance(metadata, dict):
return
spend_logs_metadata: Final = metadata.get("spend_logs_metadata")
if not isinstance(spend_logs_metadata, dict) or not spend_logs_metadata:
return
try:
encoded: Final = json.dumps(spend_logs_metadata)
except (TypeError, ValueError):
verbose_proxy_logger.warning(
"spend_logs_metadata is not JSON-serializable, not forwarding it to the LLM API"
)
return
encoded_size: Final = len(encoded.encode("utf-8"))
if encoded_size > MAX_SPEND_LOGS_METADATA_HEADER_BYTES:
verbose_proxy_logger.warning(
"spend_logs_metadata is %s bytes, above the %s byte header limit, not forwarding it to the LLM API",
encoded_size,
MAX_SPEND_LOGS_METADATA_HEADER_BYTES,
)
return
existing_headers: Final = data.get("headers")
if isinstance(existing_headers, dict):
existing_headers[SPEND_LOGS_METADATA_HEADER_NAME] = encoded
else:
# Every provider reads `headers or litellm.headers`, replacing rather than
# merging, so creating this dict from scratch would drop the operator's
# `litellm_settings.headers` from every request the flag applies to.
emitted: Final = dict(litellm.headers or {})
emitted[SPEND_LOGS_METADATA_HEADER_NAME] = encoded
data["headers"] = emitted # rebind-ok: emitting this header is what this helper is for
# `extra_headers` beats `headers` in every provider handler, so a caller that put
# this header in the request body would otherwise overwrite the proxy's resolved
# value and forge the attribution the upstream records. The proxy owns this header;
# a caller contributes through the `x-litellm-spend-logs-metadata` request header,
# which is merged above and loses to nothing.
caller_extra_headers: Final = data.get("extra_headers")
if isinstance(caller_extra_headers, dict):
for key in caller_extra_headers:
if isinstance(key, str) and key.lower() == SPEND_LOGS_METADATA_HEADER_NAME:
caller_extra_headers[key] = encoded
@staticmethod
def add_headers_to_llm_call_by_model_group(data: dict, headers: dict, user_api_key_dict: UserAPIKeyAuth) -> dict:
"""
@ -2267,6 +2342,14 @@ async def add_litellm_data_to_request(
user_api_key_dict=user_api_key_dict,
)
# Runs after the key/team spend_logs_metadata merges above so the forwarded header
# carries the same values this proxy writes to its own SpendLogs.
LiteLLMProxyRequestSetup.add_spend_logs_metadata_to_llm_call_headers(
data=data,
_metadata_variable_name=_metadata_variable_name,
general_settings=general_settings,
)
verbose_proxy_logger.debug("[PROXY] returned data from litellm_pre_call_utils: %s", data)
# Team/Project credential overrides from model_config

View file

@ -16512,6 +16512,7 @@ _GENERAL_SETTINGS_CONFIG_LIST_FIELD_TYPES: Final[Mapping[str, str]] = MappingPro
"mcp_xff_num_trusted_hops": "Integer",
"always_include_stream_usage": "Boolean",
"forward_client_headers_to_llm_api": "Boolean",
"forward_spend_logs_metadata_to_llm_api": "Boolean",
"mcp_required_fields": "List",
"cancel_on_disconnect": "Boolean",
"disable_auto_add_proxy_admin_to_teams": "Boolean",

View file

@ -264,6 +264,16 @@ class UISettings(BaseModel):
),
)
forward_spend_logs_metadata_to_llm_api: bool = Field(
default=False,
description=(
"Sends the request's resolved spend_logs_metadata (key, team and caller values merged) to the upstream "
"LLM as the x-litellm-spend-logs-metadata header, so an upstream LiteLLM proxy records it in its own "
"SpendLogs. Use for proxy-to-proxy setups where per-user attribution has to survive the hop. Proxy-wide, "
"so enable it only when the configured upstreams are LiteLLM proxies you trust with those identifiers."
),
)
disable_agents_for_internal_users: bool = Field(
default=False,
description="If true, internal users cannot access agent management endpoints or the Agents page in the UI.",
@ -317,6 +327,7 @@ ALLOWED_UI_SETTINGS_FIELDS: Final = {
"require_auth_for_public_ai_hub",
"allow_public_health_readiness_details",
"forward_client_headers_to_llm_api",
"forward_spend_logs_metadata_to_llm_api",
"forward_llm_provider_auth_headers",
"disable_agents_for_internal_users",
"allow_agents_for_team_admins",
@ -354,6 +365,7 @@ def _derived_ui_setting_value(key: str) -> object:
_RUNTIME_GENERAL_SETTINGS_FLAGS: Final = [
"allow_public_health_readiness_details",
"forward_client_headers_to_llm_api",
"forward_spend_logs_metadata_to_llm_api",
"forward_llm_provider_auth_headers",
"disable_agents_for_internal_users",
"allow_agents_for_team_admins",

View file

@ -141,7 +141,8 @@ class ResponsesAPIRequestUtils:
) -> dict[str, object] | None:
"""
Merge headers forwarded by the proxy (`headers` kwarg, set when
`forward_client_headers_to_llm_api` is enabled) into `extra_headers`.
`forward_client_headers_to_llm_api` or `forward_spend_logs_metadata_to_llm_api`
is enabled) into `extra_headers`.
`extra_headers` wins on conflicts, since it is set explicitly by the caller.
Header names are compared case-insensitively, as HTTP defines them.

View file

@ -14,6 +14,7 @@ from pydantic import ValidationError as PydanticValidationError
from starlette.datastructures import Headers
import litellm
from litellm.constants import MAX_SPEND_LOGS_METADATA_HEADER_BYTES, SPEND_LOGS_METADATA_HEADER_NAME
from litellm.proxy._types import AddTeamCallback, ProxyException, TeamCallbackMetadata, UserAPIKeyAuth
from litellm.proxy.litellm_pre_call_utils import (
KeyAndTeamLoggingSettings,
@ -7895,3 +7896,214 @@ async def test_missing_session_id_unknown_value_is_ignored():
)
assert "session_id" not in updated["metadata"]
def _spend_logs_metadata_request(header_value: str | None = None) -> MagicMock:
request = _request_for("/v1/chat/completions")
headers = {"Content-Type": "application/json"}
if header_value is not None:
headers[SPEND_LOGS_METADATA_HEADER_NAME] = header_value
request.headers = headers
return request
def _proxy_chain_auth(
metadata: dict | None = None,
team_metadata: dict | None = None,
) -> UserAPIKeyAuth:
"""Downstream virtual key carrying per-user attribution, plus a team-level default."""
return UserAPIKeyAuth(
api_key="hashed-key",
metadata=(
metadata
if metadata is not None
else {"spend_logs_metadata": {"user_id": "U0099887", "username": "jdoe"}}
),
team_metadata=(
team_metadata
if team_metadata is not None
else {"spend_logs_metadata": {"cost_center": "CC-42", "user_id": "team-default"}}
),
)
@pytest.mark.asyncio
async def test_forward_spend_logs_metadata_covers_litellm_metadata_routes():
"""
`/v1/responses` and `/v1/messages` keep request metadata under `litellm_metadata`, not
`metadata`, so reading the slot by name is what makes the feature work on them at all.
"""
request = _spend_logs_metadata_request()
request.scope = {"path": "/v1/responses"}
request.url.path = "/v1/responses"
updated = await add_litellm_data_to_request(
data={"model": "gpt-4o", "input": "hi"},
request=request,
user_api_key_dict=_proxy_chain_auth(),
proxy_config=MagicMock(),
general_settings={"forward_spend_logs_metadata_to_llm_api": True},
)
assert "litellm_metadata" in updated, "route should resolve to the litellm_metadata slot"
forwarded = json.loads(updated["headers"][SPEND_LOGS_METADATA_HEADER_NAME])
assert forwarded == updated["litellm_metadata"]["spend_logs_metadata"]
assert forwarded["user_id"] == "U0099887"
@pytest.mark.asyncio
async def test_forward_spend_logs_metadata_overrides_caller_supplied_extra_headers():
"""
`extra_headers` in the request body wins over `headers` in every provider handler, so a
caller could otherwise forge the attribution the upstream proxy records for them.
"""
updated = await add_litellm_data_to_request(
data={
"model": "gpt-4o",
"messages": [],
"extra_headers": {SPEND_LOGS_METADATA_HEADER_NAME: json.dumps({"user_id": "forged"})},
},
request=_spend_logs_metadata_request(),
user_api_key_dict=_proxy_chain_auth(),
proxy_config=MagicMock(),
general_settings={"forward_spend_logs_metadata_to_llm_api": True},
)
resolved = json.loads(updated["headers"][SPEND_LOGS_METADATA_HEADER_NAME])
assert resolved["user_id"] == "U0099887"
forged = json.loads(updated["extra_headers"][SPEND_LOGS_METADATA_HEADER_NAME])
assert forged == resolved, "the proxy's resolved value must win over the request body"
@pytest.mark.asyncio
async def test_forward_spend_logs_metadata_keeps_globally_configured_headers():
"""
Providers read `headers or litellm.headers`, replacing rather than merging, so creating
`data["headers"]` here would silently drop `litellm_settings.headers` from the call.
"""
original = litellm.headers
litellm.headers = {"x-org-routing": "eu"}
try:
updated = await add_litellm_data_to_request(
data={"model": "gpt-4o", "messages": []},
request=_spend_logs_metadata_request(),
user_api_key_dict=_proxy_chain_auth(),
proxy_config=MagicMock(),
general_settings={"forward_spend_logs_metadata_to_llm_api": True},
)
finally:
litellm.headers = original
assert updated["headers"]["x-org-routing"] == "eu"
assert json.loads(updated["headers"][SPEND_LOGS_METADATA_HEADER_NAME])["user_id"] == "U0099887"
@pytest.mark.asyncio
async def test_forward_spend_logs_metadata_emits_resolved_key_and_team_values():
"""
LIT-3371: proxy-to-proxy attribution. `forward_client_headers_to_llm_api` only relays
headers the caller sent, so `spend_logs_metadata` resolved from the downstream virtual
key or its team never reached the upstream proxy. The opt-in must emit the fully
resolved dict, which means it has to run after the key AND team merges.
"""
updated = await add_litellm_data_to_request(
data={"model": "gpt-4o", "messages": []},
request=_spend_logs_metadata_request(header_value=json.dumps({"request_id": "req-1"})),
user_api_key_dict=_proxy_chain_auth(),
proxy_config=MagicMock(),
general_settings={"forward_spend_logs_metadata_to_llm_api": True},
)
forwarded = json.loads(updated["headers"][SPEND_LOGS_METADATA_HEADER_NAME])
assert forwarded == {
"request_id": "req-1",
"user_id": "U0099887",
"username": "jdoe",
"cost_center": "CC-42",
}
# the header must carry exactly what this proxy logs for itself
assert forwarded == updated["metadata"]["spend_logs_metadata"]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"general_settings",
[
{},
{"forward_spend_logs_metadata_to_llm_api": False},
{"forward_client_headers_to_llm_api": True},
],
)
async def test_forward_spend_logs_metadata_is_opt_in(general_settings: dict[str, object]):
"""The values are customer identifiers, so nothing is emitted without the explicit opt-in."""
updated = await add_litellm_data_to_request(
data={"model": "gpt-4o", "messages": []},
request=_spend_logs_metadata_request(),
user_api_key_dict=_proxy_chain_auth(),
proxy_config=MagicMock(),
general_settings=general_settings,
)
assert SPEND_LOGS_METADATA_HEADER_NAME not in updated.get("headers", {})
assert updated["metadata"]["spend_logs_metadata"]["user_id"] == "U0099887"
@pytest.mark.asyncio
async def test_forward_spend_logs_metadata_keeps_other_forwarded_headers():
"""The emitted header is merged into the forwarded-header map, never replacing it."""
request = _spend_logs_metadata_request()
request.headers = {"Content-Type": "application/json", "x-custom-trace": "trace-1"}
updated = await add_litellm_data_to_request(
data={"model": "gpt-4o", "messages": []},
request=request,
user_api_key_dict=_proxy_chain_auth(),
proxy_config=MagicMock(),
general_settings={
"forward_client_headers_to_llm_api": True,
"forward_spend_logs_metadata_to_llm_api": True,
},
)
assert updated["headers"]["x-custom-trace"] == "trace-1"
assert json.loads(updated["headers"][SPEND_LOGS_METADATA_HEADER_NAME])["user_id"] == "U0099887"
@pytest.mark.asyncio
async def test_forward_spend_logs_metadata_skips_oversized_value():
"""
An oversized header makes the upstream reject the whole request, so drop the header
rather than the traffic. Everything else about the request is unchanged.
"""
oversized = {"blob": "x" * (MAX_SPEND_LOGS_METADATA_HEADER_BYTES + 1)}
request = _spend_logs_metadata_request()
request.headers = {"Content-Type": "application/json", "x-custom-trace": "trace-1"}
updated = await add_litellm_data_to_request(
data={"model": "gpt-4o", "messages": []},
request=request,
user_api_key_dict=_proxy_chain_auth(metadata={"spend_logs_metadata": oversized}, team_metadata={}),
proxy_config=MagicMock(),
general_settings={
"forward_client_headers_to_llm_api": True,
"forward_spend_logs_metadata_to_llm_api": True,
},
)
assert SPEND_LOGS_METADATA_HEADER_NAME not in updated["headers"]
assert updated["headers"]["x-custom-trace"] == "trace-1"
assert updated["metadata"]["spend_logs_metadata"] == oversized
@pytest.mark.asyncio
async def test_forward_spend_logs_metadata_noop_without_values():
"""A key with no spend_logs_metadata must not add an empty header."""
updated = await add_litellm_data_to_request(
data={"model": "gpt-4o", "messages": []},
request=_spend_logs_metadata_request(),
user_api_key_dict=UserAPIKeyAuth(api_key="hashed-key"),
proxy_config=MagicMock(),
general_settings={"forward_spend_logs_metadata_to_llm_api": True},
)
assert SPEND_LOGS_METADATA_HEADER_NAME not in updated.get("headers", {})

View file

@ -9821,6 +9821,39 @@ def test_get_config_list_includes_apply_user_budget_to_team_keys(monkeypatch):
app.dependency_overrides.clear()
def test_get_config_list_includes_forward_spend_logs_metadata_to_llm_api(monkeypatch):
"""Resolves LIT-3371: the proxy-to-proxy opt-in must be discoverable via
/config/list so it renders as a Boolean toggle on the Admin UI General
Settings table. This needs both the ConfigGeneralSettings field and the
allowed_args entry; missing either silently hides it from the UI."""
import types
from unittest.mock import AsyncMock, MagicMock
from fastapi.testclient import TestClient
import litellm.proxy.proxy_server as ps
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.proxy_server import app
mock_prisma = MagicMock()
mock_config_table = MagicMock()
mock_config_table.find_first = AsyncMock(return_value=None)
mock_prisma.db = types.SimpleNamespace(litellm_config=mock_config_table)
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
user_id="admin", user_role=LitellmUserRoles.PROXY_ADMIN
)
try:
client = TestClient(app)
resp = client.get("/config/list", params={"config_type": "general_settings"})
assert resp.status_code == 200, resp.text
fields = {item["field_name"]: item for item in resp.json()}
assert "forward_spend_logs_metadata_to_llm_api" in fields
assert fields["forward_spend_logs_metadata_to_llm_api"]["field_type"] == "Boolean"
finally:
app.dependency_overrides.clear()
def test_get_config_list_includes_budget_exceeded_throttle_percentage(monkeypatch):
"""The throttle fraction is a litellm_settings scalar surfaced on the General
Settings table as a Float field so it sits with the other global limits; it

View file

@ -34,12 +34,16 @@ const buildSettingsResponse = (overrides?: Partial<Record<string, unknown>>) =>
require_auth_for_public_ai_hub: {
description: "Require authentication for public AI Hub",
},
forward_spend_logs_metadata_to_llm_api: {
description: "Forward spend logs metadata to LLM API",
},
},
},
values: {
disable_model_add_for_internal_users: false,
disable_team_admin_delete_team_user: false,
require_auth_for_public_ai_hub: false,
forward_spend_logs_metadata_to_llm_api: false,
},
},
isLoading: false,
@ -155,4 +159,32 @@ describe("UISettings", () => {
);
expect(toast.success).toHaveBeenCalledWith("UI settings updated successfully");
});
it("should toggle forward spend logs metadata setting and call update", () => {
const mutateMock = vi.fn((_settings, options) => {
options?.onSuccess?.();
});
mockUseUpdateUISettings.mockReturnValue({
mutate: mutateMock,
isPending: false,
error: null,
});
render(<UISettings />);
const toggle = screen.getByRole("switch", { name: "Forward spend logs metadata to LLM API" });
act(() => {
fireEvent.click(toggle);
});
expect(mutateMock).toHaveBeenCalledWith(
{ forward_spend_logs_metadata_to_llm_api: true },
expect.objectContaining({
onSuccess: expect.any(Function),
onError: expect.any(Function),
}),
);
expect(toast.success).toHaveBeenCalledWith("UI settings updated successfully");
});
});

View file

@ -56,6 +56,7 @@ export default function UISettings() {
const requireAuthForPublicAIHubProperty = schema?.properties?.require_auth_for_public_ai_hub;
const forwardClientHeadersProperty = schema?.properties?.forward_client_headers_to_llm_api;
const forwardLLMProviderAuthHeadersProperty = schema?.properties?.forward_llm_provider_auth_headers;
const forwardSpendLogsMetadataProperty = schema?.properties?.forward_spend_logs_metadata_to_llm_api;
const enableProjectsUIProperty = schema?.properties?.enable_projects_ui;
const enableChatUIProperty = schema?.properties?.enable_chat_ui;
const enabledPagesProperty = schema?.properties?.enabled_ui_pages_internal_users;
@ -138,6 +139,20 @@ export default function UISettings() {
);
};
const handleToggleForwardSpendLogsMetadata = (checked: boolean) => {
updateSettings(
{ forward_spend_logs_metadata_to_llm_api: checked },
{
onSuccess: () => {
toast.success("UI settings updated successfully");
},
onError: (error) => {
toast.fromError(error);
},
},
);
};
const handleToggleEnableProjectsUI = (checked: boolean) => {
updateSettings(
{ enable_projects_ui: checked },
@ -341,6 +356,17 @@ export default function UISettings() {
"Forwards provider auth headers (x-api-key, x-goog-api-key, api-key, ocp-apim-subscription-key) to the upstream LLM, overriding any deployment-configured key for that request. Enable for Claude Code BYOK (clients bring their own API key). Independent of the client-headers toggle — enable only the one(s) you need."
}
/>
<SettingRow
checked={Boolean(values.forward_spend_logs_metadata_to_llm_api)}
disabled={isUpdating}
onCheckedChange={handleToggleForwardSpendLogsMetadata}
ariaLabel={forwardSpendLogsMetadataProperty?.description ?? "Forward spend logs metadata to LLM API"}
label="Forward spend logs metadata to LLM API"
description={
forwardSpendLogsMetadataProperty?.description ??
"Sends the request's resolved spend_logs_metadata (key, team and caller values merged) to the upstream LLM as the x-litellm-spend-logs-metadata header, so an upstream LiteLLM proxy records it in its own SpendLogs. Use for proxy-to-proxy setups where per-user attribution has to survive the hop. Proxy-wide, so enable it only when the configured upstreams are LiteLLM proxies you trust with those identifiers."
}
/>
{enableProjectsUIProperty && (
<SettingRow
checked={Boolean(values.enable_projects_ui)}

View file

@ -25658,6 +25658,11 @@ export interface components {
* @description If True, forwards client headers (e.g. Authorization) to the LLM API. Required for Claude Code with Max subscription.
*/
forward_client_headers_to_llm_api?: boolean | null;
/**
* Forward Spend Logs Metadata To Llm Api
* @description If True, sends the request's resolved spend_logs_metadata (key, team and caller values merged) to the LLM API as the x-litellm-spend-logs-metadata header, so an upstream LiteLLM proxy records it in its own SpendLogs. Proxy-wide: only enable it when the configured upstreams are LiteLLM proxies you trust with those identifiers. Default is False.
*/
forward_spend_logs_metadata_to_llm_api?: boolean | null;
/**
* Global Max Parallel Requests
* @description global max parallel requests to allow for a proxy instance.