| 461 | } |
| 462 | |
| 463 | bool LlmProxyService::ForwardToUpstream(uintptr_t client_sock, |
| 464 | const settings::LlmModelMapping& mapping, |
| 465 | const std::string& modified_body, |
| 466 | bool is_streaming, bool keep_alive, |
| 467 | int& out_status, std::string& out_response_body, |
| 468 | StreamEndInfo& out_stream_end) { |
| 469 | SOCKET client = static_cast<SOCKET>(client_sock); |
| 470 | const uint64_t t_start = NowMonotonicMs(); |
| 471 | |
| 472 | // Default classification: becomes "n/a" for non-streaming, otherwise |
| 473 | // overwritten by the SSE loop below. |
| 474 | out_stream_end.reason = is_streaming ? "unknown" : "n/a"; |
| 475 | |
| 476 | auto set_handshake_error = [&](const std::string& msg, uint32_t err = 0) { |
| 477 | out_stream_end.reason = "handshake_error"; |
| 478 | out_stream_end.detail = msg; |
| 479 | out_stream_end.winhttp_error = err; |
| 480 | out_stream_end.duration_ms = NowMonotonicMs() - t_start; |
| 481 | }; |
| 482 | |
| 483 | // Build upstream URL: target_url + /chat/completions |
| 484 | std::string upstream_url = mapping.target_url; |
| 485 | // Ensure no trailing slash duplication |
| 486 | if (!upstream_url.empty() && upstream_url.back() == '/') |
| 487 | upstream_url.pop_back(); |
| 488 | upstream_url += "/chat/completions"; |
| 489 | |
| 490 | UrlParts parts; |
| 491 | if (!ParseUrl(upstream_url, parts)) { |
| 492 | set_handshake_error("invalid_upstream_url"); |
| 493 | SendErrorResponse(client_sock, 502, "Bad Gateway", |
| 494 | "Invalid upstream URL: " + mapping.target_url, keep_alive); |
| 495 | return false; |
| 496 | } |
| 497 | |
| 498 | HINTERNET session = WinHttpOpen(L"TenBox-LLM-Proxy/1.0", |
| 499 | WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, |
| 500 | WINHTTP_NO_PROXY_NAME, |
| 501 | WINHTTP_NO_PROXY_BYPASS, 0); |
| 502 | if (!session) { |
| 503 | set_handshake_error("WinHttpOpen failed", GetLastError()); |
| 504 | SendErrorResponse(client_sock, 502, "Bad Gateway", |
| 505 | "WinHttpOpen failed", keep_alive); |
| 506 | return false; |
| 507 | } |
| 508 | |
| 509 | // Set explicit WinHTTP timeouts. Default receive timeout (30s) is too short for SSE streams. |
| 510 | if (!WinHttpSetTimeouts(session, 15000, 60000, 30000, 300000)) { |
| 511 | DWORD err = GetLastError(); |
| 512 | LOG_ERROR("WinHttpSetTimeouts failed: %lu", err); |
| 513 | } |
| 514 | |
| 515 | // Enable HTTP/1.1 keep-alive (default in WinHTTP) |
| 516 | HINTERNET conn = WinHttpConnect(session, parts.host.c_str(), parts.port, 0); |
| 517 | if (!conn) { |
| 518 | DWORD err = GetLastError(); |
| 519 | set_handshake_error("WinHttpConnect failed", err); |
| 520 | WinHttpCloseHandle(session); |
nothing calls this directly
no test coverage detected