server { # 文件名前缀是 000-,会比 ai.sggai.site.conf 更早加载。 # 这样可以在不修改原配置文件的情况下,让这里的 mock 逻辑优先生效。 listen 80; listen 443 ssl; server_name ai.sggai.site; ssl_certificate /etc/letsencrypt/live/ai.sggai.site/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/ai.sggai.site/privkey.pem; location ^~ /.well-known/acme-challenge/ { root /var/www; default_type text/plain; try_files $uri =404; } client_max_body_size 200m; gzip off; gunzip off; # 公共反代配置放在 server 级别,主后端 proxy_pass location 会继承这些配置。 proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Authorization $http_authorization; proxy_set_header Content-Type $http_content_type; proxy_set_header Accept $http_accept; proxy_set_header User-Agent $http_user_agent; proxy_set_header Originator $http_originator; proxy_set_header Session_id $http_session_id; proxy_set_header X-Codex-Beta-Features $http_x_codex_beta_features; proxy_set_header X-Codex-Turn-Metadata $http_x_codex_turn_metadata; proxy_set_header X-Stainless-Arch $http_x_stainless_arch; proxy_set_header X-Stainless-Lang $http_x_stainless_lang; proxy_set_header X-Stainless-Os $http_x_stainless_os; proxy_set_header X-Stainless-Package-Version $http_x_stainless_package_version; proxy_set_header X-Stainless-Retry-Count $http_x_stainless_retry_count; proxy_set_header X-Stainless-Runtime $http_x_stainless_runtime; proxy_set_header X-Stainless-Runtime-Version $http_x_stainless_runtime_version; proxy_set_header X-Stainless-Timeout $http_x_stainless_timeout; proxy_set_header X-App $http_x_app; proxy_set_header Anthropic-Beta $http_anthropic_beta; proxy_set_header Anthropic-Dangerous-Direct-Browser-Access $http_anthropic_dangerous_direct_browser_access; proxy_set_header Anthropic-Version $http_anthropic_version; proxy_set_header Accept-Encoding ""; proxy_set_header X-Real-IP ""; proxy_set_header X-Forwarded-For ""; proxy_set_header X-Forwarded-Proto ""; proxy_set_header X-Forwarded-Host ""; proxy_set_header X-Forwarded-Port ""; proxy_set_header Connection ""; proxy_buffering off; proxy_request_buffering off; proxy_cache off; proxy_cache_bypass 1; proxy_connect_timeout 600s; proxy_read_timeout 3600s; proxy_send_timeout 3600s; # 只拦截很小的非流式 Chat Completions 请求。 # 不满足条件的请求会内部跳转到正常后端,不直接返回 mock。 location = /v1/chat/completions { client_body_buffer_size 16k; content_by_lua_block { -- 不满足 mock 条件时,统一内部跳转到这个 named location。 local backend = "@ai_sggai_site_backend" -- 请求体总大小上限:只处理非常小的探测类请求。 local max_body_bytes = 1024 -- 只处理 POST;其它方法按普通请求转发到真实后端。 if ngx.req.get_method() ~= "POST" then return ngx.exec(backend) end -- 先看 Content-Length,超过阈值就不读取 body,直接放行。 local content_length = tonumber(ngx.var.http_content_length) if not content_length then -- 没有 Content-Length 的请求可能是 chunked,不为了判断 mock 去读取未知大小的 body。 return ngx.exec(backend) end -- 请求体太大,说明不是目标小请求,直接走真实后端。 if content_length > max_body_bytes then return ngx.exec(backend) end -- 到这里说明 body 足够小,可以安全读取并解析 JSON。 ngx.req.read_body() local body = ngx.req.get_body_data() if not body or #body > max_body_bytes then -- body 不在内存里或实际大小仍然超限时,不 mock,转发到真实后端。 return ngx.exec(backend) end -- JSON 解析失败,或 stream 不是 false,都不 mock。 local cjson = require "cjson.safe" local payload = cjson.decode(body) if type(payload) ~= "table" or payload.stream ~= false then return ngx.exec(backend) end -- 必须有 messages 数组;没有就不 mock。 if type(payload.messages) ~= "table" then return ngx.exec(backend) end local has_hi_content = false -- 遍历所有 message,只匹配精确的 "content": "hi"。 for _, message in ipairs(payload.messages) do if type(message) == "table" and message.content == "hi" then has_hi_content = true break end end -- 没有精确命中 "content": "hi",交给真实后端处理。 if not has_hi_content then return ngx.exec(backend) end -- 命中小请求 + stream=false + "content": "hi" 条件,直接返回 mock 响应。 ngx.status = ngx.HTTP_OK ngx.header["Content-Type"] = "application/json; charset=utf-8" ngx.say([[{ "id": "chatcmpl-mock", "object": "chat.completion", "created": 1716030000, "model": "xxx", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "ok" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2 } }]]) return ngx.exit(ngx.HTTP_OK) } } # 仅供上面的 Lua 逻辑通过 ngx.exec 内部跳转使用,外部 URL 不能直接访问这个 location。 location @ai_sggai_site_backend { proxy_pass http://10.1.0.1:3001; } # 普通流量保持原来的上游转发行为。 location / { proxy_pass http://10.1.0.1:3001; } }