mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix(anthropic): rename the federation workspace param so Bedrock's anthropic_workspace_id keeps working
The Bedrock Claude Platform route already reads anthropic_workspace_id from optional_params, so banning that spelling as a server-owned federation parameter broke a pre-existing client capability. The federation field is now anthropic_federation_workspace_id (env ANTHROPIC_FEDERATION_WORKSPACE_ID), which restores the base branch's behavior for Bedrock callers, drops the Bedrock-specific hint from the refusal message, and deletes the unconditional ban constant that no longer had a reader
This commit is contained in:
parent
e0ee4b6016
commit
7a53a2bc1d
10 changed files with 50 additions and 81 deletions
|
|
@ -85,8 +85,8 @@ _DENIAL_HINT: Final = (
|
|||
" Settings > Workload identity, in the rule's authentication history"
|
||||
)
|
||||
_WORKSPACE_HINT: Final = (
|
||||
"If the federation rule is enabled in more than one workspace, set anthropic_workspace_id"
|
||||
" (or ANTHROPIC_WORKSPACE_ID) to the wrkspc_ id of the workspace to mint tokens for, or to 'default'"
|
||||
"If the federation rule is enabled in more than one workspace, set anthropic_federation_workspace_id"
|
||||
" (or ANTHROPIC_FEDERATION_WORKSPACE_ID) to the wrkspc_ id of the workspace to mint tokens for, or to 'default'"
|
||||
)
|
||||
_SERVICE_ACCOUNT_HINT: Final = (
|
||||
"Anthropic's reference lists service_account_id as required: set anthropic_service_account_id"
|
||||
|
|
@ -137,7 +137,9 @@ def resolve_anthropic_wif_params(litellm_params: Mapping[str, object] | None) ->
|
|||
service_account_id=_config_value(
|
||||
litellm_params, "anthropic_service_account_id", "ANTHROPIC_SERVICE_ACCOUNT_ID"
|
||||
),
|
||||
workspace_id=_config_value(litellm_params, "anthropic_workspace_id", "ANTHROPIC_WORKSPACE_ID"),
|
||||
workspace_id=_config_value(
|
||||
litellm_params, "anthropic_federation_workspace_id", "ANTHROPIC_FEDERATION_WORKSPACE_ID"
|
||||
),
|
||||
assertion_ref=assertion_ref,
|
||||
assertion_source=assertion_source,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ from litellm.types.passthrough_endpoints.pass_through_endpoints import (
|
|||
)
|
||||
from litellm.types.router import CONFIGURABLE_CLIENTSIDE_AUTH_PARAMS
|
||||
from litellm.types.router import reject_server_owned_wif_params as _reject_server_owned_wif_params
|
||||
from litellm.types.utils import CustomPricingLiteLLMParams, server_owned_wif_litellm_params
|
||||
from litellm.types.utils import CustomPricingLiteLLMParams
|
||||
|
||||
|
||||
def is_invalid_virtual_key_error(exception: BaseException | None) -> bool:
|
||||
|
|
@ -227,11 +227,6 @@ def _allow_model_level_clientside_configurable_parameters(
|
|||
# ``extra_body.aws_web_identity_token``) without re-validating, so the
|
||||
# banned-key check has to descend into it the same way it descends into
|
||||
# ``litellm_embedding_config``.
|
||||
_SERVER_OWNED_WIF_UNCONDITIONAL_BANNED: Final[tuple[str, ...]] = server_owned_wif_litellm_params
|
||||
# The Bedrock Claude Platform route reads a workspace from workspace_id or aws_workspace_id as
|
||||
# well, and neither is a federation parameter, so say so rather than leaving that caller stuck.
|
||||
|
||||
|
||||
# Re-exported from litellm.types.router, where it lives so the router can call it on a
|
||||
# post-authentication merge without core importing from the proxy package.
|
||||
reject_server_owned_wif_params = _reject_server_owned_wif_params
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ class CredentialLiteLLMParams(BaseModel):
|
|||
anthropic_federation_rule_id: str | None = None
|
||||
anthropic_organization_id: str | None = None
|
||||
anthropic_service_account_id: str | None = None
|
||||
anthropic_workspace_id: str | None = None
|
||||
anthropic_federation_workspace_id: str | None = None
|
||||
anthropic_identity_token_file: str | None = None
|
||||
anthropic_identity_token: str | None = None
|
||||
anthropic_identity_source: str | None = None
|
||||
|
|
@ -1140,9 +1140,6 @@ class AdaptiveRouterPreferences(BaseModel):
|
|||
strengths: list[RequestType] = Field(default_factory=list)
|
||||
|
||||
|
||||
_BEDROCK_WORKSPACE_HINT: Final = " On the Bedrock Claude Platform route, pass workspace_id or aws_workspace_id instead."
|
||||
|
||||
|
||||
def reject_server_owned_wif_params(body: Mapping[str, object]) -> None:
|
||||
"""Raise ``ValueError`` if a mapping that did not come from deployment config carries a
|
||||
server-owned workload identity federation field.
|
||||
|
|
@ -1156,5 +1153,4 @@ def reject_server_owned_wif_params(body: Mapping[str, object]) -> None:
|
|||
raise ValueError(
|
||||
f"Rejected Request: {param} is a server-owned workload identity federation parameter "
|
||||
"and cannot be set in a request body; configure it on the deployment instead."
|
||||
+ (_BEDROCK_WORKSPACE_HINT if param == "anthropic_workspace_id" else "")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ ANTHROPIC_WIF_KWARGS_KEYS: Final = frozenset(
|
|||
"anthropic_federation_rule_id",
|
||||
"anthropic_organization_id",
|
||||
"anthropic_service_account_id",
|
||||
"anthropic_workspace_id",
|
||||
"anthropic_federation_workspace_id",
|
||||
"anthropic_identity_token_file",
|
||||
"anthropic_identity_token",
|
||||
"anthropic_identity_source",
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ class TestAnthropicWifKeys:
|
|||
"anthropic_federation_rule_id": "fdrl_1",
|
||||
"anthropic_organization_id": "org-1",
|
||||
"anthropic_service_account_id": "svcacct_1",
|
||||
"anthropic_workspace_id": "wrkspc_1",
|
||||
"anthropic_federation_workspace_id": "wrkspc_1",
|
||||
"anthropic_identity_token_file": "/var/run/secrets/tok",
|
||||
"anthropic_identity_token": "oidc/env/TOK",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2204,7 +2204,7 @@ ANTHROPIC_ENV_VARS = (
|
|||
"ANTHROPIC_FEDERATION_RULE_ID",
|
||||
"ANTHROPIC_ORGANIZATION_ID",
|
||||
"ANTHROPIC_SERVICE_ACCOUNT_ID",
|
||||
"ANTHROPIC_WORKSPACE_ID",
|
||||
"ANTHROPIC_FEDERATION_WORKSPACE_ID",
|
||||
"ANTHROPIC_IDENTITY_TOKEN_FILE",
|
||||
"ANTHROPIC_IDENTITY_TOKEN",
|
||||
"LITELLM_OIDC_ALLOWED_CREDENTIAL_DIRS",
|
||||
|
|
@ -3025,7 +3025,7 @@ class TestWifRespxEndToEnd:
|
|||
"anthropic_federation_rule_id",
|
||||
"anthropic_organization_id",
|
||||
"anthropic_service_account_id",
|
||||
"anthropic_workspace_id",
|
||||
"anthropic_federation_workspace_id",
|
||||
"anthropic_identity_token_file",
|
||||
"anthropic_identity_token",
|
||||
)
|
||||
|
|
@ -3055,7 +3055,7 @@ class TestWifRespxEndToEnd:
|
|||
anthropic_federation_rule_id="fdrl_e2e",
|
||||
anthropic_organization_id="org-e2e",
|
||||
anthropic_service_account_id="svcacct_e2e",
|
||||
anthropic_workspace_id="wrkspc_e2e",
|
||||
anthropic_federation_workspace_id="wrkspc_e2e",
|
||||
anthropic_identity_token_file=str(token_file),
|
||||
anthropic_identity_token="oidc/env/UNUSED_FALLBACK",
|
||||
)
|
||||
|
|
@ -3503,20 +3503,6 @@ class TestWifExchangeTransportHardening:
|
|||
assert handler.client.follow_redirects is False
|
||||
|
||||
|
||||
class TestWifParamsAreNotClientSettable:
|
||||
def test_every_minting_param_is_server_owned(self):
|
||||
"""Each of these selects which server-side secret is read, or the scope it is minted for.
|
||||
The workspace id was once carved out here as inert; it is not. It is the scope of the
|
||||
minted org credential, and the router merges request kwargs over deployment params, so a
|
||||
caller who set it picked the scope instead of the administrator."""
|
||||
from litellm.proxy.auth.auth_utils import _SERVER_OWNED_WIF_UNCONDITIONAL_BANNED
|
||||
from litellm.types.utils import anthropic_wif_litellm_params, openai_wif_litellm_params
|
||||
|
||||
assert set(_SERVER_OWNED_WIF_UNCONDITIONAL_BANNED) == set(anthropic_wif_litellm_params) | set(
|
||||
openai_wif_litellm_params
|
||||
)
|
||||
|
||||
|
||||
class TestWifServerOwnedParamsAreUnconditional:
|
||||
"""The minting fields choose which server-side secret is read and, with api_base, where it goes,
|
||||
so no client-side credential opt-in may re-enable them."""
|
||||
|
|
@ -3580,32 +3566,19 @@ class TestWifServerOwnedParamsAreUnconditional:
|
|||
|
||||
with pytest.raises(Exception, match="server-owned workload identity federation parameter"):
|
||||
is_request_body_safe(
|
||||
request_body={"model": "claude-sonnet-5", "anthropic_workspace_id": "wrkspc_abc"},
|
||||
general_settings={},
|
||||
llm_router=None,
|
||||
model="claude-sonnet-5",
|
||||
)
|
||||
|
||||
def test_refusal_points_bedrock_callers_at_their_own_spelling(self):
|
||||
"""Banning this spelling must not read as "no workspace selection anywhere": the Bedrock
|
||||
Claude Platform route takes workspace_id/aws_workspace_id, neither of which is a
|
||||
federation parameter, so the error names them."""
|
||||
from litellm.proxy.auth.auth_utils import is_request_body_safe
|
||||
|
||||
with pytest.raises(Exception, match="workspace_id or aws_workspace_id"):
|
||||
is_request_body_safe(
|
||||
request_body={"model": "claude-sonnet-5", "anthropic_workspace_id": "wrkspc_abc"},
|
||||
request_body={"model": "claude-sonnet-5", "anthropic_federation_workspace_id": "wrkspc_abc"},
|
||||
general_settings={},
|
||||
llm_router=None,
|
||||
model="claude-sonnet-5",
|
||||
)
|
||||
|
||||
def test_bedrock_workspace_spellings_are_untouched(self):
|
||||
"""The Bedrock route's own spellings stay settable, which is what keeps this ban from
|
||||
removing a pre-existing capability."""
|
||||
"""The Bedrock Claude Platform route reads its per-request workspace from these three
|
||||
spellings, anthropic_workspace_id included, none of which is a federation parameter; the
|
||||
federation field carries its own name so this pre-existing capability survives the ban."""
|
||||
from litellm.proxy.auth.auth_utils import is_request_body_safe
|
||||
|
||||
for spelling in ("workspace_id", "aws_workspace_id"):
|
||||
for spelling in ("workspace_id", "aws_workspace_id", "anthropic_workspace_id"):
|
||||
assert (
|
||||
is_request_body_safe(
|
||||
request_body={"model": "claude-sonnet-5", spelling: "wrkspc_abc"},
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ WIF_ENV_VARS: Final = (
|
|||
"ANTHROPIC_FEDERATION_RULE_ID",
|
||||
"ANTHROPIC_ORGANIZATION_ID",
|
||||
"ANTHROPIC_SERVICE_ACCOUNT_ID",
|
||||
"ANTHROPIC_WORKSPACE_ID",
|
||||
"ANTHROPIC_FEDERATION_WORKSPACE_ID",
|
||||
"ANTHROPIC_IDENTITY_TOKEN_FILE",
|
||||
"ANTHROPIC_IDENTITY_TOKEN",
|
||||
"ANTHROPIC_IDENTITY_SOURCE",
|
||||
|
|
@ -169,7 +169,7 @@ class TestWireProtocolExact:
|
|||
"anthropic_federation_rule_id": "fdrl_abc123",
|
||||
"anthropic_organization_id": "org-uuid-1",
|
||||
"anthropic_service_account_id": "svcacct_1",
|
||||
"anthropic_workspace_id": "wrkspc_1",
|
||||
"anthropic_federation_workspace_id": "wrkspc_1",
|
||||
"anthropic_identity_token_file": str(token_file),
|
||||
},
|
||||
"https://api.anthropic.com",
|
||||
|
|
@ -385,7 +385,7 @@ class TestResolutionMatrix:
|
|||
monkeypatch.setenv("ANTHROPIC_FEDERATION_RULE_ID", "fdrl_env")
|
||||
monkeypatch.setenv("ANTHROPIC_ORGANIZATION_ID", "org-env")
|
||||
monkeypatch.setenv("ANTHROPIC_SERVICE_ACCOUNT_ID", "svc-env")
|
||||
monkeypatch.setenv("ANTHROPIC_WORKSPACE_ID", "wrkspc_env")
|
||||
monkeypatch.setenv("ANTHROPIC_FEDERATION_WORKSPACE_ID", "wrkspc_env")
|
||||
monkeypatch.setenv("ANTHROPIC_IDENTITY_TOKEN_FILE", "/var/run/secrets/env-token")
|
||||
|
||||
params = resolve_anthropic_wif_params(
|
||||
|
|
@ -393,7 +393,7 @@ class TestResolutionMatrix:
|
|||
"anthropic_federation_rule_id": "fdrl_param",
|
||||
"anthropic_organization_id": "org-param",
|
||||
"anthropic_service_account_id": "svc-param",
|
||||
"anthropic_workspace_id": "wrkspc_param",
|
||||
"anthropic_federation_workspace_id": "wrkspc_param",
|
||||
"anthropic_identity_token_file": "/var/run/secrets/param-token",
|
||||
}
|
||||
)
|
||||
|
|
@ -501,7 +501,7 @@ class TestResolutionMatrix:
|
|||
assert params.assertion_source is not None
|
||||
|
||||
def test_empty_workspace_env_coerced_to_none(self, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setenv("ANTHROPIC_WORKSPACE_ID", "")
|
||||
monkeypatch.setenv("ANTHROPIC_FEDERATION_WORKSPACE_ID", "")
|
||||
params = resolve_anthropic_wif_params(
|
||||
{
|
||||
"anthropic_federation_rule_id": "fdrl_1",
|
||||
|
|
@ -733,7 +733,7 @@ class TestErrorMappingExhaustive:
|
|||
{
|
||||
"anthropic_federation_rule_id": "fdrl_1",
|
||||
"anthropic_organization_id": "org-1",
|
||||
"anthropic_workspace_id": "wrkspc_1",
|
||||
"anthropic_federation_workspace_id": "wrkspc_1",
|
||||
},
|
||||
500,
|
||||
{"error": "server_error."},
|
||||
|
|
@ -776,35 +776,35 @@ class TestDenialHints:
|
|||
def test_500_carries_no_denial_hints(self, monkeypatch: pytest.MonkeyPatch):
|
||||
message = self._raise(self.BASE_PARAMS, 500, monkeypatch)
|
||||
assert "authentication history" not in message
|
||||
assert "ANTHROPIC_WORKSPACE_ID" not in message
|
||||
assert "ANTHROPIC_FEDERATION_WORKSPACE_ID" not in message
|
||||
assert "ANTHROPIC_SERVICE_ACCOUNT_ID" not in message
|
||||
|
||||
def test_hints_name_both_ids_when_both_unset(self, monkeypatch: pytest.MonkeyPatch):
|
||||
message = self._raise(self.BASE_PARAMS, 401, monkeypatch)
|
||||
assert "anthropic_workspace_id" in message
|
||||
assert "ANTHROPIC_WORKSPACE_ID" in message
|
||||
assert "anthropic_federation_workspace_id" in message
|
||||
assert "ANTHROPIC_FEDERATION_WORKSPACE_ID" in message
|
||||
assert "anthropic_service_account_id" in message
|
||||
assert "ANTHROPIC_SERVICE_ACCOUNT_ID" in message
|
||||
assert not message.endswith(".")
|
||||
|
||||
def test_no_workspace_hint_when_workspace_set(self, monkeypatch: pytest.MonkeyPatch):
|
||||
message = self._raise({**self.BASE_PARAMS, "anthropic_workspace_id": "wrkspc_1"}, 401, monkeypatch)
|
||||
assert "ANTHROPIC_WORKSPACE_ID" not in message
|
||||
message = self._raise({**self.BASE_PARAMS, "anthropic_federation_workspace_id": "wrkspc_1"}, 401, monkeypatch)
|
||||
assert "ANTHROPIC_FEDERATION_WORKSPACE_ID" not in message
|
||||
assert "ANTHROPIC_SERVICE_ACCOUNT_ID" in message
|
||||
|
||||
def test_no_service_account_hint_when_service_account_set(self, monkeypatch: pytest.MonkeyPatch):
|
||||
message = self._raise({**self.BASE_PARAMS, "anthropic_service_account_id": "svac_1"}, 401, monkeypatch)
|
||||
assert "ANTHROPIC_SERVICE_ACCOUNT_ID" not in message
|
||||
assert "ANTHROPIC_WORKSPACE_ID" in message
|
||||
assert "ANTHROPIC_FEDERATION_WORKSPACE_ID" in message
|
||||
|
||||
def test_only_console_pointer_when_both_set(self, monkeypatch: pytest.MonkeyPatch):
|
||||
message = self._raise(
|
||||
{**self.BASE_PARAMS, "anthropic_workspace_id": "wrkspc_1", "anthropic_service_account_id": "svac_1"},
|
||||
{**self.BASE_PARAMS, "anthropic_federation_workspace_id": "wrkspc_1", "anthropic_service_account_id": "svac_1"},
|
||||
401,
|
||||
monkeypatch,
|
||||
)
|
||||
assert "authentication history" in message
|
||||
assert "ANTHROPIC_WORKSPACE_ID" not in message
|
||||
assert "ANTHROPIC_FEDERATION_WORKSPACE_ID" not in message
|
||||
assert "ANTHROPIC_SERVICE_ACCOUNT_ID" not in message
|
||||
assert ".." not in message
|
||||
assert not message.endswith(".")
|
||||
|
|
|
|||
|
|
@ -27,19 +27,22 @@ from litellm.proxy.auth.auth_utils import (
|
|||
get_request_route_template,
|
||||
is_request_body_safe,
|
||||
)
|
||||
from litellm.types.workload_identity import ANTHROPIC_WIF_KWARGS_KEYS, OPENAI_WIF_KWARGS_KEYS
|
||||
|
||||
|
||||
def test_every_server_owned_wif_kwarg_key_is_request_banned():
|
||||
"""server_owned_wif_litellm_params (types/utils.py) is derived from ANTHROPIC_WIF_KWARGS_KEYS
|
||||
and OPENAI_WIF_KWARGS_KEYS (types/workload_identity.py) precisely so a new WIF field can never be
|
||||
added to the kwargs funnel
|
||||
without automatically joining the request-body ban list; this guards that invariant itself,
|
||||
independent of today's field count, so it fails if the derivation is ever reverted to a
|
||||
hand-typed list that drifts."""
|
||||
from litellm.proxy.auth.auth_utils import _SERVER_OWNED_WIF_UNCONDITIONAL_BANNED
|
||||
from litellm.types.workload_identity import ANTHROPIC_WIF_KWARGS_KEYS, OPENAI_WIF_KWARGS_KEYS
|
||||
|
||||
assert ANTHROPIC_WIF_KWARGS_KEYS | OPENAI_WIF_KWARGS_KEYS == set(_SERVER_OWNED_WIF_UNCONDITIONAL_BANNED)
|
||||
@pytest.mark.parametrize("param", sorted(ANTHROPIC_WIF_KWARGS_KEYS | OPENAI_WIF_KWARGS_KEYS))
|
||||
def test_every_wif_kwarg_key_is_refused_from_a_request_body(param: str):
|
||||
"""Every key the kwargs funnel carries into litellm_params selects a server-side secret or the
|
||||
scope a token is minted for, so each one must be refused from a request body even with the
|
||||
proxy-wide client-credential opt-in; a key added to the funnel without joining the ban shows up
|
||||
here as a body the proxy accepted."""
|
||||
with pytest.raises(ValueError, match="server-owned workload identity federation parameter"):
|
||||
is_request_body_safe(
|
||||
request_body={"model": "claude-sonnet-5", param: "attacker-chosen"},
|
||||
general_settings={"allow_client_side_credentials": True},
|
||||
llm_router=None,
|
||||
model="claude-sonnet-5",
|
||||
)
|
||||
|
||||
|
||||
class TestCustomAuthCommonChecksWarning:
|
||||
|
|
|
|||
|
|
@ -1081,7 +1081,7 @@ async def test_a_stored_fallback_target_cannot_carry_a_federation_field():
|
|||
with pytest.raises(ValueError, match="server-owned workload identity federation parameter"):
|
||||
await run_async_fallback(
|
||||
litellm_router=FakeRouter(),
|
||||
fallback_model_group=[{"model": "anthropic-backup", "anthropic_workspace_id": "wrkspc_other"}],
|
||||
fallback_model_group=[{"model": "anthropic-backup", "anthropic_federation_workspace_id": "wrkspc_other"}],
|
||||
original_model_group="primary-model",
|
||||
original_exception=RuntimeError("upstream limited request"),
|
||||
max_fallbacks=3,
|
||||
|
|
|
|||
8
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
8
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -29308,6 +29308,8 @@ export interface components {
|
|||
anthropic_disable_workload_identity_federation?: boolean | null;
|
||||
/** Anthropic Federation Rule Id */
|
||||
anthropic_federation_rule_id?: string | null;
|
||||
/** Anthropic Federation Workspace Id */
|
||||
anthropic_federation_workspace_id?: string | null;
|
||||
/** Anthropic Identity Source */
|
||||
anthropic_identity_source?: string | null;
|
||||
/** Anthropic Identity Token */
|
||||
|
|
@ -29338,8 +29340,6 @@ export interface components {
|
|||
anthropic_organization_id?: string | null;
|
||||
/** Anthropic Service Account Id */
|
||||
anthropic_service_account_id?: string | null;
|
||||
/** Anthropic Workspace Id */
|
||||
anthropic_workspace_id?: string | null;
|
||||
/** Api Base */
|
||||
api_base?: string | null;
|
||||
/** Api Key */
|
||||
|
|
@ -39484,6 +39484,8 @@ export interface components {
|
|||
anthropic_disable_workload_identity_federation?: boolean | null;
|
||||
/** Anthropic Federation Rule Id */
|
||||
anthropic_federation_rule_id?: string | null;
|
||||
/** Anthropic Federation Workspace Id */
|
||||
anthropic_federation_workspace_id?: string | null;
|
||||
/** Anthropic Identity Source */
|
||||
anthropic_identity_source?: string | null;
|
||||
/** Anthropic Identity Token */
|
||||
|
|
@ -39514,8 +39516,6 @@ export interface components {
|
|||
anthropic_organization_id?: string | null;
|
||||
/** Anthropic Service Account Id */
|
||||
anthropic_service_account_id?: string | null;
|
||||
/** Anthropic Workspace Id */
|
||||
anthropic_workspace_id?: string | null;
|
||||
/** Api Base */
|
||||
api_base?: string | null;
|
||||
/** Api Key */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue