Repository: ggml-org/llama.cpp
Pull request: llama-server: fix duplicate HTTP headers in multiple models mode

*Make sure to read the [contributing guidelines](https://github.com/ggml-org/llama.cpp/blob/master/CONTRIBUTING.md) before submitting a PR*

## Approach: Filter at source

This patch filters headers before forwarding them to avoid duplication.

Why headers get duplicated:
When the router proxies child process responses, both the router (via 
set_default_headers) and the child send the same headers (Server, 
Transfer-Encoding, Keep-Alive, CORS). The proxy was forwarding everything, 
resulting in duplicates.

Solution:
1. Skip headers that will be added by the router or httplib:
   - server, transfer-encoding, keep-alive (duplicated by router defaults)
   - access-control-* (CORS headers injected by router)

2. Handle Content-Type separately via msg_t.content_type to avoid duplication 
   when httplib calls set_chunked_content_provider() or set_content().

Tested with:

```
curl -v http://127.0.0.1:8082/v1/chat/completions   -H "Content-Type: application/json"   -d '{
    "model": "Codestral-22B-v0.1-i1-GGUF",
    "messages": [{"role": "user", "content": "hi"}],
    "max_tokens": 5
  }'
*   Trying 127.0.0.1:8082...
* Connected to 127.0.0.1 (127.0.0.1) port 8082 (#0)
> POST /v1/chat/completions HTTP/1.1
> Host: 127.0.0.1:8082
> User-Agent: curl/7.88.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 121
>
< HTTP/1.1 200 OK
< Keep-Alive: timeout=5, max=100
< Transfer-Encoding: chunked
< Access-Control-Allow-Origin:
< Server: llama.cpp
< Content-Length: 591
< Content-Type: application/json; charset=utf-8
< Connection: close
<
* Closing connection 0
{"choices":[{"finish_reason":"length","index":0,"message":{"role":"assistant","content":" Hello! I'm"}}],"created":1764699011,"model":"Codestral-22B-v0.1-i1-GGUF","system_fingerprint":"b7274-f57a165eb","object":"chat.completion","usage":{"completion_tokens":5,"prompt_tokens":5,"total_tokens":10},"id":"chatcmpl-6WGgrUU4EWxLfKAXfzUq9hVDLrbybfJm","timings":{"cache_n":4,"prompt_n":1,"prompt_ms":21.409,"prompt_per_token_ms":21.409,"prompt_per_second":46.7093278527722,"predicted_n":5,"predicted_ms":51.717,"predicted_per_token_ms":10.343399999999999,"predicted_per_second":96.68000850784075}}
```

Before: duplicate Server, Transfer-Encoding, Keep-Alive, Access-Control-Allow-Origin, Content-Type
After: all headers appear exactly once

Fixes #17693

Changed files:
filename: tools/server/server-models.cpp
patch: @@ -7,6 +7,7 @@
 #include <sheredom/subprocess.h>
 
 #include <functional>
+#include <algorithm>
 #include <thread>
 #include <mutex>
 #include <condition_variable>
@@ -889,6 +890,28 @@ struct pipe_t {
     }
 };
 
+static std::string to_lower_copy(const std::string & value) {
+    std::string lowered(value.size(), '\0');
+    std::transform(value.begin(), value.end(), lowered.begin(), [](unsigned char c) { return std::tolower(c); });
+    return lowered;
+}
+
+static bool should_strip_proxy_header(const std::string & header_name) {
+    // Headers that get duplicated when router forwards child responses
+    if (header_name == "server" ||
+        header_name == "transfer-encoding" ||
+        header_name == "keep-alive") {
+        return true;
+    }
+
+    // Router injects CORS, child also sends them: duplicate
+    if (header_name.rfind("access-control-", 0) == 0) {
+        return true;
+    }
+
+    return false;
+}
+
 server_http_proxy::server_http_proxy(
         const std::string & method,
         const std::string & host,
@@ -925,14 +948,22 @@ server_http_proxy::server_http_proxy(
         msg_t msg;
         msg.status = response.status;
         for (const auto & [key, value] : response.headers) {
+            const auto lowered = to_lower_copy(key);
+            if (should_strip_proxy_header(lowered)) {
+                continue;
+            }
+            if (lowered == "content-type") {
+                msg.content_type = value;
+                continue;
+            }
             msg.headers[key] = value;
         }
         return pipe->write(std::move(msg)); // send headers first
     };
     httplib::ContentReceiverWithProgress content_receiver = [pipe](const char * data, size_t data_length, size_t, size_t) {
         // send data chunks
         // returns false if pipe is closed / broken (signal to stop receiving)
-        return pipe->write({{}, 0, std::string(data, data_length)});
+        return pipe->write({{}, 0, std::string(data, data_length), ""});
     };
 
     // prepare the request to destination server
@@ -955,21 +986,26 @@ server_http_proxy::server_http_proxy(
         if (result.error() != httplib::Error::Success) {
             auto err_str = httplib::to_string(result.error());
             SRV_ERR("http client error: %s\n", err_str.c_str());
-            pipe->write({{}, 500, ""}); // header
-            pipe->write({{}, 0, "proxy error: " + err_str}); // body
+            pipe->write({{}, 500, "", ""}); // header
+            pipe->write({{}, 0, "proxy error: " + err_str, ""}); // body
         }
         pipe->close_write(); // signal EOF to reader
         SRV_DBG("%s", "client request thread ended\n");
     });
     this->thread.detach();
 
     // wait for the first chunk (headers)
-    msg_t header;
-    if (pipe->read(header, should_stop)) {
-        SRV_DBG("%s", "received response headers\n");
-        this->status  = header.status;
-        this->headers = header.headers;
-    } else {
-        SRV_DBG("%s", "no response headers received (request cancelled?)\n");
+    {
+        msg_t header;
+        if (pipe->read(header, should_stop)) {
+            SRV_DBG("%s", "received response headers\n");
+            this->status  = header.status;
+            this->headers = std::move(header.headers);
+            if (!header.content_type.empty()) {
+                this->content_type = std::move(header.content_type);
+            }
+        } else {
+            SRV_DBG("%s", "no response headers received (request cancelled?)\n");
+        }
     }
 }

filename: tools/server/server-models.h
patch: @@ -170,5 +170,6 @@ struct server_http_proxy : server_http_res {
         std::map<std::string, std::string> headers;
         int status = 0;
         std::string data;
+        std::string content_type;
     };
 };