Loading
close

使用代理接入外部模型提供商

time 更新时间:2026-10-08 16:41:12

背景描述

企业 MaaS 平台需要接入 OpenAI、Anthropic 等模型服务,而这些服务在部署环境中无法直连,必须经由出口代理出网。这里采用中转(relay)方案,此方案会将目标地址改变,平台改去连一个集群内的服务,由该服务替你转发到模型提供商。这个方案只需在集群内部署一个中转服务,AI 服务提供者指向它即可。

本文介绍如何部署 OpenAI 和 Anthropic 的中转服务,并将其接入模型网关对外提供服务。

前提条件

  • 已具备《安全容器服务》的部署创建权限,以及《大模型服务》中创建 AI 服务提供者、路由、套餐和 API Key 的权限。
  • 已获取可用的 OpenAI API Key 和 / 或 Anthropic API Key。两家是完全独立的账号体系,密钥互不通用:
    服务商 密钥获取地址 密钥前缀
    OpenAI https://platform.openai.com/login → API keys sk-
    Anthropic https://console.anthropic.com/settings/keys sk-ant-

    说明:

    如果只是功能验证而账户尚未充值,可以在上表对应的控制台自行创建一个密钥。余额为零时模型列表接口(/v1/models)仍可正常返回,足以验证链路是否打通;真正发起推理才需要账户有余额。

  • 已确认集群内可以访问公司出口代理 http://cloud-proxy.fake.io。可在任意一个业务 Pod 内执行以下命令验证:
    curl -sS -o /dev/null -w '%{http_code}\n' -x http://cloud-proxy.fake.io https://api.openai.com/v1/models
    返回 401 即表示代理链路正常(401 是因为没带 API Key,说明请求已经到达 OpenAI)。若返回 000 或超时,请先排查代理可达性,后续步骤无法进行。

操作步骤

  1. 准备中转服务的 YAML。

    中转服务是一个极小的 Go 程序,职责只有三件:接收集群内的请求、经出口代理转发到提供商、把响应原样回传。

    两家服务商各用一份独立的 YAML:只接入一家,就用对应的那一份;两家都要,就把两份都部署一次。

    (1) OpenAI —— 保存为 openai-proxy.yaml

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: go-relay
    data:
      main.go: |
        package main
    
        import (
            "log"
            "net/http"
            "net/http/httputil"
            "net/url"
            "os"
            "time"
        )
    
        func main() {
            targetURL := os.Getenv("TARGET_URL")
            if targetURL == "" {
                targetURL = "https://api.openai.com"
            }
    
            listenAddr := os.Getenv("LISTEN_ADDR")
            if listenAddr == "" {
                listenAddr = ":8080"
            }
    
            target, err := url.Parse(targetURL)
            if err != nil {
                log.Fatalf("invalid TARGET_URL: %v", err)
            }
    
            proxy := httputil.NewSingleHostReverseProxy(target)
    
            // 保证发给 OpenAI 的 Host 正确
            originalDirector := proxy.Director
            proxy.Director = func(req *http.Request) {
                originalDirector(req)
                req.Host = target.Host
            }
    
            // 使用 HTTP_PROXY / HTTPS_PROXY / NO_PROXY
            // HTTPS 上游会自动通过 HTTP CONNECT 走代理
            proxy.Transport = &http.Transport{
                Proxy: http.ProxyFromEnvironment,
    
                MaxIdleConns:        100,
                MaxIdleConnsPerHost: 100,
                IdleConnTimeout:     90 * time.Second,
    
                TLSHandshakeTimeout:   10 * time.Second,
                ExpectContinueTimeout: 1 * time.Second,
            }
    
            // 对 SSE / OpenAI streaming 尽快 flush
            proxy.FlushInterval = -1
    
            mux := http.NewServeMux()
    
            // Kubernetes 探针使用,不转发给 OpenAI
            mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
                w.WriteHeader(http.StatusOK)
                _, _ = w.Write([]byte("ok"))
            })
    
            // 其他请求全部转发
            mux.Handle("/", proxy)
    
            log.Printf("relay listen=%s target=%s", listenAddr, targetURL)
            log.Printf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY"))
            log.Printf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY"))
            log.Printf("NO_PROXY=%s", os.Getenv("NO_PROXY"))
    
            server := &http.Server{
                Addr:              listenAddr,
                Handler:           mux,
                ReadHeaderTimeout: 30 * time.Second,
    
                // 不设置 WriteTimeout,避免限制长时间 streaming
                IdleTimeout: 120 * time.Second,
            }
    
            log.Fatal(server.ListenAndServe())
        }
    
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: openai-relay
    spec:
      replicas: 1
    
      selector:
        matchLabels:
          app: openai-relay
    
      template:
        metadata:
          labels:
            app: openai-relay
    
        spec:
          containers:
            - name: relay
              image: golang:1.23-bullseye
              imagePullPolicy: IfNotPresent
    
              command:
                - go
    
              args:
                - run
                - /app/main.go
    
              env:
                # 真正的上游地址
                - name: TARGET_URL
                  value: "https://api.openai.com"
    
                - name: LISTEN_ADDR
                  value: ":8080"
    
                # ==============================
                # 修改成你的企业 HTTP 正向代理
                # ==============================
                - name: HTTPS_PROXY
                  value: "https://cloud-proxy.fake.io"
    
                - name: HTTP_PROXY
                  value: "http://cloud-proxy.fake.io"
    
                - name: NO_PROXY
                  value: "localhost,127.0.0.1,.svc,.cluster.local"
    
              ports:
                - name: http
                  containerPort: 8080
    
              volumeMounts:
                - name: source
                  mountPath: /app
                  readOnly: true
    
              readinessProbe:
                httpGet:
                  path: /healthz
                  port: 8080
                initialDelaySeconds: 2
                periodSeconds: 5
    
              livenessProbe:
                httpGet:
                  path: /healthz
                  port: 8080
                initialDelaySeconds: 5
                periodSeconds: 10
    
          volumes:
            - name: source
              configMap:
                name: go-relay
    
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: openai-relay
    spec:
      selector:
        app: openai-relay
    
      ports:
        - name: http
          port: 8080
          targetPort: 8080
    
      type: ClusterIP

    (2) Anthropic —— 保存为 anthropic-proxy.yaml

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: go-relay-anthropic
    data:
      main.go: |
        package main
    
        import (
            "log"
            "net/http"
            "net/http/httputil"
            "net/url"
            "os"
            "strings"
            "time"
        )
    
        func main() {
            targetURL := os.Getenv("TARGET_URL")
            if targetURL == "" {
                targetURL = "https://api.anthropic.com"
            }
    
            listenAddr := os.Getenv("LISTEN_ADDR")
            if listenAddr == "" {
                listenAddr = ":8080"
            }
    
            // Anthropic authenticates with x-api-key, not "Authorization: Bearer",
            // which is the only form the model gateway sends. Set AUTH_MODE to
            // "anthropic" on a relay whose TARGET_URL is api.anthropic.com.
            anthropic := os.Getenv("AUTH_MODE") == "anthropic"
            anthropicVersion := os.Getenv("ANTHROPIC_VERSION")
            if anthropicVersion == "" {
                anthropicVersion = "2023-06-01"
            }
    
            target, err := url.Parse(targetURL)
            if err != nil {
                log.Fatalf("invalid TARGET_URL: %v", err)
            }
    
            proxy := httputil.NewSingleHostReverseProxy(target)
    
            // 保证发给 Anthropic 的 Host 正确
            originalDirector := proxy.Director
            proxy.Director = func(req *http.Request) {
                originalDirector(req)
                req.Host = target.Host
                if anthropic {
                    // Move the credential onto the header Anthropic reads.
                    if v := req.Header.Get("Authorization"); strings.HasPrefix(v, "Bearer ") {
                        req.Header.Set("x-api-key", strings.TrimSpace(strings.TrimPrefix(v, "Bearer ")))
                        req.Header.Del("Authorization")
                    }
                    // Documented as required by Anthropic; nothing upstream sets it.
                    if req.Header.Get("anthropic-version") == "" {
                        req.Header.Set("anthropic-version", anthropicVersion)
                    }
                }
            }
    
            // 使用 HTTP_PROXY / HTTPS_PROXY / NO_PROXY
            // HTTPS 上游会自动通过 HTTP CONNECT 走代理
            proxy.Transport = &http.Transport{
                Proxy: http.ProxyFromEnvironment,
    
                MaxIdleConns:        100,
                MaxIdleConnsPerHost: 100,
                IdleConnTimeout:     90 * time.Second,
    
                TLSHandshakeTimeout:   10 * time.Second,
                ExpectContinueTimeout: 1 * time.Second,
            }
    
            // 对 SSE / streaming 尽快 flush
            proxy.FlushInterval = -1
    
            mux := http.NewServeMux()
    
            // Kubernetes 探针使用,不转发给上游
            mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
                w.WriteHeader(http.StatusOK)
                _, _ = w.Write([]byte("ok"))
            })
    
            // 其他请求全部转发
            mux.Handle("/", proxy)
    
            log.Printf("relay listen=%s target=%s", listenAddr, targetURL)
            log.Printf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY"))
            log.Printf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY"))
            log.Printf("NO_PROXY=%s", os.Getenv("NO_PROXY"))
    
            server := &http.Server{
                Addr:              listenAddr,
                Handler:           mux,
                ReadHeaderTimeout: 30 * time.Second,
    
                // 不设置 WriteTimeout,避免限制长时间 streaming
                IdleTimeout: 120 * time.Second,
            }
    
            log.Fatal(server.ListenAndServe())
        }
    
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: anthropic-relay
    spec:
      replicas: 1
    
      selector:
        matchLabels:
          app: anthropic-relay
    
      template:
        metadata:
          labels:
            app: anthropic-relay
    
        spec:
          containers:
            - name: relay
              image: golang:1.23-bullseye
              imagePullPolicy: IfNotPresent
    
              command:
                - go
    
              args:
                - run
                - /app/main.go
    
              env:
                # 真正的上游地址
                - name: TARGET_URL
                  value: "https://api.anthropic.com"
    
                # 把网关下发的 Authorization: Bearer 改写成 Anthropic 要的 x-api-key
                - name: AUTH_MODE
                  value: "anthropic"
    
                - name: LISTEN_ADDR
                  value: ":8080"
    
                # ==============================
                # 修改成你的企业 HTTP 正向代理
                # ==============================
                - name: HTTPS_PROXY
                  value: "https://cloud-proxy.fake.io"
    
                - name: HTTP_PROXY
                  value: "http://cloud-proxy.fake.io"
    
                - name: NO_PROXY
                  value: "localhost,127.0.0.1,.svc,.cluster.local"
    
              ports:
                - name: http
                  containerPort: 8080
    
              volumeMounts:
                - name: source
                  mountPath: /app
                  readOnly: true
    
              readinessProbe:
                httpGet:
                  path: /healthz
                  port: 8080
                initialDelaySeconds: 2
                periodSeconds: 5
    
              livenessProbe:
                httpGet:
                  path: /healthz
                  port: 8080
                initialDelaySeconds: 5
                periodSeconds: 10
    
          volumes:
            - name: source
              configMap:
                name: go-relay-anthropic
    
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: anthropic-relay
    spec:
      selector:
        app: anthropic-relay
    
      ports:
        - name: http
          port: 8080
          targetPort: 8080
    
      type: ClusterIP
  2. 创建中转服务的部署。

    1. 在云平台中依次进入《安全容器服务》-《工作负载》,单击 创建部署。
    2. 在页面右下角单击 导入 YAML,粘贴需要的那一份内容,确认命名空间后提交。
    3. 如果两家都要接入,重复上一步,导入另一份。两份之间没有共享资源,先后顺序随意。
    4. 等待部署状态变为运行中。创建完成后,服务在集群内的访问地址为:
      http://openai-relay.<命名空间>.svc.cluster.local:8080        # openai-proxy.yaml
      http://anthropic-relay.<命名空间>.svc.cluster.local:8080     # anthropic-proxy.yaml
      后续步骤需要用到这些地址,请记录下实际的命名空间。
  3. 验证中转服务本身。

    在接入平台之前先单独验证中转链路,可以把问题范围限定在最小。在集群内任意 Pod 中执行:

    # OpenAI:带上自己的 API Key,应返回模型列表
    curl -sS http://openai-relay.<命名空间>.svc.cluster.local:8080/v1/models \
      -H "Authorization: Bearer <OpenAI API Key>" | head -c 300
    
    # Anthropic:注意这里同样用 Bearer,由中转服务改写为 x-api-key
    curl -sS http://anthropic-relay.<命名空间>.svc.cluster.local:8080/v1/models \
      -H "Authorization: Bearer <Anthropic API Key>" | head -c 300

    两条命令都返回 JSON 模型列表,说明「集群 → 中转 → 公司代理 → 服务提供商」整条链路已经打通。若返回 502,查看对应 Pod 日志中的 relay error 一行即可定位是代理不通还是上游拒绝。

  4. 接入 AI 服务提供者。

    1. 进入《大模型服务》-《AI 服务提供者》,单击 创建 AI 服务提供者。
    2. 「类型」选择「外部」(即「外部 AI 服务(OpenAI / DeepSeek 等,需密钥)」)。
    3. 按下表填写:
      字段 OpenAI Anthropic
      API 规范 OpenAI Anthropic
      访问地址 http://openai-relay.<命名空间>.svc.cluster.local:8080 http://anthropic-relay.<命名空间>.svc.cluster.local:8080
      访问密钥 (API Key) OpenAI 的 API Key Anthropic 的 API Key
    4. 单击创建。
  5. 创建模型网关路由。

    1. 进入《模型网关》,单击 创建路由,输入路由域名。
    2. 添加模型时,后端选择步骤 4 创建的 AI 服务提供者。此时页面会列出该提供者可用的模型名称——能够成功列出模型,即再次证明网关经中转服务访问服务是通的。
    3. 单击 创建。

      说明:

      同一模型的多个后端必须使用相同的 API 规范,因此不要把 OpenAI 提供者和 Anthropic 提供者配置为同一个模型的主备后端。不同模型之间可以自由混用,一条路由里同时提供 OpenAI 和 Anthropic 的模型是推荐做法,业务侧只需记住一个域名和一个密钥。

  6. 创建套餐并签发 API Key。

    1. 进入《套餐管理》,单击 创建套餐,「可访问路由」选择步骤 5 创建的路由,设置调用频率与 Token 限额后创建。
    2. 进入《API Key》,单击 创建,选择上述套餐,单击 创建并签发密钥,复制并妥善保存密钥。密钥只在此时完整展示一次。

结果验证

  1. 确认路由域名可以解析到网关地址。可以配置 hosts 记录,也可以在 curl 中用 --resolve 临时指定。
  2. 调用 OpenAI 模型,确认正常返回:
    curl -sS http://<路由域名>:<网关端口>/v1/chat/completions \
      -X POST \
      -H 'Content-Type: application/json' \
      -H 'Authorization: Bearer <平台签发的 API Key>' \
      -d '{
        "model": "<路由中配置的模型名称>",
        "messages": [{ "role": "user", "content": "Hello!" }]
      }'
  3. 调用 Anthropic 模型。路径和请求体都与 OpenAI 不同,必须用 Anthropic 的 Messages API:路径是 /anthropic/v1/messages,请求体是 Anthropic 原生格式,其中 max_tokens 为必填。只有平台签发的密钥仍然放在 Authorization: Bearer 里,中转服务会把它改写成 Anthropic 要的 x-api-key。
    curl -sS http://<路由域名>:<网关端口>/anthropic/v1/messages \
      -X POST \
      -H 'Content-Type: application/json' \
      -H 'Authorization: Bearer <平台签发的 API Key>' \
      -d '{
        "model": "<路由中配置的 Anthropic 模型名称>",
        "max_tokens": 64,
        "messages": [{ "role": "user", "content": "Hello!" }]
      }'
  4. 验证流式响应未被中转服务缓冲。在请求体中加入 "stream": true,确认输出是逐段返回而不是最后一次性吐出(下面以 OpenAI 为例;验证 Anthropic 时把路径和请求体换成第 3 条的形式,同样加 "stream": true):
    curl -N -sS http://<路由域名>:<网关端口>/v1/chat/completions \
      -X POST \
      -H 'Content-Type: application/json' \
      -H 'Authorization: Bearer <平台签发的 API Key>' \
      -d '{ "model": "<模型名称>", "messages": [{"role":"user","content":"数到二十"}], "stream": true }'
  5. 进入《Token 用量》,确认出现对应的调用记录与 Token 消耗。

约束与注意事项

  • 两种 API 规范的端点和请求格式不互通。 网关按路径挑选翻译器,/v1/chat/completions 只接受 OpenAI、AWSBedrock、AWSAnthropic、AzureOpenAI、GCPVertexAI、GCPAnthropic 六种规范的后端,不含 Anthropic;Anthropic 规范的后端只在 /anthropic/v1/messages 上提供服务,且请求体必须是 Anthropic 原生格式。
  • 一个中转服务只对应一个提供商。 上游地址在部署时固定。接入新的服务商,复制一份 YAML、改掉资源名和 TARGET_URL 即可;注意 ConfigMap 也要一并改名,否则会覆盖已有的那一份。
  • 中转服务本身不做鉴权。 它只负责转发,凭据由调用方携带。
  • 请求超时上限为 300 秒,由模型网关控制,该时长包含重试在内。中转服务未设置写超时,不会先于网关切断长响应。
  • 多了一跳网络。 中转服务是链路上的一个新组件,排查问题时的定位顺序是:先看中转服务 Pod 日志中的 proxy error,再看网关侧。
  • 本方案是中转,不是正向代理。 如果后续出现「必须保持目标地址不变」的硬性要求(例如上游按 SNI 做准入),本方案不适用,需要重新评估。
此篇文章对你是否有帮助?
没帮助
locked-file

您暂无权限访问该产品