背景描述
企业 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/models401即表示代理链路正常(401 是因为没带 API Key,说明请求已经到达 OpenAI)。若返回000或超时,请先排查代理可达性,后续步骤无法进行。
操作步骤
准备中转服务的 YAML。
中转服务是一个极小的 Go 程序,职责只有三件:接收集群内的请求、经出口代理转发到提供商、把响应原样回传。
两家服务商各用一份独立的 YAML:只接入一家,就用对应的那一份;两家都要,就把两份都部署一次。
(1) OpenAI —— 保存为
openai-proxy.yamlapiVersion: 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.yamlapiVersion: 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创建中转服务的部署。
- 在云平台中依次进入《安全容器服务》-《工作负载》,单击
创建部署。 - 在页面右下角单击
导入 YAML,粘贴需要的那一份内容,确认命名空间后提交。 - 如果两家都要接入,重复上一步,导入另一份。两份之间没有共享资源,先后顺序随意。
- 等待部署状态变为运行中。创建完成后,服务在集群内的访问地址为:
后续步骤需要用到这些地址,请记录下实际的命名空间。http://openai-relay.<命名空间>.svc.cluster.local:8080 # openai-proxy.yaml http://anthropic-relay.<命名空间>.svc.cluster.local:8080 # anthropic-proxy.yaml
- 在云平台中依次进入《安全容器服务》-《工作负载》,单击
验证中转服务本身。
在接入平台之前先单独验证中转链路,可以把问题范围限定在最小。在集群内任意 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一行即可定位是代理不通还是上游拒绝。接入 AI 服务提供者。
- 进入《大模型服务》-《AI 服务提供者》,单击
创建 AI 服务提供者。 - 「类型」选择「外部」(即「外部 AI 服务(OpenAI / DeepSeek 等,需密钥)」)。
- 按下表填写:
字段 OpenAI Anthropic API 规范 OpenAI Anthropic 访问地址 http://openai-relay.<命名空间>.svc.cluster.local:8080http://anthropic-relay.<命名空间>.svc.cluster.local:8080访问密钥 (API Key) OpenAI 的 API Key Anthropic 的 API Key - 单击创建。
- 进入《大模型服务》-《AI 服务提供者》,单击
创建模型网关路由。
- 进入《模型网关》,单击
创建路由,输入路由域名。 - 添加模型时,后端选择步骤 4 创建的 AI 服务提供者。此时页面会列出该提供者可用的模型名称——能够成功列出模型,即再次证明网关经中转服务访问服务是通的。
- 单击
创建。说明:
同一模型的多个后端必须使用相同的 API 规范,因此不要把 OpenAI 提供者和 Anthropic 提供者配置为同一个模型的主备后端。不同模型之间可以自由混用,一条路由里同时提供 OpenAI 和 Anthropic 的模型是推荐做法,业务侧只需记住一个域名和一个密钥。
- 进入《模型网关》,单击
创建套餐并签发 API Key。
- 进入《套餐管理》,单击
创建套餐,「可访问路由」选择步骤 5 创建的路由,设置调用频率与 Token 限额后创建。 - 进入《API Key》,单击
创建,选择上述套餐,单击创建并签发密钥,复制并妥善保存密钥。密钥只在此时完整展示一次。
- 进入《套餐管理》,单击
结果验证
- 确认路由域名可以解析到网关地址。可以配置 hosts 记录,也可以在 curl 中用
--resolve临时指定。 - 调用 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!" }] }' - 调用 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!" }] }' - 验证流式响应未被中转服务缓冲。在请求体中加入
"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 }' - 进入《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 做准入),本方案不适用,需要重新评估。