mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-22 05:24:40 +00:00
feat(proxy): serve Relay managed config from a dedicated YAML file
Add GET /relay/managed-config so Relay devices can pull an admin-owned Claude Code policy (pinned version, model, managed settings) from the Gateway. The policy lives in a standalone YAML file located by LITELLM_RELAY_SETTINGS_PATH (default relay_settings.yaml), separate from the proxy config.yaml, and is re-read on each request so edits apply on the next sync. The endpoint requires existing LiteLLM auth. Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>
This commit is contained in:
parent
6a213de9f4
commit
383b12d0cf
8 changed files with 366 additions and 0 deletions
|
|
@ -212,6 +212,11 @@ LAZY_FEATURES: Tuple[LazyFeature, ...] = (
|
|||
path_prefixes=("/claude-code",),
|
||||
register_fn=_include_router("claude_code_marketplace_router"),
|
||||
),
|
||||
LazyFeature(
|
||||
name="relay",
|
||||
module_path="litellm.proxy.relay_endpoints.endpoints",
|
||||
path_prefixes=("/relay",),
|
||||
),
|
||||
LazyFeature(
|
||||
name="scim",
|
||||
module_path="litellm.proxy.management_endpoints.scim.scim_v2",
|
||||
|
|
|
|||
|
|
@ -23826,6 +23826,133 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"relay": {
|
||||
"components": {
|
||||
"schemas": {
|
||||
"JsonValue": {},
|
||||
"RelayClaudeCodeConfig": {
|
||||
"properties": {
|
||||
"channel": {
|
||||
"default": "pinned",
|
||||
"title": "Channel",
|
||||
"type": "string"
|
||||
},
|
||||
"managed_settings": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/components/schemas/JsonValue"
|
||||
},
|
||||
"title": "Managed Settings",
|
||||
"type": "object"
|
||||
},
|
||||
"model": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Model"
|
||||
},
|
||||
"package": {
|
||||
"default": "@anthropic-ai/claude-code",
|
||||
"title": "Package",
|
||||
"type": "string"
|
||||
},
|
||||
"registry": {
|
||||
"default": "npm",
|
||||
"title": "Registry",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Version"
|
||||
}
|
||||
},
|
||||
"title": "RelayClaudeCodeConfig",
|
||||
"type": "object"
|
||||
},
|
||||
"RelayManagedConfigResponse": {
|
||||
"properties": {
|
||||
"claude_code": {
|
||||
"$ref": "#/components/schemas/RelayClaudeCodeConfig"
|
||||
},
|
||||
"policy_version": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Policy Version"
|
||||
},
|
||||
"updated_at": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Updated At"
|
||||
},
|
||||
"updated_by": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Updated By"
|
||||
}
|
||||
},
|
||||
"title": "RelayManagedConfigResponse",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"/relay/managed-config": {
|
||||
"get": {
|
||||
"operationId": "get_relay_managed_config_relay_managed_config_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RelayManagedConfigResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"APIKeyHeader": []
|
||||
}
|
||||
],
|
||||
"summary": "Get Relay Managed Config",
|
||||
"tags": [
|
||||
"relay"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scim": {
|
||||
"components": {
|
||||
"schemas": {
|
||||
|
|
|
|||
0
litellm/proxy/relay_endpoints/__init__.py
Normal file
0
litellm/proxy/relay_endpoints/__init__.py
Normal file
49
litellm/proxy/relay_endpoints/endpoints.py
Normal file
49
litellm/proxy/relay_endpoints/endpoints.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.types.proxy.relay_endpoints import RelayManagedConfigResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
RELAY_SETTINGS_PATH_ENV = "LITELLM_RELAY_SETTINGS_PATH"
|
||||
DEFAULT_RELAY_SETTINGS_PATH = "relay_settings.yaml"
|
||||
|
||||
|
||||
def _relay_settings_path() -> Path:
|
||||
return Path(os.getenv(RELAY_SETTINGS_PATH_ENV, DEFAULT_RELAY_SETTINGS_PATH))
|
||||
|
||||
|
||||
def _load_managed_config(path: Path) -> RelayManagedConfigResponse:
|
||||
if not path.exists():
|
||||
return RelayManagedConfigResponse()
|
||||
|
||||
try:
|
||||
parsed = yaml.safe_load(path.read_text())
|
||||
except yaml.YAMLError as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": f"Failed to parse Relay settings at {path}: {e}"},
|
||||
)
|
||||
|
||||
if parsed is None:
|
||||
return RelayManagedConfigResponse()
|
||||
if not isinstance(parsed, dict):
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": f"Relay settings at {path} must be a YAML mapping, got {type(parsed).__name__}"},
|
||||
)
|
||||
return RelayManagedConfigResponse.model_validate(parsed)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/relay/managed-config",
|
||||
tags=["LiteLLM Relay"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=RelayManagedConfigResponse,
|
||||
)
|
||||
async def get_relay_managed_config() -> RelayManagedConfigResponse:
|
||||
return _load_managed_config(_relay_settings_path())
|
||||
19
litellm/types/proxy/relay_endpoints.py
Normal file
19
litellm/types/proxy/relay_endpoints.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from typing import Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, JsonValue
|
||||
|
||||
|
||||
class RelayClaudeCodeConfig(BaseModel):
|
||||
channel: str = Field(default="pinned")
|
||||
version: Optional[str] = Field(default=None)
|
||||
registry: str = Field(default="npm")
|
||||
package: str = Field(default="@anthropic-ai/claude-code")
|
||||
model: Optional[str] = Field(default=None)
|
||||
managed_settings: Dict[str, JsonValue] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class RelayManagedConfigResponse(BaseModel):
|
||||
claude_code: RelayClaudeCodeConfig = Field(default_factory=RelayClaudeCodeConfig)
|
||||
policy_version: Optional[int] = Field(default=None)
|
||||
updated_by: Optional[str] = Field(default=None)
|
||||
updated_at: Optional[str] = Field(default=None)
|
||||
0
tests/test_litellm/proxy/relay_endpoints/__init__.py
Normal file
0
tests/test_litellm/proxy/relay_endpoints/__init__.py
Normal file
92
tests/test_litellm/proxy/relay_endpoints/test_endpoints.py
Normal file
92
tests/test_litellm/proxy/relay_endpoints/test_endpoints.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm.proxy.relay_endpoints.endpoints import _load_managed_config
|
||||
|
||||
|
||||
def test_load_managed_config_parses_pinned_version(tmp_path: Path) -> None:
|
||||
path = tmp_path / "relay_settings.yaml"
|
||||
path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"claude_code:",
|
||||
" channel: pinned",
|
||||
" version: 1.2.3",
|
||||
" registry: npm",
|
||||
" model: claude-sonnet-4-5",
|
||||
"policy_version: 7",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
config = _load_managed_config(path)
|
||||
|
||||
assert config.claude_code.version == "1.2.3"
|
||||
assert config.claude_code.channel == "pinned"
|
||||
assert config.claude_code.registry == "npm"
|
||||
assert config.claude_code.model == "claude-sonnet-4-5"
|
||||
assert config.policy_version == 7
|
||||
|
||||
|
||||
def test_load_managed_config_missing_file_returns_empty_policy(tmp_path: Path) -> None:
|
||||
config = _load_managed_config(tmp_path / "does_not_exist.yaml")
|
||||
|
||||
assert config.claude_code.version is None
|
||||
assert config.policy_version is None
|
||||
|
||||
|
||||
def test_load_managed_config_empty_file_returns_empty_policy(tmp_path: Path) -> None:
|
||||
path = tmp_path / "relay_settings.yaml"
|
||||
path.write_text("")
|
||||
|
||||
config = _load_managed_config(path)
|
||||
|
||||
assert config.claude_code.version is None
|
||||
|
||||
|
||||
def test_load_managed_config_rejects_non_mapping(tmp_path: Path) -> None:
|
||||
path = tmp_path / "relay_settings.yaml"
|
||||
path.write_text("- just\n- a\n- list\n")
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_load_managed_config(path)
|
||||
|
||||
assert exc_info.value.status_code == 500
|
||||
|
||||
|
||||
def test_load_managed_config_rejects_invalid_yaml(tmp_path: Path) -> None:
|
||||
path = tmp_path / "relay_settings.yaml"
|
||||
path.write_text("claude_code: {version: '1.2.3'\n")
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_load_managed_config(path)
|
||||
|
||||
assert exc_info.value.status_code == 500
|
||||
|
||||
|
||||
def test_load_managed_config_preserves_nested_managed_settings(tmp_path: Path) -> None:
|
||||
path = tmp_path / "relay_settings.yaml"
|
||||
path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"claude_code:",
|
||||
" version: 2.1.208",
|
||||
" managed_settings:",
|
||||
" env:",
|
||||
" ANTHROPIC_MODEL: claude-sonnet-4-5",
|
||||
" permissions:",
|
||||
" defaultMode: acceptEdits",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
config = _load_managed_config(path)
|
||||
|
||||
assert config.claude_code.managed_settings["env"] == {
|
||||
"ANTHROPIC_MODEL": "claude-sonnet-4-5"
|
||||
}
|
||||
assert config.claude_code.managed_settings["permissions"] == {
|
||||
"defaultMode": "acceptEdits"
|
||||
}
|
||||
74
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
74
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -11071,6 +11071,23 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/relay/managed-config": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get Relay Managed Config */
|
||||
get: operations["get_relay_managed_config_relay_managed_config_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/reload/anthropic_beta_headers": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -24541,6 +24558,7 @@ export interface components {
|
|||
/** Updated By */
|
||||
updated_by?: string | null;
|
||||
};
|
||||
JsonValue: unknown;
|
||||
/** KeyHealthResponse */
|
||||
KeyHealthResponse: {
|
||||
/**
|
||||
|
|
@ -30192,6 +30210,42 @@ export interface components {
|
|||
/** Review Notes */
|
||||
review_notes?: string | null;
|
||||
};
|
||||
/** RelayClaudeCodeConfig */
|
||||
RelayClaudeCodeConfig: {
|
||||
/**
|
||||
* Channel
|
||||
* @default pinned
|
||||
*/
|
||||
channel: string;
|
||||
/** Managed Settings */
|
||||
managed_settings?: {
|
||||
[key: string]: components["schemas"]["JsonValue"];
|
||||
};
|
||||
/** Model */
|
||||
model?: string | null;
|
||||
/**
|
||||
* Package
|
||||
* @default @anthropic-ai/claude-code
|
||||
*/
|
||||
package: string;
|
||||
/**
|
||||
* Registry
|
||||
* @default npm
|
||||
*/
|
||||
registry: string;
|
||||
/** Version */
|
||||
version?: string | null;
|
||||
};
|
||||
/** RelayManagedConfigResponse */
|
||||
RelayManagedConfigResponse: {
|
||||
claude_code?: components["schemas"]["RelayClaudeCodeConfig"];
|
||||
/** Policy Version */
|
||||
policy_version?: number | null;
|
||||
/** Updated At */
|
||||
updated_at?: string | null;
|
||||
/** Updated By */
|
||||
updated_by?: string | null;
|
||||
};
|
||||
/** ResetSpendRequest */
|
||||
ResetSpendRequest: {
|
||||
/** Reset To */
|
||||
|
|
@ -47451,6 +47505,26 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
get_relay_managed_config_relay_managed_config_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["RelayManagedConfigResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
reload_anthropic_beta_headers_reload_anthropic_beta_headers_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue