From 6f657a538917ff080c5b4fbe413ea8f9f03b74fa Mon Sep 17 00:00:00 2001 From: wdh-home <243823965@qq.com> Date: Mon, 18 May 2026 23:52:14 +0800 Subject: [PATCH] 1 --- conf/conf.d/ai.sggai.site.conf | 374 +++++++++++++++++++++++++--- conf/conf.d/ai.sggai.site.conf.back | 147 +++++++++++ 2 files changed, 481 insertions(+), 40 deletions(-) create mode 100644 conf/conf.d/ai.sggai.site.conf.back diff --git a/conf/conf.d/ai.sggai.site.conf b/conf/conf.d/ai.sggai.site.conf index 784d691..936caed 100644 --- a/conf/conf.d/ai.sggai.site.conf +++ b/conf/conf.d/ai.sggai.site.conf @@ -59,21 +59,262 @@ server { location = /v1/chat/completions { client_body_buffer_size 16k; + subrequest_output_buffer_size 8m; content_by_lua_block { local backend = "@ai_sggai_site_backend" local max_body_bytes = 1024 + local cjson = require "cjson.safe" + local json_null = cjson.null + + local function write_json(status, value) + local encoded = cjson.encode(value) + if not encoded then + encoded = '{"error":{"message":"failed to encode gateway response","type":"gateway_error"}}' + status = ngx.HTTP_BAD_GATEWAY + end + + ngx.status = status + ngx.header["Content-Type"] = "application/json; charset=utf-8" + ngx.say(encoded) + return ngx.exit(status) + end + + local function ensure_choice(choices, index) + index = tonumber(index) or 0 + local key = index + 1 + + if not choices[key] then + choices[key] = { + index = index, + role = nil, + content = {}, + reasoning_content = {}, + finish_reason = nil, + tool_calls = {} + } + end + + return choices[key] + end + + local function append_tool_call(choice_state, tool_call_delta) + if type(tool_call_delta) ~= "table" then + return + end + + local tool_index = tonumber(tool_call_delta.index) or #choice_state.tool_calls + local key = tool_index + 1 + local tool_state = choice_state.tool_calls[key] + + if not tool_state then + tool_state = { + index = tool_index, + id = nil, + type = nil, + function_name = {}, + function_arguments = {} + } + choice_state.tool_calls[key] = tool_state + end + + if type(tool_call_delta.id) == "string" then + tool_state.id = tool_call_delta.id + end + + if type(tool_call_delta.type) == "string" then + tool_state.type = tool_call_delta.type + end + + local fn = tool_call_delta["function"] + if type(fn) == "table" then + if type(fn.name) == "string" then + tool_state.function_name[#tool_state.function_name + 1] = fn.name + end + + if type(fn.arguments) == "string" then + tool_state.function_arguments[#tool_state.function_arguments + 1] = fn.arguments + end + end + end + + local function flush_sse_data(data, state) + if data == "[DONE]" then + return true, true + end + + local chunk, decode_err = cjson.decode(data) + if type(chunk) ~= "table" then + return nil, "bad upstream SSE JSON: " .. (decode_err or data) + end + + if type(chunk.id) == "string" then + state.id = chunk.id + end + + if type(chunk.created) == "number" then + state.created = chunk.created + end + + if type(chunk.model) == "string" then + state.model = chunk.model + end + + if type(chunk.system_fingerprint) == "string" then + state.system_fingerprint = chunk.system_fingerprint + end + + if type(chunk.usage) == "table" then + state.usage = chunk.usage + end + + if type(chunk.choices) ~= "table" then + return true, false + end + + for _, upstream_choice in ipairs(chunk.choices) do + if type(upstream_choice) == "table" then + local choice_state = ensure_choice(state.choices, upstream_choice.index) + local delta = upstream_choice.delta + + if type(delta) == "table" then + if type(delta.role) == "string" then + choice_state.role = delta.role + end + + if type(delta.content) == "string" then + choice_state.content[#choice_state.content + 1] = delta.content + end + + if type(delta.reasoning_content) == "string" then + choice_state.reasoning_content[#choice_state.reasoning_content + 1] = delta.reasoning_content + end + + if type(delta.tool_calls) == "table" then + for _, tool_call_delta in ipairs(delta.tool_calls) do + append_tool_call(choice_state, tool_call_delta) + end + end + end + + if upstream_choice.finish_reason ~= nil and upstream_choice.finish_reason ~= json_null then + choice_state.finish_reason = upstream_choice.finish_reason + end + end + end + + return true, false + end + + local function parse_sse_body(sse_body, original_payload) + local state = { + id = nil, + created = nil, + model = original_payload.model, + system_fingerprint = nil, + usage = nil, + choices = {} + } + local data_lines = {} + + for raw_line in (sse_body .. "\n"):gmatch("([^\n]*)\n") do + local line = raw_line:gsub("\r$", "") + + if line == "" then + if #data_lines > 0 then + local ok, done_or_err = flush_sse_data(table.concat(data_lines, "\n"), state) + data_lines = {} + + if not ok then + return nil, done_or_err + end + + if done_or_err == true then + break + end + end + else + local data = line:match("^data:%s?(.*)$") + if data then + data_lines[#data_lines + 1] = data + end + end + end + + if #data_lines > 0 then + local ok, done_or_err = flush_sse_data(table.concat(data_lines, "\n"), state) + if not ok then + return nil, done_or_err + end + end + + local choices = {} + for _, choice_state in ipairs(state.choices) do + local content = table.concat(choice_state.content) + local message = { + role = choice_state.role or "assistant", + content = content + } + local tool_calls = {} + + if #choice_state.reasoning_content > 0 then + message.reasoning_content = table.concat(choice_state.reasoning_content) + end + + for _, tool_state in ipairs(choice_state.tool_calls) do + tool_calls[#tool_calls + 1] = { + id = tool_state.id, + type = tool_state.type or "function", + ["function"] = { + name = table.concat(tool_state.function_name), + arguments = table.concat(tool_state.function_arguments) + } + } + end + + if #tool_calls > 0 then + message.tool_calls = tool_calls + if content == "" then + message.content = json_null + end + end + + choices[#choices + 1] = { + index = choice_state.index, + message = message, + finish_reason = choice_state.finish_reason or "stop" + } + end + + if #choices == 0 then + return nil, "upstream stream response did not contain choices" + end + + local completion = { + id = state.id or ("chatcmpl-gateway-" .. ngx.now()), + object = "chat.completion", + created = state.created or ngx.time(), + model = state.model, + choices = choices + } + + if state.usage then + completion.usage = state.usage + end + + if state.system_fingerprint then + completion.system_fingerprint = state.system_fingerprint + end + + return completion + end if ngx.req.get_method() ~= "POST" then return ngx.exec(backend) end local content_length = tonumber(ngx.var.http_content_length) - if not content_length then - return ngx.exec(backend) - end - - if content_length > max_body_bytes then + if not content_length or content_length > max_body_bytes then return ngx.exec(backend) end @@ -84,59 +325,112 @@ server { return ngx.exec(backend) end - local cjson = require "cjson.safe" - local short_post_log = { + local payload = cjson.decode(body) + if type(payload) ~= "table" or payload.stream ~= false then + return ngx.exec(backend) + end + + payload.stream = true + + local upstream_body, encode_err = cjson.encode(payload) + if not upstream_body then + ngx.log(ngx.ERR, "[ai.sggai.site] failed to encode upstream stream request: ", encode_err or "unknown") + return write_json(ngx.HTTP_BAD_GATEWAY, { + error = { + message = "failed to encode upstream stream request", + type = "gateway_error" + } + }) + end + + ngx.log(ngx.NOTICE, "[ai.sggai.site] bridge short non-stream POST as upstream stream: ", cjson.encode({ method = ngx.req.get_method(), uri = ngx.var.request_uri, content_length = content_length, content_type = ngx.var.http_content_type, user_agent = ngx.var.http_user_agent, body = body - } - ngx.log(ngx.NOTICE, "[ai.sggai.site] short POST request: ", cjson.encode(short_post_log) or body) + }) or body) - local payload = cjson.decode(body) - if type(payload) ~= "table" or payload.stream ~= false then - return ngx.exec(backend) + local upstream_res = ngx.location.capture("/__ai_sggai_stream_bridge_backend", { + method = ngx.HTTP_POST, + body = upstream_body, + args = ngx.var.args or "" + }) + + if not upstream_res then + ngx.log(ngx.ERR, "[ai.sggai.site] upstream stream subrequest failed") + return write_json(ngx.HTTP_BAD_GATEWAY, { + error = { + message = "upstream stream subrequest failed", + type = "gateway_error" + } + }) end - if type(payload.messages) ~= "table" then - return ngx.exec(backend) + if upstream_res.status < 200 or upstream_res.status >= 300 then + ngx.status = upstream_res.status + ngx.header["Content-Type"] = (upstream_res.header and upstream_res.header["Content-Type"]) or "application/json; charset=utf-8" + ngx.print(upstream_res.body or "") + return ngx.exit(upstream_res.status) end - local has_hi_content = false - - for _, message in ipairs(payload.messages) do - if type(message) == "table" and message.content == "hi" then - has_hi_content = true - break - end + local plain_completion = cjson.decode(upstream_res.body or "") + if type(plain_completion) == "table" and type(plain_completion.choices) == "table" then + return write_json(ngx.HTTP_OK, plain_completion) end - if not has_hi_content then - return ngx.exec(backend) + local completion, parse_err = parse_sse_body(upstream_res.body or "", payload) + if not completion then + ngx.log(ngx.ERR, "[ai.sggai.site] failed to parse upstream stream response: ", parse_err or "unknown", "; upstream_body=", (upstream_res.body or ""):sub(1, 2048)) + return write_json(ngx.HTTP_BAD_GATEWAY, { + error = { + message = "failed to parse upstream stream response", + type = "gateway_error" + } + }) end - 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) + return write_json(ngx.HTTP_OK, completion) } } + location = /__ai_sggai_stream_bridge_backend { + internal; + proxy_buffering on; + proxy_request_buffering on; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Authorization $http_authorization; + proxy_set_header Content-Type "application/json"; + proxy_set_header Accept "text/event-stream"; + 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_pass http://10.1.0.1:3001/v1/chat/completions; + } + location @ai_sggai_site_backend { proxy_pass http://10.1.0.1:3001; } diff --git a/conf/conf.d/ai.sggai.site.conf.back b/conf/conf.d/ai.sggai.site.conf.back new file mode 100644 index 0000000..784d691 --- /dev/null +++ b/conf/conf.d/ai.sggai.site.conf.back @@ -0,0 +1,147 @@ +server { + 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; + + error_log /usr/local/openresty/nginx/logs/ai.sggai.site.error.log notice; + error_log /dev/stderr notice; + + 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; + + 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; + + location = /v1/chat/completions { + client_body_buffer_size 16k; + + content_by_lua_block { + local backend = "@ai_sggai_site_backend" + local max_body_bytes = 1024 + + if ngx.req.get_method() ~= "POST" then + return ngx.exec(backend) + end + + local content_length = tonumber(ngx.var.http_content_length) + if not content_length then + return ngx.exec(backend) + end + + if content_length > max_body_bytes then + return ngx.exec(backend) + end + + ngx.req.read_body() + + local body = ngx.req.get_body_data() + if not body or #body > max_body_bytes then + return ngx.exec(backend) + end + + local cjson = require "cjson.safe" + local short_post_log = { + method = ngx.req.get_method(), + uri = ngx.var.request_uri, + content_length = content_length, + content_type = ngx.var.http_content_type, + user_agent = ngx.var.http_user_agent, + body = body + } + ngx.log(ngx.NOTICE, "[ai.sggai.site] short POST request: ", cjson.encode(short_post_log) or body) + + local payload = cjson.decode(body) + if type(payload) ~= "table" or payload.stream ~= false then + return ngx.exec(backend) + end + + if type(payload.messages) ~= "table" then + return ngx.exec(backend) + end + + local has_hi_content = false + + for _, message in ipairs(payload.messages) do + if type(message) == "table" and message.content == "hi" then + has_hi_content = true + break + end + end + + if not has_hi_content then + return ngx.exec(backend) + end + + 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) + } + } + + location @ai_sggai_site_backend { + proxy_pass http://10.1.0.1:3001; + } + + location / { + proxy_pass http://10.1.0.1:3001; + } +}