Skip to main content
LLM 认证代理允许您的组织为来自 LangSmith 的所有模型调用实施自己的认证流程,这样提供商凭据永远不会暴露给最终用户,并且每个请求都可以追溯到特定的操作者。 LLM 认证代理是一个基于 Envoy 的组件,它在您的环境中运行,位于 LangSmith 和您的上游 LLM 提供商或网关(如 OpenAI、Anthropic 或内部 LLM 网关如 LiteLLM)之间。LangSmith 使用短期 JWT(JSON Web Token)对每个请求进行签名。代理验证 JWT,可选地注入提供商凭据或转换请求和响应主体,然后将请求转发到上游。它对 SaaS自托管 LangSmith 客户均可用。
LLM 认证代理需要 LangSmith 企业版计划。更多详情,请参阅 定价联系我们的销售团队
当您需要以下功能时,请使用 LLM 认证代理:
  • PlaygroundLLM 作为评判的评估 请求进行身份验证,以对接您自己的提供商网关。
  • 注入特定于提供商的 API 密钥或认证头,而不将其暴露给最终用户。
  • 转换请求或响应主体(例如,在 OpenAI 格式和自定义网关格式之间转换)。

工作原理

来自 LangSmith 的每个请求在代理中经过以下步骤:
  1. 验证 JWT(签名、签发者、受众)
  2. 调用您的 ext_authz 服务,该服务接收已验证的 JWT 并返回要作为头信息注入的提供商凭据
  3. 可选地调用您的 ext_proc 转换器,它可以重写请求和响应主体(例如,在 OpenAI 格式和自定义网关格式之间转换)
  4. 将带有自定义头(静态或动态)的请求转发到上游提供商
ext_authz 服务和转换器都是客户部署的组件,与代理一起在您的环境中运行。根据您的用例,可以启用其中一个或两个(取决于您的用例) 架构图显示 LangSmith 向自托管认证代理颁发签名 JWT,代理验证 JWT,应用客户定义的认证,并将请求转发到上游模型提供商。

先决条件

  • LangSmith 企业版计划(SaaS 或版本 0.13.33+ 的自托管)
  • 带有 Helm 3 的 Kubernetes 集群
  • Envoy v1.37 或更高版本(Helm chart 默认为 envoyproxy/envoy:v1.37-latest
  • 您的上游 LLM 提供商或网关的 URL(代理将请求转发到的目标)
认证代理目前支持 PlaygroundEvalsFleetPollyInsights 功能。 Playground 和 Evals 在 v0.13.33+ 中可用。Polly 和 Insights 在 v0.13.39+ 中可用。

1. 配置 JWT 签名(仅限自托管 LangSmith)

对于 LangSmith SaaS,请跳过此步骤。JWT 签名已配置。 使用 step CLI(或您偏好的内部流程)生成 Ed25519 密钥对。Ed25519 是 LangSmith 用于签署 JWT 的签名算法。私钥对每个请求进行签名;认证代理仅使用公钥验证签名。
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 中:
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 中引用该 Secret:
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 中认证代理 chart 中的 jwtIssuer 匹配。

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

选项 A: 为特定组织启用:在 LangSmith UI 中,导航到 Settings 页面,复制左上角 Organizations 旁边的组织 ID。针对您的 LangSmith PostgreSQL 数据库运行以下命令:
UPDATE organizations
SET config = config || '{"can_use_llm_auth_proxy": true}'
WHERE id = '<organization_id>';
选项 B: 为安装中的所有组织启用:在您的 LangSmith values.yaml 中的 commonEnv 添加以下内容:
commonEnv:
  DEFAULT_ORG_FEATURE_CAN_USE_LLM_AUTH_PROXY: "true"
此设置对个人组织无效。

3. 在 LangSmith 中配置组织设置

在 LangSmith UI 中,导航到 Settings > General,配置以下内容:
  1. JWT 受众: 代理将验证的 aud 声明值(例如 example-audience)。这必须与 步骤 4 中认证代理 chart 中的 jwtAudiences 匹配。
  2. 启用 LLM 认证代理: 为您的组织开启此开关。
  3. 允许的 URL: 控制代理被允许将 JWT 转发到的目标 URL。这可以防止凭据被转发到非预期的主机。选择以下三个选项之一:
    • 允许全部(默认):允许将 JWT 转发到任何上游 URL。等同于无限制。
    • 阻止全部: 阻止将 JWT 转发到所有 URL。
    • 自定义: 指定允许的 URL 模式的显式列表。不接受空字符串和裸 *。当 LLM 认证代理开关关闭时,此控件被禁用。
    LangSmith 中的 LLM 认证代理设置,显示启用 LLM 认证代理复选框、JWT 受众字段和允许的 URL 单选按钮,已选择允许全部。

4. 安装认证代理 Helm chart

添加 LangChain Helm 仓库:
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 优先。
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:
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 过滤器(不是 gRPC)。 values.yaml 中启用它:
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 模式(默认:authorizationx-*)的头都会注入到上游请求中。要在转发前剥离 JWT,请在响应中包含 x-envoy-auth-headers-to-remove: x-langsmith-llm-auth
  • 2xx 拒绝请求。状态码和任何匹配 allowedClientHeaders 模式(默认:www-authenticatex-*)的头会返回给客户端。

部署选项

您的 ext_authz 服务可以以两种方式运行:
  • Sidecar: 在与代理相同的 Pod 中运行服务。在 values.yamlauthProxy.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/
"""执行 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()
有关 extAuthz 参数的完整列表,请参阅 Helm chart README

编写 ext_proc 转换器

当您需要重写请求或响应主体时,使用 ext_proc,例如,在 OpenAI 格式和自定义网关格式之间转换,或向请求负载注入额外字段。这使用 Envoy 的 ext_proc 过滤器 ext_authz(HTTP)不同,ext_proc 使用双向 gRPC 流。Envoy 在每个处理阶段(请求头、请求体、响应头、响应体)向您的转换器服务发送一条消息,您的服务为每个阶段回复变更。您的转换器必须实现 envoy.service.ext_proc.v3.ExternalProcessor gRPC 服务。示例 Go 实现请参见 chart 仓库中的 e2e/transformer/

何时使用 ext_procext_authz

能力ext_authzext_proc
修改请求头
修改响应头
修改请求体
修改响应体
协议HTTPgRPC
如果您只需要注入认证头(例如,用于 API 密钥),请使用 ext_authz。如果您需要重写主体,请使用 ext_proc。两者可以同时启用。 values.yaml 中启用 ext_proc
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 控制哪些阶段发送到您的转换器。仅启用您需要的阶段,因为禁用未使用的阶段可以减少延迟。
字段选项描述
requestHeaderModeSEND, SKIP, DEFAULT是否转发请求头。
responseHeaderModeSEND, SKIP, DEFAULT是否转发响应头。
requestBodyModeNONE, BUFFERED, STREAMED, BUFFERED_PARTIAL如何发送请求体。
responseBodyModeNONE, BUFFERED, STREAMED, BUFFERED_PARTIAL如何发送响应体。
requestTrailerModeSEND, SKIP是否转发请求尾部。
responseTrailerModeSEND, SKIP是否转发响应尾部。
  • 对于请求体重写,使用 BUFFERED:在发送前缓冲完整主体,对于 JSON 重写最简单。
  • 对于流式 LLM 响应体重写,使用 STREAMED:在数据块到达时发送,延迟更低但实现更复杂。
  • 使用 NONE 完全跳过某个阶段。
当修改主体时,您的 ext_proc 服务还必须通过 HeaderMutation 更新 content-length 头以匹配新的主体大小。Envoy 会拒绝 content-length 与修改后主体不匹配的响应。

请求流程

启用 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 格式重写为自定义格式。
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
    )
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
对于生产环境,请预先构建容器镜像,而不是在 init 容器中编译。示例多阶段构建请参见 Helm chart 仓库 中的 e2e/transformer/Dockerfile

附加配置

HTTP 代理

Envoy 不遵循 HTTP_PROXYHTTPS_PROXYNO_PROXY 环境变量。请显式配置 HTTP 代理:
authProxy:
  httpProxy:
    enabled: true
    host: "proxy.example.com"
    port: 3128
    noProxy:
      - "internal.corp"
      - ".internal.corp"

其他选项

有关入口、自动扩缩、资源限制和其他配置选项,请参阅 Helm chart README
为了生产可靠性,请将 authProxy.autoscaling.hpa.minReplicas 设置为至少 3

完整配置示例

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以下之一:userevaluatoragent-builderapi_key
workspace_id工作区 ID
workspace_name工作区名称
organization_id组织 ID
organization_name组织名称
request_id请求关联 ID
ls_user_idLangSmith 用户 ID(仅当 actor_typeuser 时存在)
JWT 通过 x-langsmith-llm-auth 请求头传递给您的 ext_authz 或转换器服务。

常见问题

是。通过 values.yaml 中的 httpProxy 部分配置 HTTP 代理。详情请参阅 HTTP 代理
是,通过 customCa 用于自定义 CA 证书,通过 mtls 用于双向 TLS。
否。认证代理只有一个 upstream 字段。
是。多个组织可以通过其在 LangSmith 中的模型配置指向同一个认证代理实例。
可以,但仅限于自托管,我们通常建议将认证代理放在专用入口后面,以便通信使用 HTTPS。要允许 HTTP,请在您的 LangSmith values.yaml 中的 commonEnvplayground.deployment.extraEnv 添加 LLM_AUTH_PROXY_ACCEPT_HTTP。 要为 Polly 和 Insights 启用到认证代理的 HTTP 流量,请在相应的 extraEnv 部分设置此环境变量:config.polly.agent.extraEnvconfig.insights.agent.extraEnv

Helm chart 参考

有关可配置值的完整列表,请参阅 Helm chart README