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; 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 or 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 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 }) or body) 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 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 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 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 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; } location / { proxy_pass http://10.1.0.1:3001; } }