> ## Documentation Index
> Fetch the complete documentation index at: https://cndoc-langchain.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 设置 LLM 认证代理

> 部署一个基于 Envoy 的认证代理，用于验证 LangSmith 签名的 JWT 并将 LLM 请求路由到您的上游提供商或网关。

LLM 认证代理允许您的组织为来自 LangSmith 的所有模型调用实施自己的认证流程，这样提供商凭据永远不会暴露给最终用户，并且每个请求都可以追溯到特定的操作者。

LLM 认证代理是一个基于 [Envoy](https://www.envoyproxy.io/) 的组件，它在您的环境中运行，位于 LangSmith 和您的上游 LLM 提供商或网关（如 OpenAI、Anthropic 或内部 LLM 网关如 LiteLLM）之间。LangSmith 使用短期 JWT（JSON Web Token）对每个请求进行签名。代理验证 JWT，可选地注入提供商凭据或转换请求和响应主体，然后将请求转发到上游。它对 [SaaS](/langsmith/cloud) 和 [自托管](/langsmith/self-hosted) LangSmith 客户均可用。

<Info>
  LLM 认证代理需要 LangSmith 企业版计划。更多详情，请参阅 [定价](https://www.langchain.com/pricing) 或 [联系我们的销售团队](https://www.langchain.com/contact-sales)。
</Info>

当您需要以下功能时，请使用 LLM 认证代理：

* 对 [Playground](/langsmith/custom-endpoint#use-the-model-in-the-playground) 或 [LLM 作为评判的评估](/langsmith/evaluation) 请求进行身份验证，以对接您自己的提供商网关。
* 注入特定于提供商的 API 密钥或认证头，而不将其暴露给最终用户。
* 转换请求或响应主体（例如，在 OpenAI 格式和自定义网关格式之间转换）。

## 工作原理

来自 LangSmith 的每个请求在代理中经过以下步骤：

1. 验证 JWT（签名、签发者、受众）
2. 调用您的 [`ext_authz`](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/ext_authz_filter) 服务，该服务接收已验证的 JWT 并返回要作为头信息注入的提供商凭据
3. 可选地调用您的 [`ext_proc`](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_proc_filter) 转换器，它可以重写请求和响应主体（例如，在 OpenAI 格式和自定义网关格式之间转换）
4. 将带有自定义头（静态或动态）的请求转发到上游提供商

`ext_authz` 服务和转换器都是客户部署的组件，与代理一起在您的环境中运行。根据您的用例，可以启用其中一个或两个[（取决于您的用例）](#when-to-use-ext_proc-vs-ext_authz)。

<img className="block dark:hidden" src="https://mintcdn.com/other-405835d4/ueBtfwQGQIUkvxdc/langsmith/images/llm-auth-proxy-architecture-light.png?fit=max&auto=format&n=ueBtfwQGQIUkvxdc&q=85&s=3647cbe605d78bbd403aa4d72f14c0e0" alt="架构图显示 LangSmith 向自托管认证代理颁发签名 JWT，代理验证 JWT，应用客户定义的认证，并将请求转发到上游模型提供商。" width="1016" height="1195" data-path="langsmith/images/llm-auth-proxy-architecture-light.png" />

<img className="hidden dark:block" src="https://mintcdn.com/other-405835d4/ueBtfwQGQIUkvxdc/langsmith/images/llm-auth-proxy-architecture-dark.png?fit=max&auto=format&n=ueBtfwQGQIUkvxdc&q=85&s=02f0cb1a7f8dad188324dbe5118e93f5" alt="架构图显示 LangSmith 向自托管认证代理颁发签名 JWT，代理验证 JWT，应用客户定义的认证，并将请求转发到上游模型提供商。" width="1016" height="1195" data-path="langsmith/images/llm-auth-proxy-architecture-dark.png" />

## 先决条件

* LangSmith 企业版计划（SaaS 或版本 0.13.33+ 的自托管）
* 带有 Helm 3 的 Kubernetes 集群
* Envoy v1.37 或更高版本（Helm chart 默认为 `envoyproxy/envoy:v1.37-latest`）
* 您的上游 LLM 提供商或网关的 URL（代理将请求转发到的目标）

<Note>
  认证代理目前支持 [Playground](/langsmith/prompt-engineering-concepts)、[Evals](/langsmith/evaluation)、[Fleet](/langsmith/fleet)、[Polly](/langsmith/polly) 和 [Insights](/langsmith/insights) 功能。
  Playground 和 Evals 在 v0.13.33+ 中可用。Polly 和 Insights 在 v0.13.39+ 中可用。
</Note>

## 1. 配置 JWT 签名（仅限自托管 LangSmith）

对于 LangSmith SaaS，请跳过此步骤。JWT 签名已配置。

**使用 [step CLI](https://smallstep.com/docs/step-cli/installation/)（或您偏好的内部流程）生成 Ed25519 密钥对**。Ed25519 是 LangSmith 用于签署 JWT 的签名算法。私钥对每个请求进行签名；认证代理仅使用公钥验证签名。

```shell theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
TMPDIR_KEYS="$(mktemp -d)"
step crypto keypair "$TMPDIR_KEYS/pub.pem" "$TMPDIR_KEYS/priv.pem" \
  --kty OKP --crv Ed25519 --no-password --insecure
PRIV_JWK=$(step crypto key format --jwk --no-password --insecure < "$TMPDIR_KEYS/priv.pem")
SIGNING_JWKS=$(echo "$PRIV_JWK" | jq -c '{keys: [. + {use: "sig", alg: "EdDSA"}]}')
echo "$SIGNING_JWKS"
```

**将 JWKS 存储在 Kubernetes Secret 中：**

```shell theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
kubectl create secret generic langsmith-signing-jwks \
  --namespace <namespace> \
  --from-literal=LANGSMITH_SIGNING_JWKS="$SIGNING_JWKS"
```

JWKS（JSON Web Key Set）是用于发布加密密钥的标准 JSON 格式。`LANGSMITH_SIGNING_JWKS` 包含 Ed25519 私钥，并作为 Kubernetes Secret 存储。它永远不会被暴露。LangSmith 自动提取相应的公钥并在 `/.well-known/jwks.json` 提供服务。认证代理获取此公共端点以验证 JWT 签名，而无需私钥。

**在您的 [LangSmith `values.yaml`](https://github.com/langchain-ai/helm/blob/main/charts/langsmith/values.yaml) 中引用该 Secret：**

```yaml theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
platformBackend:
  deployment:
    extraEnv:
      - name: LLM_AUTH_PROXY_ISSUER
        value: "langsmith"        # 必须与认证代理 chart 中的 jwtIssuer 匹配
      - secretRef:
          name: langsmith-signing-jwks
```

`LLM_AUTH_PROXY_ISSUER` 设置签名 JWT 中的 `iss` 声明。使用 `langsmith` 以匹配 SaaS 默认值，或使用自定义标识符如 `langsmith:self-hosted:<short_identifier>` 来区分您的安装。该值必须与 [步骤 4](#4-install-the-auth-proxy-helm-chart) 中认证代理 chart 中的 `jwtIssuer` 匹配。

## 2. 为您的组织启用 LLM 认证代理

<Tabs>
  <Tab title="自托管">
    **选项 A：** 为特定组织启用：

    在 LangSmith UI 中，导航到 **Settings** 页面，复制左上角 **Organizations** 旁边的组织 ID。

    针对您的 LangSmith PostgreSQL 数据库运行以下命令：

    ```sql theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
    UPDATE organizations
    SET config = config || '{"can_use_llm_auth_proxy": true}'
    WHERE id = '<organization_id>';
    ```

    **选项 B：** 为安装中的所有组织启用：

    在您的 [LangSmith `values.yaml`](https://github.com/langchain-ai/helm/blob/main/charts/langsmith/values.yaml) 中的 `commonEnv` 添加以下内容：

    ```yaml theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
    commonEnv:
      DEFAULT_ORG_FEATURE_CAN_USE_LLM_AUTH_PROXY: "true"
    ```

    <Note>
      此设置对个人组织无效。
    </Note>
  </Tab>

  <Tab title="SaaS">
    联系 [LangChain 支持](mailto:support@langchain.dev) 为您的组织启用 LLM 认证代理。
  </Tab>
</Tabs>

## 3. 在 LangSmith 中配置组织设置

在 LangSmith UI 中，导航到 **Settings** > **General**，配置以下内容：

1. **JWT 受众：** 代理将验证的 `aud` 声明值（例如 `example-audience`）。这必须与 [步骤 4](#4-install-the-auth-proxy-helm-chart) 中认证代理 chart 中的 `jwtAudiences` 匹配。
2. **启用 LLM 认证代理：** 为您的组织开启此开关。
3. **允许的 URL：** 控制代理被允许将 JWT 转发到的目标 URL。这可以防止凭据被转发到非预期的主机。选择以下三个选项之一：

   * **允许全部**（默认）：允许将 JWT 转发到任何上游 URL。等同于无限制。
   * **阻止全部：** 阻止将 JWT 转发到所有 URL。
   * **自定义：** 指定允许的 URL 模式的显式列表。不接受空字符串和裸 `*`。当 LLM 认证代理开关关闭时，此控件被禁用。

   <img className="block dark:hidden" src="https://mintcdn.com/other-405835d4/ueBtfwQGQIUkvxdc/langsmith/images/llm-auth-proxy-settings-light.png?fit=max&auto=format&n=ueBtfwQGQIUkvxdc&q=85&s=50118b9922c902b869c406f1b2be389d" alt="LangSmith 中的 LLM 认证代理设置，显示启用 LLM 认证代理复选框、JWT 受众字段和允许的 URL 单选按钮，已选择允许全部。" width="518" height="341" data-path="langsmith/images/llm-auth-proxy-settings-light.png" />

   <img className="hidden dark:block" src="https://mintcdn.com/other-405835d4/ueBtfwQGQIUkvxdc/langsmith/images/llm-auth-proxy-settings-dark.png?fit=max&auto=format&n=ueBtfwQGQIUkvxdc&q=85&s=68f10b26260ccdffefd08eaecfc6366d" alt="LangSmith 中的 LLM 认证代理设置，显示启用 LLM 认证代理复选框、JWT 受众字段和允许的 URL 单选按钮，已选择允许全部。" width="506" height="334" data-path="langsmith/images/llm-auth-proxy-settings-dark.png" />

## 4. 安装认证代理 Helm chart

添加 LangChain Helm 仓库：

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
helm repo add langchain https://langchain-ai.github.io/helm/
helm repo update
```

创建一个包含上游 URL 和 JWT 验证设置的 `values.yaml`。JWKS 配置有两个选项：

* **`jwksUri`（推荐）：** 指向您的 LangSmith 实例的 `/.well-known/jwks.json` 端点。Envoy 自动获取和缓存公钥，支持无缝密钥轮换。
* **`jwksJson`（内联）：** 将 JWKS JSON 直接粘贴到 `values.yaml` 中。用于测试或认证代理无法访问 LangSmith 的网络隔离环境。需要更新 chart 来轮换密钥。仅包含公钥组件；省略 `d` 字段（私钥）。

如果两者都设置了，`jwksUri` 优先。

```yaml theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
authProxy:
  upstream: "https://gateway.example.com"
  jwtIssuer: "langsmith" # 必须与 LangSmith values.yaml 中的 LLM_AUTH_PROXY_ISSUER 匹配
  jwtAudiences:
    - "example-audience" # 必须与 LangSmith 中的组织设置匹配

  # 选项 A：远程 JWKS（推荐用于生产环境）
  # Envoy 从 LangSmith 的 /.well-known/jwks.json 获取并缓存公钥。
  jwksUri: "https://langsmith.example.com/.well-known/jwks.json"       # 自托管
  # jwksUri: "https://api.smith.langchain.com/.well-known/jwks.json"   # SaaS
  jwksCacheDurationSeconds: 300

  # 选项 B：内联 JWKS（仅用于测试或网络隔离环境）
  # jwksJson: '{"keys": [{"kty": "OKP", "crv": "Ed25519", "x": "<base64url-public-key>", "use": "sig", "alg": "EdDSA"}]}'
```

安装 chart：

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
helm install langsmith-auth-proxy langchain/langsmith-auth-proxy \
  --namespace <your-namespace> \
  -f values.yaml
```

## 编写 `ext_authz` 服务

当您需要添加、删除或编辑授权头时，使用 `ext_authz`，例如，根据 JWT 中的身份注入提供商 API 密钥。您的服务接收已验证的 JWT 和可选的请求主体，并返回要注入上游的头信息。这使用 Envoy 的 [HTTP `ext_authz` 过滤器](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter)（不是 gRPC）。

在 `values.yaml` 中启用它：

```yaml theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
authProxy:
  extAuthz:
    enabled: true
    serviceUrl: "http://my-auth-service:8080"
    timeout: "10s"
```

### 工作原理

在转发每个请求之前，Envoy 使用与原始请求相同的 HTTP 方法调用您的服务，地址为 `<serviceUrl>/check<original_path>`。您的服务在 `x-langsmith-llm-auth` 头中接收已验证的 JWT。

您的服务返回一个普通的 HTTP 响应：

* **`2xx`：** 允许请求。任何匹配 `allowedUpstreamHeaders` 模式（默认：`authorization` 和 `x-*`）的头都会注入到上游请求中。要在转发前剥离 JWT，请在响应中包含 `x-envoy-auth-headers-to-remove: x-langsmith-llm-auth`。
* **非 `2xx`：** 拒绝请求。状态码和任何匹配 `allowedClientHeaders` 模式（默认：`www-authenticate` 和 `x-*`）的头会返回给客户端。

### 部署选项

您的 `ext_authz` 服务可以以两种方式运行：

* **Sidecar：** 在与代理相同的 Pod 中运行服务。在 `values.yaml` 的 `authProxy.deployment.sidecars` 下添加容器，在 `authProxy.deployment.volumes` 下添加任何所需的卷。使用 `localhost` URL，例如 `http://localhost:10002`。
* **独立部署：** 独立部署服务，并将 `extAuthz.serviceUrl` 指向它。使用集群内 DNS 名称，例如 `http://my-auth-service.my-namespace.svc.cluster.local:8080`，或者如果服务有自己的入口，则使用外部 HTTPS URL。

### 示例部署

下面的示例是一个最小的 Python `ext_authz` 服务，执行 OAuth2 客户端凭据令牌交换。在每个请求上，它返回一个缓存的 `Authorization` 头，其中包含新的访问令牌，并在令牌过期前从配置的令牌端点刷新它。完整示例请参见 chart 仓库中的 [e2e/oauth/](https://github.com/langchain-ai/helm/tree/main/charts/langsmith-auth-proxy/e2e/oauth)。

<Accordion title="ext-authz-oauth.py">
  ```python theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  """执行 OAuth2 客户端凭据令牌交换的 ext_authz 服务。

  作为 sidecar（或独立服务）与主认证代理组件一起运行。
  在每个 ext_authz 检查请求上，它返回一个缓存的 OAuth 访问令牌，
  并在令牌过期时从配置的令牌端点刷新它。

  环境变量：
    OAUTH_TOKEN_URL    – 令牌端点（例如 https://login.example.com/oauth/token）
    OAUTH_CLIENT_ID    – 客户端凭据授予的客户端 ID
    OAUTH_CLIENT_SECRET– 客户端凭据授予的客户端密钥
    OAUTH_SCOPE        – （可选）请求的范围，以空格分隔
    LISTEN_PORT        – （可选）监听端口，默认 10002
  """

  from http.server import HTTPServer, BaseHTTPRequestHandler
  import json
  import os
  import sys
  import threading
  import time
  import urllib.request
  import urllib.parse

  # ---------------------------------------------------------------------------
  # 配置
  # ---------------------------------------------------------------------------
  TOKEN_URL = os.environ["OAUTH_TOKEN_URL"]
  CLIENT_ID = os.environ["OAUTH_CLIENT_ID"]
  CLIENT_SECRET = os.environ["OAUTH_CLIENT_SECRET"]
  SCOPE = os.environ.get("OAUTH_SCOPE", "")
  LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "10002"))

  # 在令牌实际过期前这么多秒刷新令牌。
  EXPIRY_BUFFER_SECONDS = 30

  # ---------------------------------------------------------------------------
  # 令牌缓存（线程安全）
  # ---------------------------------------------------------------------------
  _lock = threading.Lock()
  _cached_token: str | None = None
  _token_expiry: float = 0  # epoch 秒


  def _fetch_token() -> tuple[str, float]:
      """执行客户端凭据授予并返回 (access_token, expiry_epoch)。"""
      data = urllib.parse.urlencode({
          "grant_type": "client_credentials",
          "client_id": CLIENT_ID,
          "client_secret": CLIENT_SECRET,
          **({"scope": SCOPE} if SCOPE else {}),
      }).encode()

      req = urllib.request.Request(
          TOKEN_URL,
          data=data,
          headers={"Content-Type": "application/x-www-form-urlencoded"},
          method="POST",
      )
      with urllib.request.urlopen(req, timeout=10) as resp:
          body = json.loads(resp.read())

      access_token = body["access_token"]
      expires_in = int(body.get("expires_in", 3600))
      expiry = time.time() + expires_in - EXPIRY_BUFFER_SECONDS
      return access_token, expiry


  def get_token() -> str:
      """返回有效的访问令牌，必要时刷新。"""
      global _cached_token, _token_expiry
      with _lock:
          if _cached_token and time.time() < _token_expiry:
              return _cached_token
      # 在锁外获取令牌，这样其他请求不会被 I/O 阻塞。
      token, expiry = _fetch_token()
      with _lock:
          _cached_token = token
          _token_expiry = expiry
      print(f"刷新了 OAuth 令牌（将在 {int(expiry - time.time())} 秒后过期）", flush=True)
      return token


  # ---------------------------------------------------------------------------
  # ext_authz HTTP 处理程序
  # ---------------------------------------------------------------------------
  class Handler(BaseHTTPRequestHandler):
      def do_any(self):
          try:
              token = get_token()
          except Exception as exc:
              print(f"OAuth 令牌获取失败：{exc}", flush=True)
              self.send_response(500)
              self.send_header("Content-Type", "text/plain")
              self.end_headers()
              self.wfile.write(b"OAuth 令牌交换失败")
              return

          self.send_response(200)
          # 根据需要替换头名称 - 此头将被转发到上游 LLM 提供商/网关。
          self.send_header("Authorization", f"Bearer {token}")
          self.end_headers()

      # 处理 Envoy 可能为 ext_authz 检查发送的每种方法。
      do_GET = do_POST = do_PUT = do_DELETE = do_PATCH = do_HEAD = do_OPTIONS = do_any

      def log_message(self, format, *args):
          # 更安静的日志 - 仅打印错误。
          pass


  if __name__ == "__main__":
      server = HTTPServer(("0.0.0.0", LISTEN_PORT), Handler)
      print(f"ext-authz-oauth 正在监听 :{LISTEN_PORT}", flush=True)
      print(f"  token_url={TOKEN_URL} client_id=<已隐藏>", flush=True)
      server.serve_forever()
  ```
</Accordion>

有关 `extAuthz` 参数的完整列表，请参阅 [Helm chart README](https://github.com/langchain-ai/helm/tree/main/charts/langsmith-auth-proxy#readme)。

## 编写 `ext_proc` 转换器

当您需要重写请求或响应主体时，使用 `ext_proc`，例如，在 OpenAI 格式和自定义网关格式之间转换，或向请求负载注入额外字段。这使用 Envoy 的 [`ext_proc` 过滤器](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_proc_filter)。

与 `ext_authz`（HTTP）不同，`ext_proc` 使用双向 gRPC 流。Envoy 在每个处理阶段（请求头、请求体、响应头、响应体）向您的转换器服务发送一条消息，您的服务为每个阶段回复变更。您的转换器必须实现 `envoy.service.ext_proc.v3.ExternalProcessor` gRPC 服务。示例 Go 实现请参见 chart 仓库中的 [e2e/transformer/](https://github.com/langchain-ai/helm/tree/main/charts/langsmith-auth-proxy/e2e/transformer)。

### 何时使用 `ext_proc` 与 `ext_authz`

| 能力    | `ext_authz` | `ext_proc` |
| ----- | ----------- | ---------- |
| 修改请求头 | 是           | 是          |
| 修改响应头 | 否           | 是          |
| 修改请求体 | 否           | 是          |
| 修改响应体 | 否           | 是          |
| 协议    | HTTP        | gRPC       |

如果您只需要注入认证头（例如，用于 API 密钥），请使用 `ext_authz`。如果您需要重写主体，请使用 `ext_proc`。两者可以同时启用。

在 `values.yaml` 中启用 `ext_proc`：

```yaml theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
authProxy:
  transformer:
    enabled: true
    serviceUrl: "grpc://my-transformer:50051"
    timeout: "10s"
    failureModeAllow: false
    processingMode:
      requestHeaderMode: "SEND"
      requestBodyMode: "BUFFERED"
      responseHeaderMode: "SKIP"
      responseBodyMode: "NONE"
```

设置 `failureModeAllow: true` 以在转换器不可用时允许请求通过。默认值（`false`）会拒绝请求。

### 处理模式

通过 `processingMode` 控制哪些阶段发送到您的转换器。仅启用您需要的阶段，因为禁用未使用的阶段可以减少延迟。

| 字段                    | 选项                                                 | 描述        |
| --------------------- | -------------------------------------------------- | --------- |
| `requestHeaderMode`   | `SEND`, `SKIP`, `DEFAULT`                          | 是否转发请求头。  |
| `responseHeaderMode`  | `SEND`, `SKIP`, `DEFAULT`                          | 是否转发响应头。  |
| `requestBodyMode`     | `NONE`, `BUFFERED`, `STREAMED`, `BUFFERED_PARTIAL` | 如何发送请求体。  |
| `responseBodyMode`    | `NONE`, `BUFFERED`, `STREAMED`, `BUFFERED_PARTIAL` | 如何发送响应体。  |
| `requestTrailerMode`  | `SEND`, `SKIP`                                     | 是否转发请求尾部。 |
| `responseTrailerMode` | `SEND`, `SKIP`                                     | 是否转发响应尾部。 |

* 对于请求体重写，使用 `BUFFERED`：在发送前缓冲完整主体，对于 JSON 重写最简单。
* 对于流式 LLM 响应体重写，使用 `STREAMED`：在数据块到达时发送，延迟更低但实现更复杂。
* 使用 `NONE` 完全跳过某个阶段。

<Warning>
  当修改主体时，您的 `ext_proc` 服务还必须通过 `HeaderMutation` 更新 `content-length` 头以匹配新的主体大小。Envoy 会拒绝 `content-length` 与修改后主体不匹配的响应。
</Warning>

### 请求流程

启用 `ext_proc` 进行头注入和主体重写的示例：

```
curl -H "X-LangSmith-LLM-Auth: <JWT>" -d '{"model":"gpt-4",...}'
  -> Envoy(:10000)
  -> 内置 Envoy JWT 过滤器（验证签名、iss、aud）
  -> `ext_proc` 过滤器 -> transformer:50051 (gRPC)
    <- 阶段 1: request_headers -> 修改头（注入 Authorization）
    <- 阶段 2: request_body   -> 修改主体（重写 JSON）+ 更新 content-length
  -> 上游 LLM 提供商或网关
```

### 示例部署

下面的示例将一个最小的 Go 转换器部署为 Kubernetes Deployment。它从请求头中读取 JWT，注入 `Authorization` 头，并将请求体从 OpenAI 格式重写为自定义格式。

<Accordion title="transformer-configmap.yaml">
  ```yaml theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  apiVersion: v1
  kind: ConfigMap
  metadata:
    name: transformer-source
  data:
    main.go: |
      package main

      import (
          "encoding/json"
          "fmt"
          "io"
          "log"
          "net"
          "strings"

          core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
          ext_proc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
          "google.golang.org/grpc"
      )

      type server struct {
          ext_proc.UnimplementedExternalProcessorServer
      }

      func (s *server) Process(stream ext_proc.ExternalProcessor_ProcessServer) error {
          for {
              req, err := stream.Recv()
              if err == io.EOF {
                  return nil
              }
              if err != nil {
                  return err
              }

              var resp *ext_proc.ProcessingResponse
              switch v := req.Request.(type) {
              case *ext_proc.ProcessingRequest_RequestHeaders:
                  resp = handleRequestHeaders(v.RequestHeaders)
              case *ext_proc.ProcessingRequest_RequestBody:
                  resp = handleRequestBody(v.RequestBody)
              default:
                  resp = &ext_proc.ProcessingResponse{}
              }

              if err := stream.Send(resp); err != nil {
                  return err
              }
          }
      }

      func handleRequestHeaders(headers *ext_proc.HttpHeaders) *ext_proc.ProcessingResponse {
          var jwtValue string
          for _, h := range headers.Headers.Headers {
              if strings.EqualFold(h.Key, "x-langsmith-llm-auth") {
                  if len(h.RawValue) > 0 {
                      jwtValue = string(h.RawValue)
                  } else {
                      jwtValue = h.Value
                  }
                  break
              }
          }

          resp := &ext_proc.ProcessingResponse{
              Response: &ext_proc.ProcessingResponse_RequestHeaders{
                  RequestHeaders: &ext_proc.HeadersResponse{},
              },
          }

          if jwtValue != "" {
              // TODO：替换为您的认证逻辑，例如将 JWT 交换为
              // 特定于提供商的令牌，调用密钥管理器等。
              providerKey := "Bearer your-provider-key"

              headerResp := resp.GetRequestHeaders()
              headerResp.Response = &ext_proc.CommonResponse{
                  HeaderMutation: &ext_proc.HeaderMutation{
                      SetHeaders: []*core.HeaderValueOption{
                          {
                              Header: &core.HeaderValue{
                                  Key:      "Authorization",
                                  RawValue: []byte(providerKey),
                              },
                          },
                      },
                  },
              }
          }
          return resp
      }

      func handleRequestBody(body *ext_proc.HttpBody) *ext_proc.ProcessingResponse {
          resp := &ext_proc.ProcessingResponse{
              Response: &ext_proc.ProcessingResponse_RequestBody{
                  RequestBody: &ext_proc.BodyResponse{},
              },
          }

          var original map[string]interface{}
          if err := json.Unmarshal(body.Body, &original); err != nil {
              log.Printf("主体解析失败，直接传递：%v", err)
              return resp
          }

          // TODO：替换为您的转换逻辑。
          // 此示例将 OpenAI 格式的主体包装在自定义信封中。
          transformed := map[string]interface{}{
              "custom_model":    original["model"],
              "custom_messages": original["messages"],
              "metadata":        map[string]string{"source": "langsmith"},
          }

          newBody, err := json.Marshal(transformed)
          if err != nil {
              log.Printf("主体序列化失败，直接传递：%v", err)
              return resp
          }

          // 重要：更新 content-length 以匹配新的主体大小。
          bodyResp := resp.GetRequestBody()
          bodyResp.Response = &ext_proc.CommonResponse{
              Status: ext_proc.CommonResponse_CONTINUE_AND_REPLACE,
              HeaderMutation: &ext_proc.HeaderMutation{
                  SetHeaders: []*core.HeaderValueOption{
                      {
                          Header: &core.HeaderValue{
                              Key:      "content-length",
                              RawValue: []byte(fmt.Sprintf("%d", len(newBody))),
                          },
                      },
                  },
              },
              BodyMutation: &ext_proc.BodyMutation{
                  Mutation: &ext_proc.BodyMutation_Body{
                      Body: newBody,
                  },
              },
          }
          return resp
      }

      func main() {
          lis, err := net.Listen("tcp", ":50051")
          if err != nil {
              log.Fatalf("监听失败：%v", err)
          }
          s := grpc.NewServer()
          ext_proc.RegisterExternalProcessorServer(s, &server{})
          log.Println("转换器正在监听 :50051")
          if err := s.Serve(lis); err != nil {
              log.Fatalf("服务失败：%v", err)
          }
      }
    go.mod: |
      module transformer

      go 1.23

      require (
          github.com/envoyproxy/go-control-plane/envoy v1.32.4
          google.golang.org/grpc v1.72.1
      )
  ```
</Accordion>

<Accordion title="transformer-deployment.yaml">
  ```yaml theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: transformer
    labels:
      app: transformer
  spec:
    replicas: 1
    selector:
      matchLabels:
        app: transformer
    template:
      metadata:
        labels:
          app: transformer
      spec:
        initContainers:
          - name: build
            image: golang:1.23
            command: ["sh", "-c"]
            args:
              - |
                cp /src/main.go /src/go.mod /build/ &&
                cd /build &&
                go mod tidy &&
                CGO_ENABLED=0 go build -o /build/transformer ./main.go
            volumeMounts:
              - name: source
                mountPath: /src
                readOnly: true
              - name: binary
                mountPath: /build
        containers:
          - name: transformer
            image: gcr.io/distroless/static-debian12:nonroot
            command: ["/app/transformer"]
            ports:
              - containerPort: 50051
            volumeMounts:
              - name: binary
                mountPath: /app
                readOnly: true
        volumes:
          - name: source
            configMap:
              name: transformer-source
          - name: binary
            emptyDir: {}
  ---
  apiVersion: v1
  kind: Service
  metadata:
    name: transformer
    labels:
      app: transformer
  spec:
    selector:
      app: transformer
    ports:
      - port: 50051
        targetPort: 50051
        protocol: TCP
  ```
</Accordion>

<Note>
  对于生产环境，请预先构建容器镜像，而不是在 init 容器中编译。示例多阶段构建请参见 [Helm chart 仓库](https://github.com/langchain-ai/helm/tree/main/charts/langsmith-auth-proxy) 中的 `e2e/transformer/Dockerfile`。
</Note>

## 附加配置

### HTTP 代理

Envoy 不遵循 `HTTP_PROXY`、`HTTPS_PROXY` 或 `NO_PROXY` 环境变量。请显式配置 HTTP 代理：

```yaml theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
authProxy:
  httpProxy:
    enabled: true
    host: "proxy.example.com"
    port: 3128
    noProxy:
      - "internal.corp"
      - ".internal.corp"
```

### 其他选项

有关入口、自动扩缩、资源限制和其他配置选项，请参阅 [Helm chart README](https://github.com/langchain-ai/helm/tree/main/charts/langsmith-auth-proxy#readme)。

<Tip>
  为了生产可靠性，请将 `authProxy.autoscaling.hpa.minReplicas` 设置为至少 `3`。
</Tip>

## 完整配置示例

```yaml theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
authProxy:
  upstream: "https://gateway.example.com"   # 您的 LLM 网关或提供商
  jwtIssuer: "langsmith"                    # 必须与 LangSmith 上的 LLM_AUTH_PROXY_ISSUER 匹配
  jwtAudiences:
    - "example-audience"                    # 必须与 LangSmith 中的组织设置匹配

  # 选项 A：远程 JWKS（推荐用于生产环境）
  # Envoy 从 LangSmith 的 /.well-known/jwks.json 端点获取并缓存公钥。
  jwksUri: "https://langsmith.example.com/.well-known/jwks.json"   # 自托管
  # jwksUri: "https://api.smith.langchain.com/.well-known/jwks.json"  # SaaS
  jwksCacheDurationSeconds: 300             # Envoy 缓存 JWKS 的时间（默认 5 分钟）

  # 选项 B：内联 JWKS（仅用于测试或网络隔离环境）
  # jwksJson: '{"keys": [...]}'

  # ext_authz：仅头认证逻辑（仅在需要时包含）
  # 用于注入、删除或修改授权头。
  # 您的服务在 /check 接收一个 HTTP 请求，其中包含 x-langsmith-llm-auth 头中的已验证 JWT，
  # 并响应要注入上游的头。
  extAuthz:
    enabled: true
    serviceUrl: "http://localhost:10002"    # sidecar URL
    # serviceUrl: "http://ext-authz.<namespace>.svc.cluster.local:10002"  # 独立部署
    sendBody: false                         # 设置为 true 以包含请求体

  # transformer：请求/响应体转换（仅在需要时包含）
  # 当您需要重写请求或响应体时使用此选项（例如 OpenAI -> 自定义格式）。
  # 可以与 ext_authz 同时启用。
  transformer:
    enabled: true
    serviceUrl: "grpc://transformer.<namespace>.svc.cluster.local:50051"
    timeout: "10s"
    failureModeAllow: false                 # 如果转换器不可用则拒绝
    processingMode:
      requestHeaderMode: "SEND"             # 转发请求头（读取 JWT，注入认证）
      responseHeaderMode: "SKIP"            # 跳过响应头
      requestBodyMode: "BUFFERED"           # 缓冲完整主体以进行 JSON 重写
      responseBodyMode: "NONE"              # 跳过响应体
      requestTrailerMode: "SKIP"
      responseTrailerMode: "SKIP"
```

## JWT 声明参考

LangSmith 使用 **Ed25519 (EdDSA)** 签署 JWT。公钥在 `/.well-known/jwks.json` 提供服务，并由代理自动获取。认证代理使用这些公钥验证签名。

| 声明                         | 描述                                                      |
| -------------------------- | ------------------------------------------------------- |
| `iat`, `exp`, `jti`, `nbf` | 标准 JWT 声明（签发时间、过期时间、JWT ID、生效时间）                        |
| `iss`                      | 签发者。SaaS 为 `langsmith`；自托管通过 `LLM_AUTH_PROXY_ISSUER` 设置 |
| `aud`                      | 受众。与 LangSmith 组织设置中的 JWT 受众匹配                          |
| `sub`                      | 操作者标识符（用户 ID、评估者 ID、助手 ID 或 API 密钥 ID）                  |
| `actor_type`               | 以下之一：`user`、`evaluator`、`agent-builder`、`api_key`       |
| `workspace_id`             | 工作区 ID                                                  |
| `workspace_name`           | 工作区名称                                                   |
| `organization_id`          | 组织 ID                                                   |
| `organization_name`        | 组织名称                                                    |
| `request_id`               | 请求关联 ID                                                 |
| `ls_user_id`               | LangSmith 用户 ID（仅当 `actor_type` 为 `user` 时存在）           |

JWT 通过 `x-langsmith-llm-auth` 请求头传递给您的 `ext_authz` 或转换器服务。

## 常见问题

<Accordion title="认证代理是否支持企业代理？">
  是。通过 `values.yaml` 中的 `httpProxy` 部分配置 HTTP 代理。详情请参阅 [HTTP 代理](#http-proxy)。
</Accordion>

<Accordion title="认证代理是否支持自定义证书？">
  是，通过 `customCa` 用于自定义 CA 证书，通过 `mtls` 用于双向 TLS。
</Accordion>

<Accordion title="单个认证代理是否可以路由到多个上游 LLM 网关？">
  否。认证代理只有一个 `upstream` 字段。
</Accordion>

<Accordion title="认证代理是否可以为多个组织提供服务？">
  是。多个组织可以通过其在 LangSmith 中的模型配置指向同一个认证代理实例。
</Accordion>

<Accordion title="LangSmith 到认证代理的连接是否可以使用 HTTP 而不是 HTTPS？">
  可以，但仅限于自托管，我们通常建议将认证代理放在专用入口后面，以便通信使用 HTTPS。要允许 HTTP，请在您的 [LangSmith `values.yaml`](https://github.com/langchain-ai/helm/blob/main/charts/langsmith/values.yaml) 中的 `commonEnv` 和 `playground.deployment.extraEnv` 添加 `LLM_AUTH_PROXY_ACCEPT_HTTP`。
  要为 [Polly 和 Insights](/langsmith/deploy-self-hosted-full-platform#enable-fleet-insights-and-polly) 启用到认证代理的 HTTP 流量，请在相应的 `extraEnv` 部分设置此环境变量：`config.polly.agent.extraEnv` 和 `config.insights.agent.extraEnv`。
</Accordion>

## Helm chart 参考

有关可配置值的完整列表，请参阅 [Helm chart README](https://github.com/langchain-ai/helm/tree/main/charts/langsmith-auth-proxy)。

***

<div className="source-links">
  <Callout icon="terminal-2">
    [通过 MCP 将这些文档](/use-these-docs) 连接到 Claude、VSCode 等，以获取实时答案。
  </Callout>

  <Callout icon="edit">
    [在 GitHub 上编辑此页面](https://github.com/langchain-ai/docs/edit/main/src/langsmith/llm-auth-proxy-self-hosted.mdx) 或 [提交问题](https://github.com/langchain-ai/docs/issues/new/choose)。
  </Callout>
</div>
