commit 11185339a9c3ca401fab4179825d1a25f0e0ca5a Author: Luxferre Date: Wed Sep 2 16:19:51 2026 +0300 ini upl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92db623 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Binaries +bin/ +*.exe +*.dll +*.so +*.dylib + +# Test artifacts +*.test +*.out diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a971413 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +# Makefile for k3r053n3 (Kimi K3 LLM Gateway) + +k3r053n3: + go build -trimpath -ldflags="-s -w" -o bin/k3r053n3 . + +test: + go test -v ./... + +all: k3r053n3 + +clean: + rm -rf bin/ + +.PHONY: all clean k3r053n3 test diff --git a/README.md b/README.md new file mode 100644 index 0000000..591018b --- /dev/null +++ b/README.md @@ -0,0 +1,307 @@ +# k3r053n3: Kimi K3 OpenAI-compatible proxy gateway + +A standalone, high-performance, zero-dependency Go proxy gateway that exposes a standard OpenAI-compatible API (`/v1/chat/completions` and `/v1/models`) for the **Kimi K3** model hosted on the [`cw-105/kimi-k3-gguf-demo`](https://cw-105-kimi-k3-gguf-demo.hf.space) Gradio space. + +## Features + +- **Zero external dependencies**: pure Go standard library (`net/http`, `encoding/json`, `bufio`, `bytes`, `crypto/rand`, `flag`, `strings`, `time`). +- **SOCKS5 proxy support**: zero-dependency built-in RFC 1928 / RFC 1929 SOCKS5 client supporting domain name resolution (`socks5h://`), IPv4/IPv6, and username/password authentication (via `-socks`, `-proxy`, or `ALL_PROXY`/`all_proxy`/`SOCKS5_PROXY`/`socks5_proxy` environment variables). +- **Reasoning effort support**: defaults to `"max"` reasoning effort out of the box, with support for `"max"`, `"high"`, `"low"`, and `"default"` (via `reasoning_effort` request field or CLI flag). +- **Real-time token streaming**: Server-Sent Events (SSE) streaming engine (`stream: true`) with separate token-by-token emission for `delta.reasoning_content` and `delta.content`. +- **Reasoning extraction**: clean separation of `...` internal thoughts into `reasoning_content` (streaming chunks and non-streaming messages) without leaking raw tags into `content`. +- **OpenAI-compatible tool calling**: + - automatic tool definition formatting into system instructions. + - multi-turn tool execution response formatting (`role: "tool"` / `role: "function"`). + - real-time stream interceptor (`StreamToolInterceptor`) that catches `` blocks on the fly and emits standard OpenAI `delta.tool_calls` chunks with `finish_reason: "tool_calls"`. + - non-streaming tool call parsing with structured `tool_calls` and `finish_reason: "tool_calls"`. +- **Backend & model routing**: route between multiple upstream backends (`direct:together`, `direct:fireworks`, `hf:together`, `hf:fireworks-ai`, `hf:featherless-ai`, `hf:baseten`) dynamically or via model suffix (`kimi-k3:together`, `kimi-k3:fireworks`, etc.). +- **Reliability & resilience**: automatic Fibonacci exponential backoff retry mechanism (`DoWithFibonacciRetry`) on upstream network connections. +- **Full CORS support**: ready for direct browser integration, web frontends, and OpenAI-compatible client libraries. + +## Quick start + +### Installation + +Install directly with `go install`: + +```bash +go install code.luxferre.top/luxferre/k3r053n3@latest +``` + +### Build from source + +```bash +make k3r053n3 +``` + +Or build manually with Go: + +```bash +go build -trimpath -ldflags="-s -w" -o bin/k3r053n3 . +``` + +### Run + +```bash +./bin/k3r053n3 +``` + +By default, the server starts on port `8080` pointing to `https://cw-105-kimi-k3-gguf-demo.hf.space` with default reasoning effort `"max"` and default backend `"direct:together"`. + +## CLI options + +| Flag | Default | Description | +|||| +| `-port` | `8080` | Port to listen on | +| `-endpoint` | `https://cw-105-kimi-k3-gguf-demo.hf.space` | Root URL of the Kimi K3 Gradio space | +| `-model` | `kimi-k3` | Exposed default model name | +| `-backend` | `direct:together` | Default Space backend (`direct:together`, `direct:fireworks`, `hf:together`, `hf:fireworks-ai`, `hf:featherless-ai`, `hf:baseten`) | +| `-reasoning` | `max` | Default reasoning effort (`max`, `high`, `low`, `default`) | +| `-max-tokens` | `8192` | Default max completion tokens (256 - 8192) | +| `-temperature` | `0.7` | Default sampling temperature (0.0 - 1.5) | +| `-socks`, `-proxy` | `""` | SOCKS5 proxy URL (`socks5://127.0.0.1:1080` or `socks5://user:pass@host:port`, also checks `ALL_PROXY`/`all_proxy`/`SOCKS5_PROXY`/`socks5_proxy` env vars) | +| `-user-agent`, `-ua` | Firefox 153 on Linux | Custom `User-Agent` header for upstream requests | + +## Endpoints + +- `GET /` - gateway health check and route overview +- `GET /v1/models` (or `GET /models`) - list of available models and backend mappings +- `POST /v1/chat/completions` (or `POST /chat/completions`) - OpenAI-compatible chat completions + +## Usage examples + +### 1. Models list + +```bash +curl -s http://localhost:8080/v1/models +``` + +### 2. Standard chat completion (non-streaming) + +```bash +curl -s http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "kimi-k3", + "messages": [ + {"role": "user", "content": "What is 25 * 4? Show brief work."} + ], + "stream": false + }' +``` + +Response includes separated `reasoning_content` and `content`: + +```json +{ + "id": "chatcmpl-88054d2e-2084-4bcd-b9fa-8e99e9267523", + "object": "chat.completion", + "created": 1788348384, + "model": "kimi-k3", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "**25 × 4 = 100**\n\nQuick way: 25 × 4 = 25 × 2 × 2 = 50 × 2 = **100**\n\n(Think of it as 4 quarters = 1 dollar.)", + "reasoning_content": "The user is asking a simple arithmetic question: 25 * 4..." + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0 + } +} +``` + +### 3. Real-time streaming with reasoning deltas + +```bash +curl -s -N http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "kimi-k3", + "messages": [ + {"role": "user", "content": "Tell me a 1-sentence joke about computers."} + ], + "stream": true + }' +``` + +Output delivers real-time `delta.reasoning_content` chunks during thinking, followed by `delta.content` chunks for the answer, ending with `[DONE]`: + +``` +data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1788348397,"model":"kimi-k3","choices":[{"index":0,"delta":{"reasoning_content":"Thinking..."}}]} +... +data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1788348397,"model":"kimi-k3","choices":[{"index":0,"delta":{"content":"There are only 10 types of people in the world: those who understand binary and those who don't."}}]} +data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1788348397,"model":"kimi-k3","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} +data: [DONE] +``` + +### 4. Reasoning effort control + +Control reasoning effort via the `reasoning_effort` field (`"max"`, `"high"`, `"low"`, `"default"`): + +```bash +curl -s http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "kimi-k3", + "reasoning_effort": "low", + "messages": [ + {"role": "user", "content": "Hello!"} + ] + }' +``` + +### 5. Tool / function calling + +```bash +curl -s http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "kimi-k3", + "messages": [ + {"role": "user", "content": "What is the weather in Seattle right now?"} + ], + "tools": [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get current weather for a given city", + "parameters": { + "type": "object", + "properties": { + "location": {"type": "string", "description": "City name"} + }, + "required": ["location"] + } + } + } + ], + "stream": false + }' +``` + +Response emits standard OpenAI `tool_calls` with `finish_reason: "tool_calls"`: + +```json +{ + "id": "chatcmpl-cbe006c5-1b5d-42d3-ab89-254c7f012061", + "object": "chat.completion", + "created": 1788348416, + "model": "kimi-k3", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "reasoning_content": "The user is asking about the weather in Seattle...", + "tool_calls": [ + { + "index": 0, + "id": "call_de6cdf1a_0", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"location\":\"Seattle\"}" + } + } + ] + }, + "finish_reason": "tool_calls" + } + ] +} +``` + +### 6. Submitting tool results in follow-up turns + +```bash +curl -s http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "kimi-k3", + "messages": [ + {"role": "user", "content": "What is the weather in Seattle right now?"}, + { + "role": "assistant", + "tool_calls": [ + { + "id": "call_de6cdf1a_0", + "type": "function", + "function": {"name": "get_weather", "arguments": "{\"location\":\"Seattle\"}"} + } + ] + }, + { + "role": "tool", + "tool_call_id": "call_de6cdf1a_0", + "name": "get_weather", + "content": "{\"temperature\": \"16C\", \"conditions\": \"Partly cloudy with gentle breeze\"}" + } + ] + }' +``` + +## Python OpenAI client integration + +```python +from openai import OpenAI + +client = OpenAI( + base_url="http://localhost:8080/v1", + api_key="not-needed", +) + +# Streaming with reasoning +response = client.chat.completions.create( + model="kimi-k3", + messages=[ + {"role": "user", "content": "Explain quantum superposition in 2 sentences."} + ], + stream=True, + extra_body={"reasoning_effort": "max"}, +) + +for chunk in response: + delta = chunk.choices[0].delta + if hasattr(delta, "reasoning_content") and delta.reasoning_content: + print(delta.reasoning_content, end="", flush=True) + if delta.content: + print(delta.content, end="", flush=True) +print() +``` + +## SOCKS5 proxy usage + +Run the gateway through a SOCKS5 proxy (e.g. Tor or local tunnel): + +```bash +# Using CLI flag +./bin/k3r053n3 -socks socks5://127.0.0.1:9050 + +# With authentication +./bin/k3r053n3 -socks socks5://user:pass@127.0.0.1:1080 + +# Using environment variable +export ALL_PROXY=socks5://127.0.0.1:1080 +./bin/k3r053n3 +``` + +## Testing + +Run unit and integration tests: + +```bash +make test +``` + +## Credits + +Created by Luxferre in 2026, released into the public domain with no warranties. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..57d711d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module code.luxferre.top/luxferre/k3r053n3 + +go 1.26.5 diff --git a/k3r053n3_test.go b/k3r053n3_test.go new file mode 100644 index 0000000..3d5d738 --- /dev/null +++ b/k3r053n3_test.go @@ -0,0 +1,396 @@ +// k3r053n3_test: Test suite for Kimi K3 Gateway +// Created by Luxferre in 2026, released into the public domain + +package main + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestModelsEndpoint(t *testing.T) { + svc := NewKimiService("https://test.hf.space", "kimi-k3", "direct:together", "max", "", 8192, 0.7) + req, w := httptest.NewRequest("GET", "/v1/models", nil), httptest.NewRecorder() + NewMux(svc).ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", w.Code) + } + var res ModelsResponse + if err := json.NewDecoder(w.Body).Decode(&res); err != nil || len(res.Data) == 0 { + t.Fatalf("invalid models response: %v", err) + } + for _, m := range res.Data { + if m.ID == "kimi-k3" { + return + } + } + t.Fatalf("expected kimi-k3 in models list") +} + +func TestResolveReasoningEffort(t *testing.T) { + tests := []struct{ in, def, exp string }{ + {"", "max", "max"}, {"max", "default", "max"}, {"HIGH", "low", "high"}, + {"low", "max", "low"}, {"medium", "max", "default"}, {"default", "max", "default"}, {"unknown", "max", "max"}, + } + for _, tt := range tests { + if res := ResolveReasoningEffort(ChatCompletionRequest{ReasoningEffort: tt.in}, tt.def); res != tt.exp { + t.Errorf("ResolveReasoningEffort(%q, %q) = %q, want %q", tt.in, tt.def, res, tt.exp) + } + } +} + +func TestResolveBackend(t *testing.T) { + svc := NewKimiService("https://test.hf.space", "kimi-k3", "direct:together", "max", "", 8192, 0.7) + tests := []struct{ model, exp string }{ + {"kimi-k3", "direct:together"}, {"kimi-k3:fireworks", "direct:fireworks"}, + {"kimi-k3:together", "direct:together"}, {"kimi-k3:hf-together", "hf:together"}, + {"kimi-k3:hf-fireworks", "hf:fireworks-ai"}, {"kimi-k3:hf-featherless", "hf:featherless-ai"}, + {"kimi-k3:hf-baseten", "hf:baseten"}, {"custom-model", "direct:together"}, + } + for _, tt := range tests { + if b := svc.ResolveBackend(tt.model); b != tt.exp { + t.Errorf("ResolveBackend(%q) = %q, want %q", tt.model, b, tt.exp) + } + } +} + +func TestParseSOCKS5URL(t *testing.T) { + tests := []struct { + in string + exp *SOCKS5Config + }{ + {"", nil}, + {"127.0.0.1:1080", &SOCKS5Config{Address: "127.0.0.1:1080"}}, + {"socks5://127.0.0.1:9050", &SOCKS5Config{Address: "127.0.0.1:9050"}}, + {"socks5h://user:pass@proxy.example.com:1080", &SOCKS5Config{Address: "proxy.example.com:1080", Username: "user", Password: "pass"}}, + {"socks5://myuser@10.0.0.1:1080", &SOCKS5Config{Address: "10.0.0.1:1080", Username: "myuser"}}, + {"127.0.0.1", &SOCKS5Config{Address: "127.0.0.1:1080"}}, + } + for _, tt := range tests { + cfg, err := ParseSOCKS5URL(tt.in) + if err != nil { + t.Errorf("ParseSOCKS5URL(%q) err: %v", tt.in, err) + } else if tt.exp == nil && cfg != nil { + t.Errorf("ParseSOCKS5URL(%q) expected nil, got %+v", tt.in, cfg) + } else if tt.exp != nil && (cfg.Address != tt.exp.Address || cfg.Username != tt.exp.Username || cfg.Password != tt.exp.Password) { + t.Errorf("ParseSOCKS5URL(%q) = %+v, want %+v", tt.in, cfg, tt.exp) + } + } +} + +func startMockSOCKS5Server(t *testing.T, expectedUser, expectedPass string) (string, func()) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to start mock socks5 listener: %v", err) + } + stopCh := make(chan struct{}) + + go func() { + for { + conn, err := ln.Accept() + if err != nil { + return + } + go func(c net.Conn) { + defer c.Close() + hdr := make([]byte, 2) + if _, err := io.ReadFull(c, hdr); err != nil || hdr[0] != 0x05 { + return + } + methods := make([]byte, int(hdr[1])) + if _, err := io.ReadFull(c, methods); err != nil { + return + } + + if expectedUser != "" { + c.Write([]byte{0x05, 0x02}) + authHdr := make([]byte, 2) + if _, err := io.ReadFull(c, authHdr); err != nil { + return + } + userBuf := make([]byte, int(authHdr[1])) + io.ReadFull(c, userBuf) + pLenBuf := make([]byte, 1) + io.ReadFull(c, pLenBuf) + passBuf := make([]byte, int(pLenBuf[0])) + io.ReadFull(c, passBuf) + + if string(userBuf) != expectedUser || string(passBuf) != expectedPass { + c.Write([]byte{0x01, 0x01}) + return + } + c.Write([]byte{0x01, 0x00}) + } else { + c.Write([]byte{0x05, 0x00}) + } + + reqHdr := make([]byte, 4) + if _, err := io.ReadFull(c, reqHdr); err != nil || reqHdr[0] != 0x05 || reqHdr[1] != 0x01 { + return + } + var targetAddr string + switch reqHdr[3] { + case 0x01: + ipBuf := make([]byte, 4) + io.ReadFull(c, ipBuf) + targetAddr = net.IP(ipBuf).String() + case 0x03: + lBuf := make([]byte, 1) + io.ReadFull(c, lBuf) + dBuf := make([]byte, int(lBuf[0])) + io.ReadFull(c, dBuf) + targetAddr = string(dBuf) + } + portBuf := make([]byte, 2) + io.ReadFull(c, portBuf) + port := (int(portBuf[0]) << 8) | int(portBuf[1]) + + remote, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", targetAddr, port), 5*time.Second) + if err != nil { + c.Write([]byte{0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0}) + return + } + defer remote.Close() + c.Write([]byte{0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, 0x1f, 0x90}) + go io.Copy(remote, c) + io.Copy(c, remote) + }(conn) + } + }() + return ln.Addr().String(), func() { close(stopCh); ln.Close() } +} + +func TestSOCKS5EndToEnd(t *testing.T) { + mockTarget := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/test" { + w.Write([]byte("socks5-ok")) + return + } + http.NotFound(w, r) + })) + defer mockTarget.Close() + + targetHostPort := strings.TrimPrefix(mockTarget.URL, "http://") + proxyAddr, cleanupProxy := startMockSOCKS5Server(t, "alice", "secret123") + defer cleanupProxy() + + proxyURL := fmt.Sprintf("socks5://alice:secret123@%s", proxyAddr) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + conn, err := DialSOCKS5(ctx, proxyURL, targetHostPort) + if err != nil { + t.Fatalf("DialSOCKS5 failed: %v", err) + } + defer conn.Close() + + fmt.Fprintf(conn, "GET /test HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", targetHostPort) + respBody, err := io.ReadAll(conn) + if err != nil || !strings.Contains(string(respBody), "socks5-ok") { + t.Fatalf("socks5 direct dial failed: %v, got: %s", err, string(respBody)) + } + + svc := NewKimiService(mockTarget.URL, "kimi-k3", "direct:together", "max", proxyURL, 8192, 0.7) + req, _ := http.NewRequest("GET", mockTarget.URL+"/test", nil) + resp, err := svc.client.Do(req) + if err != nil { + t.Fatalf("svc.client via SOCKS5 failed: %v", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if string(body) != "socks5-ok" { + t.Fatalf("expected 'socks5-ok', got: %s", string(body)) + } +} + +func TestParseGradioSnapshot(t *testing.T) { + // Test 1: Standard reasoning done and answer present with internal tag in reasoning + sample1 := `[[ + {"role":"user","metadata":null,"content":[{"text":"Hi","type":"text"}],"options":null}, + {"role":"assistant","metadata":null,"content":[{"text":"<","type":"text"}],"options":null}, + {"role":"assistant","metadata":{"title":"Reasoning","status":"done"},"content":[{"text":">I am thinking about and tags.\n\nHello there!","type":"text"}],"options":null} + ], {"value":{"text":"","files":[]},"__type__":"update"}]` + + content, reasoning, ok := parseGradioSnapshot(sample1) + if !ok || content != "Hello there!" || reasoning != "I am thinking about and tags." { + t.Fatalf("parseGradioSnapshot sample1 failed: content=%q, reasoning=%q", content, reasoning) + } + + // Test 2: Reasoning still pending + sample2 := `[[ + {"role":"user","metadata":null,"content":[{"text":"Hi","type":"text"}],"options":null}, + {"role":"assistant","metadata":null,"content":[{"text":"<","type":"text"}],"options":null}, + {"role":"assistant","metadata":{"title":"Reasoning","status":"pending"},"content":[{"text":">Still reasoning","type":"text"}],"options":null} + ], {"value":{"text":"","files":[]},"__type__":"update"}]` + + content2, reasoning2, ok2 := parseGradioSnapshot(sample2) + if !ok2 || content2 != "" || reasoning2 != "Still reasoning" { + t.Fatalf("parseGradioSnapshot sample2 failed: content=%q, reasoning=%q", content2, reasoning2) + } + + // Test 3: Interleaved messages + sample3 := `[[ + {"role":"user","metadata":null,"content":[{"text":"Hi","type":"text"}]}, + {"role":"assistant","metadata":null,"content":[{"text":"<"}]}, + {"role":"assistant","metadata":{"title":"Reasoning","status":"done"},"content":[{"text":">Part 1"}]}, + {"role":"assistant","metadata":null,"content":[{"text":"intro text: "}]}, + {"role":"assistant","metadata":{"title":"Reasoning","status":"done"},"content":[{"text":". Part 2\n\n{\"name\":\"fn\"}"}]} + ]]` + content3, reasoning3, ok3 := parseGradioSnapshot(sample3) + if !ok3 || !strings.Contains(content3, "") || !strings.Contains(reasoning3, "Part 1. Part 2") { + t.Fatalf("parseGradioSnapshot sample3 failed: content=%q, reasoning=%q", content3, reasoning3) + } +} + +func TestDetectToolCalls(t *testing.T) { + // 1. Standard XML tag with conversational text + text1 := "Here is the weather:\n\n{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Tokyo\"}}\n\nDone." + tcs1, cleaned1, ok1 := DetectToolCalls(text1) + if !ok1 || len(tcs1) != 1 || tcs1[0].Function.Name != "get_weather" || !strings.Contains(tcs1[0].Function.Arguments, "Tokyo") { + t.Fatalf("DetectToolCalls 1 failed: ok=%v, tcs=%+v", ok1, tcs1) + } + if strings.Contains(cleaned1, "") || !strings.Contains(cleaned1, "Here is the weather:") { + t.Fatalf("DetectToolCalls cleaned 1 invalid: %q", cleaned1) + } + + // 2. tool-call variant + text2 := "\n{\"name\": \"calc\", \"arguments\": \"{\\\"x\\\": 1}\"}\n" + tcs2, _, ok2 := DetectToolCalls(text2) + if !ok2 || len(tcs2) != 1 || tcs2[0].Function.Name != "calc" { + t.Fatalf("DetectToolCalls 2 failed: ok=%v, tcs=%+v", ok2, tcs2) + } + + // 3. markdown codeblock format + text3 := "```tool_call\n{\"name\": \"search\", \"query\": \"golang\"}\n```" + tcs3, _, ok3 := DetectToolCalls(text3) + if !ok3 || len(tcs3) != 1 || tcs3[0].Function.Name != "search" { + t.Fatalf("DetectToolCalls 3 failed: ok=%v, tcs=%+v", ok3, tcs3) + } + + // 4. unclosed tag at end of response + text4 := "I will query:\n\n{\"name\": \"fetch_data\", \"id\": 123}" + tcs4, cleaned4, ok4 := DetectToolCalls(text4) + if !ok4 || len(tcs4) != 1 || tcs4[0].Function.Name != "fetch_data" || cleaned4 != "I will query:" { + t.Fatalf("DetectToolCalls 4 unclosed failed: ok=%v, tcs=%+v, cleaned=%q", ok4, tcs4, cleaned4) + } + + // 5. function call syntax: get_weather(location="Tokyo") + text5 := "get_weather(location=\"Tokyo\")" + tcs5, _, ok5 := DetectToolCalls(text5) + if !ok5 || len(tcs5) != 1 || tcs5[0].Function.Name != "get_weather" || !strings.Contains(tcs5[0].Function.Arguments, "Tokyo") { + t.Fatalf("DetectToolCalls 5 function syntax failed: ok=%v, tcs=%+v", ok5, tcs5) + } + + // 6. markdown ```json with tool payload + text6 := "```json\n{\"name\": \"lookup\", \"arguments\": {\"id\": 42}}\n```" + tcs6, _, ok6 := DetectToolCalls(text6) + if !ok6 || len(tcs6) != 1 || tcs6[0].Function.Name != "lookup" { + t.Fatalf("DetectToolCalls 6 json block failed: ok=%v, tcs=%+v", ok6, tcs6) + } +} + +func TestStreamToolInterceptor(t *testing.T) { + w := httptest.NewRecorder() + streamer := NewStreamer(w, nil, "test-id", 123456789, "kimi-k3") + interceptor := NewStreamToolInterceptor(streamer) + + interceptor.ProcessContentDelta("Checking weather for you...\n\n{\"name\": \"get_weather\", \"arguments\": {\"city\": \"SF\"}}\n\nHave a nice day!") + interceptor.FlushRemaining() + + if !interceptor.HasToolCalls() { + t.Fatalf("expected tool call registered in interceptor") + } + body := w.Body.String() + if !strings.Contains(body, "Checking weather for you...") || !strings.Contains(body, "get_weather") || strings.Contains(body, "") { + t.Fatalf("unexpected stream body: %s", body) + } +} + +func TestEndToEndMockChatCompletion(t *testing.T) { + mockGradio := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/gradio_api/call/on_submit" && r.Method == "POST" { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(GradioJoinResponse{EventID: "evt_12345"}) + return + } + if r.URL.Path == "/gradio_api/call/on_submit/evt_12345" && r.Method == "GET" { + w.Header().Set("Content-Type", "text/event-stream") + flusher, _ := w.(http.Flusher) + fmt.Fprintf(w, "event: generating\ndata: [[{\"role\":\"user\",\"metadata\":null,\"content\":[{\"text\":\"Hi\"}],\"options\":null},{\"role\":\"assistant\",\"metadata\":null,\"content\":[{\"text\":\"<\"}],\"options\":null},{\"role\":\"assistant\",\"metadata\":{\"title\":\"Reasoning\",\"status\":\"done\"},\"content\":[{\"text\":\">Thinking\"}],\"options\":null}]]\n\n") + if flusher != nil { + flusher.Flush() + } + fmt.Fprintf(w, "event: complete\ndata: [[{\"role\":\"user\",\"metadata\":null,\"content\":[{\"text\":\"Hi\"}],\"options\":null},{\"role\":\"assistant\",\"metadata\":null,\"content\":[{\"text\":\"<\"}],\"options\":null},{\"role\":\"assistant\",\"metadata\":{\"title\":\"Reasoning\",\"status\":\"done\"},\"content\":[{\"text\":\">Thinking\\n\\nHello!\"}],\"options\":null}]]\n\n") + if flusher != nil { + flusher.Flush() + } + return + } + http.NotFound(w, r) + })) + defer mockGradio.Close() + + svc := NewKimiService(mockGradio.URL, "kimi-k3", "direct:together", "max", "", 8192, 0.7) + mux := NewMux(svc) + + // 1. Non-Streaming + reqB, _ := json.Marshal(ChatCompletionRequest{Model: "kimi-k3", Messages: []ChatMessage{{Role: "user", Content: "Hi"}}, Stream: false}) + req := httptest.NewRequest("POST", "/v1/chat/completions", bytes.NewBuffer(reqB)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + + var resp ChatCompletionResponse + if err := json.NewDecoder(w.Body).Decode(&resp); err != nil || len(resp.Choices) == 0 { + t.Fatalf("non-streaming chat failed: %v", err) + } + if resp.Choices[0].Message.ReasoningContent != "Thinking" || resp.Choices[0].Message.GetContentString() != "Hello!" { + t.Errorf("unexpected choice response: %+v", resp.Choices[0]) + } + + // 2. Streaming + reqBStream, _ := json.Marshal(ChatCompletionRequest{Model: "kimi-k3", Messages: []ChatMessage{{Role: "user", Content: "Hi"}}, Stream: true}) + reqStream := httptest.NewRequest("POST", "/v1/chat/completions", bytes.NewBuffer(reqBStream)) + reqStream.Header.Set("Content-Type", "application/json") + wStream := httptest.NewRecorder() + mux.ServeHTTP(wStream, reqStream) + + scanner := bufio.NewScanner(wStream.Body) + hasReasoning, hasContent, hasDone := false, false, false + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "data: ") { + data := strings.TrimPrefix(line, "data: ") + if data == "[DONE]" { + hasDone = true + continue + } + var chunk StreamResponse + if err := json.Unmarshal([]byte(data), &chunk); err == nil && len(chunk.Choices) > 0 { + if chunk.Choices[0].Delta.ReasoningContent != "" { + hasReasoning = true + } + if chunk.Choices[0].Delta.Content != "" { + hasContent = true + } + } + } + } + if !hasReasoning || !hasContent || !hasDone { + t.Errorf("streaming test failed: reasoning=%v, content=%v, done=%v", hasReasoning, hasContent, hasDone) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f3d5aa4 --- /dev/null +++ b/main.go @@ -0,0 +1,1472 @@ +// k3r053n3: Standalone OpenAI-compatible gateway for Kimi K3 (cw-105-kimi-k3-gguf-demo) +// Created by Luxferre in 2026, released into the public domain + +package main + +import ( + "bufio" + "bytes" + "context" + "crypto/rand" + "encoding/json" + "flag" + "fmt" + "io" + "net" + "net/http" + "os" + "regexp" + "strconv" + "strings" + "time" +) + +var ( + DefaultUserAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:153.0) Gecko/20100101 Firefox/153.0" + ConfiguredUserAgent string + fnCallSyntaxRegex = regexp.MustCompile(`^([a-zA-Z0-9_\-\.]+)\s*\((.*)\)$`) + kvPairRegex = regexp.MustCompile(`([a-zA-Z0-9_]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^,\s]+))`) + toolTagRegexes = []*regexp.Regexp{ + regexp.MustCompile(`(?s)\s*(.*?)\s*`), + regexp.MustCompile(`(?s)\s*(.*?)\s*`), + regexp.MustCompile(`(?s)\s*(.*?)\s*`), + regexp.MustCompile(`(?s)\s*(.*?)\s*`), + regexp.MustCompile(`(?s)\s*(.*?)\s*`), + regexp.MustCompile("(?s)```tool_call\\s*(.*?)\\s*```"), + regexp.MustCompile("(?s)```tool-call\\s*(.*?)\\s*```"), + regexp.MustCompile("(?s)```json\\s*(.*?)\\s*```"), + } +) + +// --------------------------------------------------------------------------- +// OpenAI API Data Structures +// --------------------------------------------------------------------------- + +type ModelItem struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + OwnedBy string `json:"owned_by"` +} + +type ModelsResponse struct { + Object string `json:"object"` + Data []ModelItem `json:"data"` +} + +type ToolCallFunction struct { + Name string `json:"name"` + Arguments string `json:"arguments"` +} + +type ToolCall struct { + Index *int `json:"index,omitempty"` + ID string `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Function ToolCallFunction `json:"function"` +} + +type Tool struct { + Type string `json:"type"` + Function interface{} `json:"function"` +} + +type ChatMessage struct { + Role string `json:"role"` + Content interface{} `json:"content"` + ReasoningContent string `json:"reasoning_content,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` + Name string `json:"name,omitempty"` +} + +func (m *ChatMessage) GetContentString() string { + switch v := m.Content.(type) { + case string: + return v + case []interface{}: + var sb strings.Builder + for _, p := range v { + if s, ok := p.(string); ok { + sb.WriteString(s) + } else if tm, ok := p.(map[string]interface{}); ok { + if t, ok := tm["text"].(string); ok { + sb.WriteString(t) + } + } + } + return sb.String() + default: + if m.Content == nil { + return "" + } + b, _ := json.Marshal(m.Content) + return string(b) + } +} + +type ChatCompletionRequest struct { + Model string `json:"model"` + Messages []ChatMessage `json:"messages"` + Tools []Tool `json:"tools,omitempty"` + Functions []interface{} `json:"functions,omitempty"` + ToolChoice interface{} `json:"tool_choice,omitempty"` + FunctionCall interface{} `json:"function_call,omitempty"` + Stream bool `json:"stream"` + MaxTokens int `json:"max_tokens"` + MaxCompletionTokens int `json:"max_completion_tokens"` + Temperature *float64 `json:"temperature,omitempty"` + ReasoningEffort string `json:"reasoning_effort,omitempty"` +} + +type ChatCompletionResponseChoice struct { + Index int `json:"index"` + Message ChatMessage `json:"message"` + FinishReason string `json:"finish_reason"` +} + +type Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` +} + +type ChatCompletionResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []ChatCompletionResponseChoice `json:"choices"` + Usage Usage `json:"usage"` +} + +type StreamDelta struct { + Role string `json:"role,omitempty"` + Content string `json:"content,omitempty"` + ReasoningContent string `json:"reasoning_content,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` +} + +type StreamChoice struct { + Index int `json:"index"` + Delta StreamDelta `json:"delta"` + FinishReason *string `json:"finish_reason,omitempty"` +} + +type StreamResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []StreamChoice `json:"choices"` +} + +type GradioJoinResponse struct { + EventID string `json:"event_id"` +} + +// --------------------------------------------------------------------------- +// Zero-Dependency Pure Go SOCKS5 Proxy Client (RFC 1928 / RFC 1929) +// --------------------------------------------------------------------------- + +type SOCKS5Config struct { + Address, Username, Password string +} + +func ParseSOCKS5URL(proxyURL string) (*SOCKS5Config, error) { + u := strings.TrimSpace(proxyURL) + if u == "" { + return nil, nil + } + u = strings.TrimPrefix(strings.TrimPrefix(u, "socks5h://"), "socks5://") + cfg := &SOCKS5Config{} + if at := strings.LastIndex(u, "@"); at != -1 { + userPass := u[:at] + cfg.Address = u[at+1:] + if col := strings.Index(userPass, ":"); col != -1 { + cfg.Username, cfg.Password = userPass[:col], userPass[col+1:] + } else { + cfg.Username = userPass + } + } else { + cfg.Address = u + } + if !strings.Contains(cfg.Address, ":") { + cfg.Address += ":1080" + } + return cfg, nil +} + +func DialSOCKS5(ctx context.Context, proxyURL, targetAddr string) (net.Conn, error) { + cfg, err := ParseSOCKS5URL(proxyURL) + if err != nil || cfg == nil { + return (&net.Dialer{}).DialContext(ctx, "tcp", targetAddr) + } + conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", cfg.Address) + if err != nil { + return nil, fmt.Errorf("socks5 dial failed: %w", err) + } + if d, ok := ctx.Deadline(); ok { + conn.SetDeadline(d) + } else { + conn.SetDeadline(time.Now().Add(30 * time.Second)) + } + defer conn.SetDeadline(time.Time{}) + + greeting := []byte{0x05, 0x01, 0x00} + if cfg.Username != "" { + greeting = []byte{0x05, 0x02, 0x00, 0x02} + } + if _, err := conn.Write(greeting); err != nil { + conn.Close() + return nil, err + } + resp := make([]byte, 2) + if _, err := io.ReadFull(conn, resp); err != nil || resp[0] != 0x05 { + conn.Close() + return nil, fmt.Errorf("socks5 greeting failed") + } + + if resp[1] == 0x02 { + req := append(append([]byte{0x01, byte(len(cfg.Username))}, cfg.Username...), byte(len(cfg.Password))) + req = append(req, cfg.Password...) + if _, err := conn.Write(req); err != nil { + conn.Close() + return nil, err + } + if _, err := io.ReadFull(conn, resp); err != nil || resp[1] != 0x00 { + conn.Close() + return nil, fmt.Errorf("socks5 auth failed") + } + } else if resp[1] != 0x00 { + conn.Close() + return nil, fmt.Errorf("socks5 auth rejected: 0x%02x", resp[1]) + } + + host, portStr, err := net.SplitHostPort(targetAddr) + if err != nil { + conn.Close() + return nil, err + } + port, _ := strconv.Atoi(portStr) + + req := []byte{0x05, 0x01, 0x00} + ip := net.ParseIP(host) + if ip4 := ip.To4(); ip4 != nil { + req = append(append(req, 0x01), ip4...) + } else if ip6 := ip.To16(); ip6 != nil { + req = append(append(req, 0x04), ip6...) + } else { + req = append(append(req, 0x03, byte(len(host))), host...) + } + req = append(req, byte(port>>8), byte(port&0xFF)) + + if _, err := conn.Write(req); err != nil { + conn.Close() + return nil, err + } + + respHdr := make([]byte, 4) + if _, err := io.ReadFull(conn, respHdr); err != nil || respHdr[1] != 0x00 { + conn.Close() + return nil, fmt.Errorf("socks5 connect failed: 0x%02x", respHdr[1]) + } + + switch respHdr[3] { + case 0x01: + io.ReadFull(conn, make([]byte, 6)) + case 0x03: + lb := make([]byte, 1) + io.ReadFull(conn, lb) + io.ReadFull(conn, make([]byte, int(lb[0])+2)) + case 0x04: + io.ReadFull(conn, make([]byte, 18)) + } + return conn, nil +} + +// --------------------------------------------------------------------------- +// Helpers & Tool Calling +// --------------------------------------------------------------------------- + +func GenerateUUID() string { + var b [16]byte + rand.Read(b[:]) + b[6], b[8] = (b[6]&0x0f)|0x40, (b[8]&0x3f)|0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) +} + +func DoWithFibonacciRetry(client *http.Client, makeReq func() (*http.Request, error), maxRetries int) (*http.Response, error) { + var lastErr error + a, b := 1, 1 + for attempt := 1; attempt <= maxRetries; attempt++ { + req, err := makeReq() + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err == nil && resp.StatusCode == http.StatusOK { + return resp, nil + } + if resp != nil { + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + lastErr = fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) + } else { + lastErr = err + } + if attempt < maxRetries { + time.Sleep(time.Duration(a) * time.Second) + a, b = b, a+b + } + } + return nil, fmt.Errorf("request failed after %d retries: %v", maxRetries, lastErr) +} + +func EnableCORS(w http.ResponseWriter) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, api-key, X-User-Agent") +} + +func ResolveMaxTokens(req ChatCompletionRequest, def int) int { + mt := req.MaxTokens + if mt == 0 && req.MaxCompletionTokens > 0 { + mt = req.MaxCompletionTokens + } + if mt <= 0 { + mt = def + } + if mt < 256 { + return 256 + } + if mt > 8192 { + return 8192 + } + return mt +} + +func ResolveTemperature(req ChatCompletionRequest, def float64) float64 { + if req.Temperature != nil { + t := *req.Temperature + if t < 0.0 { + return 0.0 + } + if t > 1.5 { + return 1.5 + } + return t + } + return def +} + +func ResolveReasoningEffort(req ChatCompletionRequest, def string) string { + e := strings.ToLower(strings.TrimSpace(req.ReasoningEffort)) + if e == "" { + e = strings.ToLower(strings.TrimSpace(def)) + } + switch e { + case "max", "high", "low": + return e + case "default", "medium", "none", "off": + return "default" + default: + return "max" + } +} + +func EffectiveUserAgent(r *http.Request) string { + if r != nil { + if c := r.Header.Get("X-User-Agent"); c != "" { + return c + } + } + if ConfiguredUserAgent != "" { + return ConfiguredUserAgent + } + return DefaultUserAgent +} + +func formatArgumentsString(args interface{}) string { + if args == nil { + return "{}" + } + if str, ok := args.(string); ok { + return str + } + b, err := json.Marshal(args) + if err != nil { + return "{}" + } + return string(b) +} + +func parseFnArgsString(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return "{}" + } + var m map[string]interface{} + if err := json.Unmarshal([]byte(raw), &m); err == nil { + b, _ := json.Marshal(m) + return string(b) + } + kvMap := make(map[string]interface{}) + for _, match := range kvPairRegex.FindAllStringSubmatch(raw, -1) { + k := match[1] + v := match[2] + if v == "" { + v = match[3] + } + if v == "" { + v = match[4] + } + kvMap[k] = v + } + if len(kvMap) > 0 { + b, _ := json.Marshal(kvMap) + return string(b) + } + b, _ := json.Marshal(map[string]string{"input": raw}) + return string(b) +} + +func BuildToolsSystemPrompt(tools []Tool, toolChoice interface{}) string { + if len(tools) == 0 { + return "" + } + b, _ := json.MarshalIndent(tools, "", " ") + + choiceInstruction := "" + if tcStr, ok := toolChoice.(string); ok { + if tcStr == "required" { + choiceInstruction = "\nIMPORTANT: You MUST invoke at least one tool to satisfy the request." + } + } else if tcMap, ok := toolChoice.(map[string]interface{}); ok { + if fn, ok := tcMap["function"].(map[string]interface{}); ok { + if fnName, ok := fn["name"].(string); ok && fnName != "" { + choiceInstruction = fmt.Sprintf("\nIMPORTANT: You MUST call the %q tool.", fnName) + } + } + } + + return fmt.Sprintf("# Tools Available\nYou have access to the following tools:\n%s\n\n# Tool Calling Instructions\n1. When a task requires gathering information, inspecting files, running commands, or executing actions, you MUST emit the ... block in your response.\n2. DO NOT output conversational promises or filler statements (e.g. \"I will analyze the project\", \"Let me check the files\") without emitting the tool call.\n3. Wrap tool calls inside ... XML tags:\n\n{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}\n%s", string(b), choiceInstruction) +} + +func extractToolCallFromMap(m map[string]interface{}, idx int) *ToolCall { + var name string + var args interface{} + + for _, k := range []string{"name", "tool", "tool_name", "function_name", "action"} { + if n, ok := m[k].(string); ok && n != "" { + name = n + break + } + } + if name == "" { + if fn, ok := m["function"].(map[string]interface{}); ok { + for _, k := range []string{"name", "tool", "tool_name", "function_name"} { + if n, ok := fn[k].(string); ok && n != "" { + name = n + args = fn["arguments"] + if args == nil { + args = fn["parameters"] + } + if args == nil { + args = fn["args"] + } + break + } + } + } + } + + if name == "" { + return nil + } + + if args == nil { + for _, k := range []string{"arguments", "parameters", "args", "params", "input", "action_input"} { + if v, exists := m[k]; exists && v != nil { + args = v + break + } + } + } + + if args == nil { + rem := make(map[string]interface{}) + for k, v := range m { + if k != "name" && k != "function" && k != "tool" && k != "tool_name" && k != "function_name" && k != "action" && k != "type" && k != "id" && k != "index" { + rem[k] = v + } + } + if len(rem) > 0 { + args = rem + } else { + args = map[string]interface{}{} + } + } + + id := fmt.Sprintf("call_%s_%d", GenerateUUID()[:8], idx) + if existingID, ok := m["id"].(string); ok && existingID != "" { + id = existingID + } + + return &ToolCall{ + Index: &idx, + ID: id, + Type: "function", + Function: ToolCallFunction{ + Name: name, + Arguments: formatArgumentsString(args), + }, + } +} + +func parseRawToolPayload(raw string) []ToolCall { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil + } + + var arr []map[string]interface{} + if err := json.Unmarshal([]byte(raw), &arr); err == nil { + var res []ToolCall + for _, item := range arr { + if tc := extractToolCallFromMap(item, len(res)); tc != nil { + res = append(res, *tc) + } + } + if len(res) > 0 { + return res + } + } + + var single map[string]interface{} + if err := json.Unmarshal([]byte(raw), &single); err == nil { + if tcList, ok := single["tool_calls"].([]interface{}); ok { + var res []ToolCall + for _, item := range tcList { + if m, ok := item.(map[string]interface{}); ok { + if tc := extractToolCallFromMap(m, len(res)); tc != nil { + res = append(res, *tc) + } + } + } + if len(res) > 0 { + return res + } + } + if tc := extractToolCallFromMap(single, 0); tc != nil { + return []ToolCall{*tc} + } + } + + if fnMatch := fnCallSyntaxRegex.FindStringSubmatch(raw); len(fnMatch) >= 3 { + fnName := fnMatch[1] + argsRaw := strings.TrimSpace(fnMatch[2]) + idx := 0 + return []ToolCall{{ + Index: &idx, + ID: fmt.Sprintf("call_%s_0", GenerateUUID()[:8]), + Type: "function", + Function: ToolCallFunction{ + Name: fnName, + Arguments: parseFnArgsString(argsRaw), + }, + }} + } + + return nil +} + +func DetectToolCalls(text string) ([]ToolCall, string, bool) { + var toolCalls []ToolCall + cleaned := text + + for _, re := range toolTagRegexes { + matches := re.FindAllStringSubmatchIndex(cleaned, -1) + if len(matches) == 0 { + continue + } + var sb strings.Builder + lastIdx := 0 + foundInRe := false + for _, m := range matches { + raw := strings.TrimSpace(cleaned[m[2]:m[3]]) + parsed := parseRawToolPayload(raw) + if len(parsed) > 0 { + foundInRe = true + sb.WriteString(cleaned[lastIdx:m[0]]) + lastIdx = m[1] + for _, tc := range parsed { + idx := len(toolCalls) + tc.Index = &idx + toolCalls = append(toolCalls, tc) + } + } + } + if foundInRe { + sb.WriteString(cleaned[lastIdx:]) + cleaned = sb.String() + } + } + + if len(toolCalls) == 0 { + for _, tag := range []string{"", "", "", "", ""} { + if idx := strings.Index(cleaned, tag); idx != -1 { + raw := strings.TrimSpace(cleaned[idx+len(tag):]) + for _, tc := range parseRawToolPayload(raw) { + idxTC := len(toolCalls) + tc.Index = &idxTC + toolCalls = append(toolCalls, tc) + } + if len(toolCalls) > 0 { + cleaned = strings.TrimSpace(cleaned[:idx]) + break + } + } + } + } + + return toolCalls, strings.TrimSpace(cleaned), len(toolCalls) > 0 +} + +// --------------------------------------------------------------------------- +// Streaming & Snapshot Parser +// --------------------------------------------------------------------------- + +type Streamer struct { + w http.ResponseWriter + flusher http.Flusher + id string + created int64 + model string +} + +func NewStreamer(w http.ResponseWriter, flusher http.Flusher, id string, created int64, model string) *Streamer { + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + return &Streamer{w: w, flusher: flusher, id: id, created: created, model: model} +} + +func (s *Streamer) SendChunk(delta StreamDelta, finish ...string) { + var finishReason *string + if len(finish) > 0 && finish[0] != "" { + finishReason = &finish[0] + } + chunk := StreamResponse{ + ID: s.id, Object: "chat.completion.chunk", Created: s.created, Model: s.model, + Choices: []StreamChoice{{Index: 0, Delta: delta, FinishReason: finishReason}}, + } + b, _ := json.Marshal(chunk) + fmt.Fprintf(s.w, "data: %s\n\n", b) + if s.flusher != nil { + s.flusher.Flush() + } +} + +func (s *Streamer) Done() { + fmt.Fprintf(s.w, "data: [DONE]\n\n") + if s.flusher != nil { + s.flusher.Flush() + } +} + +func cleanReasoningArtifacts(s string) string { + s = strings.TrimPrefix(s, "<>") + s = strings.TrimPrefix(s, ">") + s = strings.TrimSuffix(s, "") + s = strings.TrimSuffix(s, "") + return s +} + +func cleanContentArtifacts(s string) string { + if s == "…" || s == "<" || s == ">" || s == "<>" || s == "" { + return "" + } + s = strings.TrimPrefix(s, ">\n\n") + s = strings.TrimPrefix(s, ">\n") + s = strings.TrimPrefix(s, ">") + s = strings.TrimPrefix(s, "<>") + s = strings.TrimSuffix(s, "") + s = strings.TrimSuffix(s, "= 0; i-- { + if m, ok := msgs[i].(map[string]interface{}); ok && m["role"] == "user" { + lastUserIdx = i + break + } + } + + var assistantMsgs []interface{} + if lastUserIdx != -1 && lastUserIdx+1 < len(msgs) { + assistantMsgs = msgs[lastUserIdx+1:] + } else { + assistantMsgs = msgs + } + + var reasoningParts []string + var contentParts []string + var rawAllText strings.Builder + + for _, item := range assistantMsgs { + m, ok := item.(map[string]interface{}) + if !ok || m["role"] != "assistant" { + continue + } + + var textParts strings.Builder + if cArr, ok := m["content"].([]interface{}); ok { + for _, part := range cArr { + if cMap, ok := part.(map[string]interface{}); ok { + if rawText, ok := cMap["text"].(string); ok { + textParts.WriteString(rawText) + } + } + } + } + msgText := textParts.String() + rawAllText.WriteString(msgText) + + if msgText == "…" || msgText == "<" || msgText == ">" || msgText == "<>" || msgText == "" { + continue + } + + isReasoning := false + if meta, ok := m["metadata"].(map[string]interface{}); ok && meta != nil { + if t, ok := meta["title"].(string); ok && strings.EqualFold(t, "Reasoning") { + isReasoning = true + } else if st, ok := meta["status"].(string); ok && (st == "pending" || st == "done") { + isReasoning = true + } + } + + if !isReasoning { + if strings.HasPrefix(msgText, "<>") || strings.HasSuffix(msgText, "") { + isReasoning = true + } else if strings.HasPrefix(msgText, ">") && !strings.HasPrefix(msgText, ">\n") { + isReasoning = true + } + } + + if isReasoning { + cleaned := cleanReasoningArtifacts(msgText) + if cleaned != "" { + reasoningParts = append(reasoningParts, cleaned) + } + } else { + cleaned := cleanContentArtifacts(msgText) + if cleaned != "" { + contentParts = append(contentParts, cleaned) + } + } + } + + reasoning := strings.Join(reasoningParts, "") + content := strings.Join(contentParts, "") + + if reasoning != "" || content != "" { + return content, reasoning, true + } + + combined := rawAllText.String() + if combined == "…" || combined == "<" || combined == ">" || combined == "<>" { + return "", "", true + } + + if strings.Contains(combined, "") { + tIdx := strings.Index(combined, "") + endIdx := strings.Index(combined, "") + if endIdx != -1 { + return strings.TrimLeft(combined[:tIdx]+combined[endIdx+8:], "\n"), combined[tIdx+7 : endIdx], true + } + return strings.TrimSpace(combined[:tIdx]), combined[tIdx+7:], true + } + + if strings.HasPrefix(combined, "<>") { + inner := combined[2:] + if idx := strings.Index(inner, ""); idx != -1 { + return strings.TrimLeft(inner[idx+3:], "\n"), inner[:idx], true + } + if idx := strings.LastIndex(inner, ""), "\n"), inner[:idx], true + } + return "", inner, true + } + + return cleanContentArtifacts(combined), "", true +} + +type StreamToolInterceptor struct { + streamer *Streamer + buffer string + inToolTag bool + matchedOpen string + closingTag string + emittedContent string + toolCallCount int +} + +func NewStreamToolInterceptor(streamer *Streamer) *StreamToolInterceptor { + return &StreamToolInterceptor{streamer: streamer} +} + +func (si *StreamToolInterceptor) HasToolCalls() bool { + return si.toolCallCount > 0 +} + +func (si *StreamToolInterceptor) ProcessContentDelta(delta string) { + if delta == "" { + return + } + si.buffer += delta + + tags := []struct{ open, close string }{ + {"", ""}, + {"", ""}, + {"", ""}, + {"", ""}, + {"", ""}, + {"```tool_call", "```"}, + {"```tool-call", "```"}, + {"```json", "```"}, + } + + for len(si.buffer) > 0 { + if !si.inToolTag { + openIdx := -1 + var foundOpen, foundClose string + for _, t := range tags { + if idx := strings.Index(si.buffer, t.open); idx != -1 { + if openIdx == -1 || idx < openIdx { + openIdx = idx + foundOpen = t.open + foundClose = t.close + } + } + } + + if openIdx == -1 { + maxOverlap := 0 + for _, t := range tags { + for o := len(t.open) - 1; o > 0; o-- { + if strings.HasSuffix(si.buffer, t.open[:o]) && o > maxOverlap { + maxOverlap = o + } + } + } + if maxOverlap > 0 { + toEmit := si.buffer[:len(si.buffer)-maxOverlap] + if toEmit != "" { + si.streamer.SendChunk(StreamDelta{Content: toEmit}) + si.emittedContent += toEmit + si.buffer = si.buffer[len(toEmit):] + } + return + } + si.streamer.SendChunk(StreamDelta{Content: si.buffer}) + si.emittedContent += si.buffer + si.buffer = "" + return + } + + if openIdx > 0 { + pre := si.buffer[:openIdx] + si.streamer.SendChunk(StreamDelta{Content: pre}) + si.emittedContent += pre + } + si.inToolTag = true + si.matchedOpen = foundOpen + si.closingTag = foundClose + si.buffer = si.buffer[openIdx+len(foundOpen):] + } + + if si.inToolTag { + closeIdx := strings.Index(si.buffer, si.closingTag) + if closeIdx == -1 { + return + } + toolJSON := strings.TrimSpace(si.buffer[:closeIdx]) + si.buffer = si.buffer[closeIdx+len(si.closingTag):] + si.inToolTag = false + parsed := parseRawToolPayload(toolJSON) + if len(parsed) > 0 { + for _, tc := range parsed { + idx := si.toolCallCount + si.toolCallCount++ + tc.Index = &idx + si.streamer.SendChunk(StreamDelta{ToolCalls: []ToolCall{tc}}) + } + } else { + rawReconstruct := si.matchedOpen + toolJSON + si.closingTag + si.streamer.SendChunk(StreamDelta{Content: rawReconstruct}) + si.emittedContent += rawReconstruct + } + } + } +} + +func (si *StreamToolInterceptor) FlushRemaining() { + if si.buffer == "" { + return + } + if si.inToolTag { + raw := strings.TrimSpace(strings.TrimSuffix(si.buffer, si.closingTag)) + tcs := parseRawToolPayload(raw) + if len(tcs) > 0 { + for _, tc := range tcs { + idx := si.toolCallCount + si.toolCallCount++ + tc.Index = &idx + si.streamer.SendChunk(StreamDelta{ToolCalls: []ToolCall{tc}}) + } + si.buffer = "" + return + } + } + tcs, cleaned, ok := DetectToolCalls(si.buffer) + if ok { + if cleaned != "" { + si.streamer.SendChunk(StreamDelta{Content: cleaned}) + si.emittedContent += cleaned + } + for _, tc := range tcs { + idx := si.toolCallCount + si.toolCallCount++ + tc.Index = &idx + si.streamer.SendChunk(StreamDelta{ToolCalls: []ToolCall{tc}}) + } + } else { + si.streamer.SendChunk(StreamDelta{Content: si.buffer}) + si.emittedContent += si.buffer + } + si.buffer = "" +} + +// --------------------------------------------------------------------------- +// Kimi K3 Service & Upstream Logic +// --------------------------------------------------------------------------- + +type KimiService struct { + endpoint, defaultModel, defaultBackend, defaultReason, socks5Proxy string + defaultTokens int + defaultTemp float64 + client *http.Client +} + +func NewKimiService(endpoint, model, backend, reasoning, socks5Proxy string, tokens int, temp float64) *KimiService { + if model == "" { + model = "kimi-k3" + } + if backend == "" { + backend = "direct:together" + } + if reasoning == "" { + reasoning = "max" + } + if tokens <= 0 { + tokens = 8192 + } + if temp <= 0 { + temp = 0.7 + } + cleanProxy := strings.TrimSpace(socks5Proxy) + transport := &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + if cleanProxy != "" { + return DialSOCKS5(ctx, cleanProxy, addr) + } + return (&net.Dialer{}).DialContext(ctx, network, addr) + }, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 15 * time.Second, + } + return &KimiService{ + endpoint: strings.TrimRight(endpoint, "/"), + defaultModel: model, + defaultBackend: backend, + defaultReason: reasoning, + socks5Proxy: cleanProxy, + defaultTokens: tokens, + defaultTemp: temp, + client: &http.Client{Transport: transport, Timeout: 300 * time.Second}, + } +} + +func (s *KimiService) ListModels() []ModelItem { + now := time.Now().Unix() + return []ModelItem{ + {ID: s.defaultModel, Object: "model", Created: now, OwnedBy: "moonshotai"}, + {ID: "moonshotai/Kimi-K3", Object: "model", Created: now, OwnedBy: "moonshotai"}, + {ID: "kimi-k3:together", Object: "model", Created: now, OwnedBy: "together"}, + {ID: "kimi-k3:fireworks", Object: "model", Created: now, OwnedBy: "fireworks"}, + {ID: "kimi-k3:hf-together", Object: "model", Created: now, OwnedBy: "huggingface"}, + {ID: "kimi-k3:hf-fireworks", Object: "model", Created: now, OwnedBy: "huggingface"}, + {ID: "kimi-k3:hf-featherless", Object: "model", Created: now, OwnedBy: "huggingface"}, + {ID: "kimi-k3:hf-baseten", Object: "model", Created: now, OwnedBy: "huggingface"}, + } +} + +func (s *KimiService) ResolveBackend(modelName string) string { + if strings.Contains(modelName, ":") { + switch strings.SplitN(modelName, ":", 2)[1] { + case "together", "direct-together", "direct:together": + return "direct:together" + case "fireworks", "direct-fireworks", "direct:fireworks": + return "direct:fireworks" + case "hf-together", "hf:together": + return "hf:together" + case "hf-fireworks", "hf:fireworks", "hf:fireworks-ai": + return "hf:fireworks-ai" + case "hf-featherless", "hf:featherless", "hf:featherless-ai": + return "hf:featherless-ai" + case "hf-baseten", "hf:baseten": + return "hf:baseten" + } + } + return s.defaultBackend +} + +func formatToolCallBlock(tc ToolCall) string { + var argsObj interface{} + argsRaw := strings.TrimSpace(tc.Function.Arguments) + if argsRaw == "" { + argsObj = map[string]interface{}{} + } else if err := json.Unmarshal([]byte(argsRaw), &argsObj); err != nil { + argsObj = map[string]string{"input": argsRaw} + } + b, _ := json.Marshal(map[string]interface{}{ + "name": tc.Function.Name, + "arguments": argsObj, + }) + return fmt.Sprintf("\n%s\n", string(b)) +} + +func formatMessageContent(m ChatMessage) (string, string) { + role := m.Role + cStr := m.GetContentString() + if role == "tool" || role == "function" { + tName := m.Name + if tName == "" { + tName = m.ToolCallID + } + if tName != "" { + cStr = fmt.Sprintf("[Tool Result for %s]: %s", tName, cStr) + } else { + cStr = fmt.Sprintf("[Tool Result]: %s", cStr) + } + role = "user" + } else if role == "assistant" && len(m.ToolCalls) > 0 { + var tcParts []string + for _, tc := range m.ToolCalls { + tcParts = append(tcParts, formatToolCallBlock(tc)) + } + if cStr != "" { + cStr += "\n\n" + strings.Join(tcParts, "\n") + } else { + cStr = strings.Join(tcParts, "\n") + } + } + return role, cStr +} + +func (s *KimiService) Chat(w http.ResponseWriter, r *http.Request, req ChatCompletionRequest) error { + modelName := req.Model + if modelName == "" { + modelName = s.defaultModel + } + + if len(req.Tools) == 0 && len(req.Functions) > 0 { + for _, fn := range req.Functions { + req.Tools = append(req.Tools, Tool{Type: "function", Function: fn}) + } + if req.ToolChoice == nil && req.FunctionCall != nil { + req.ToolChoice = req.FunctionCall + } + } + + var nonSys []ChatMessage + var sysParts []string + for _, m := range req.Messages { + if m.Role == "system" { + if c := m.GetContentString(); c != "" { + sysParts = append(sysParts, c) + } + } else { + nonSys = append(nonSys, m) + } + } + userSysPrompt := strings.Join(sysParts, "\n\n") + + toolsPrompt := "" + if len(req.Tools) > 0 { + toolsPrompt = BuildToolsSystemPrompt(req.Tools, req.ToolChoice) + } + + var history []map[string]interface{} + var promptText string + + if len(nonSys) == 0 { + promptText = "Hello" + if toolsPrompt != "" { + promptText = toolsPrompt + "\n\n" + promptText + } + if userSysPrompt != "" { + promptText = userSysPrompt + "\n\n" + promptText + } + } else { + firstTrailingTool := -1 + for i := len(nonSys) - 1; i >= 0; i-- { + if nonSys[i].Role == "tool" || nonSys[i].Role == "function" { + firstTrailingTool = i + } else { + break + } + } + + var historyMsgs []ChatMessage + var currentTurnMsgs []ChatMessage + + if firstTrailingTool != -1 { + historyMsgs = nonSys[:firstTrailingTool] + currentTurnMsgs = nonSys[firstTrailingTool:] + } else { + historyMsgs = nonSys[:len(nonSys)-1] + currentTurnMsgs = nonSys[len(nonSys)-1:] + } + + for i, m := range historyMsgs { + role, cStr := formatMessageContent(m) + if i == 0 && role == "user" { + if userSysPrompt != "" { + cStr = userSysPrompt + "\n\n" + cStr + } + if toolsPrompt != "" { + cStr = toolsPrompt + "\n\n" + cStr + } + } + history = append(history, map[string]interface{}{ + "role": role, "metadata": nil, + "content": []map[string]interface{}{{"text": cStr, "type": "text"}}, + "options": nil, + }) + } + + if firstTrailingTool != -1 { + var toolResParts []string + for _, m := range currentTurnMsgs { + _, c := formatMessageContent(m) + toolResParts = append(toolResParts, c) + } + promptText = strings.Join(toolResParts, "\n\n") + "\n\nBased on the tool results above, continue your task. If you need to invoke another tool, respond with {\"name\": \"tool_name\", \"arguments\": {...}}. Otherwise, provide your final response." + } else { + _, promptText = formatMessageContent(currentTurnMsgs[0]) + } + + if toolsPrompt != "" { + promptText = toolsPrompt + "\n\n" + promptText + } + if len(history) == 0 && userSysPrompt != "" { + promptText = userSysPrompt + "\n\n" + promptText + } + } + + gradioPayload, _ := json.Marshal(map[string]interface{}{ + "data": []interface{}{ + map[string]interface{}{"text": promptText, "files": []interface{}{}}, + history, + s.ResolveBackend(modelName), + ResolveMaxTokens(req, s.defaultTokens), + ResolveTemperature(req, s.defaultTemp), + ResolveReasoningEffort(req, s.defaultReason), + }, + }) + + effUA := EffectiveUserAgent(r) + resp, err := DoWithFibonacciRetry(s.client, func() (*http.Request, error) { + req, err := http.NewRequest("POST", s.endpoint+"/gradio_api/call/on_submit", bytes.NewBuffer(gradioPayload)) + if err == nil { + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", effUA) + } + return req, err + }, 5) + if err != nil { + return fmt.Errorf("upstream error: %w", err) + } + defer resp.Body.Close() + + var joinRes GradioJoinResponse + if err := json.NewDecoder(resp.Body).Decode(&joinRes); err != nil || joinRes.EventID == "" { + return fmt.Errorf("failed to parse Gradio event ID") + } + + streamResp, err := DoWithFibonacciRetry(s.client, func() (*http.Request, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("%s/gradio_api/call/on_submit/%s", s.endpoint, joinRes.EventID), nil) + if err == nil { + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("User-Agent", effUA) + } + return req, err + }, 5) + if err != nil { + return fmt.Errorf("upstream stream error: %w", err) + } + defer streamResp.Body.Close() + + compID := "chatcmpl-" + GenerateUUID() + created := time.Now().Unix() + + if !req.Stream { + reader := bufio.NewReader(streamResp.Body) + var finalContent, finalReasoning string + for { + line, err := reader.ReadString('\n') + if err != nil { + break + } + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "data: ") { + if c, r, ok := parseGradioSnapshot(strings.TrimPrefix(line, "data: ")); ok { + if c != "" { + finalContent = c + } + if r != "" { + finalReasoning = r + } + } + } + } + + toolCalls, cleanedContent, hasTools := DetectToolCalls(finalContent) + if !hasTools && len(req.Tools) > 0 { + if rCalls, _, rHas := DetectToolCalls(finalReasoning); rHas { + toolCalls = rCalls + hasTools = true + } + } + if !hasTools && len(req.Tools) > 0 { + if directTCs := parseRawToolPayload(finalContent); len(directTCs) > 0 { + toolCalls = directTCs + hasTools = true + cleanedContent = "" + } + } + + finishReason := "stop" + var msgContent interface{} = finalContent + if hasTools { + finishReason = "tool_calls" + if cleanedContent == "" { + msgContent = nil + } else { + msgContent = cleanedContent + } + } + w.Header().Set("Content-Type", "application/json") + return json.NewEncoder(w).Encode(ChatCompletionResponse{ + ID: compID, Object: "chat.completion", Created: created, Model: modelName, + Choices: []ChatCompletionResponseChoice{{ + Index: 0, + Message: ChatMessage{ + Role: "assistant", + Content: msgContent, + ReasoningContent: finalReasoning, + ToolCalls: toolCalls, + }, + FinishReason: finishReason, + }}, + }) + } + + flusher, _ := w.(http.Flusher) + streamer := NewStreamer(w, flusher, compID, created, modelName) + streamer.SendChunk(StreamDelta{Role: "assistant"}) + + interceptor := NewStreamToolInterceptor(streamer) + reader := bufio.NewReader(streamResp.Body) + var emittedContent, emittedReasoning string + + for { + line, err := reader.ReadString('\n') + if err != nil { + break + } + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "data: ") { + if c, r, ok := parseGradioSnapshot(strings.TrimPrefix(line, "data: ")); ok { + if len(r) > len(emittedReasoning) { + streamer.SendChunk(StreamDelta{ReasoningContent: r[len(emittedReasoning):]}) + emittedReasoning = r + } + if len(c) > len(emittedContent) { + interceptor.ProcessContentDelta(c[len(emittedContent):]) + emittedContent = c + } + } + } + } + + interceptor.FlushRemaining() + if !interceptor.HasToolCalls() && len(req.Tools) > 0 { + if rCalls, _, rHas := DetectToolCalls(emittedReasoning); rHas { + for _, tc := range rCalls { + idx := interceptor.toolCallCount + interceptor.toolCallCount++ + tc.Index = &idx + streamer.SendChunk(StreamDelta{ToolCalls: []ToolCall{tc}}) + } + } + } + if !interceptor.HasToolCalls() && len(req.Tools) > 0 { + if directTCs := parseRawToolPayload(emittedContent); len(directTCs) > 0 { + for _, tc := range directTCs { + idx := interceptor.toolCallCount + interceptor.toolCallCount++ + tc.Index = &idx + streamer.SendChunk(StreamDelta{ToolCalls: []ToolCall{tc}}) + } + } + } + + if interceptor.HasToolCalls() { + streamer.SendChunk(StreamDelta{}, "tool_calls") + } else { + streamer.SendChunk(StreamDelta{}, "stop") + } + streamer.Done() + return nil +} + +// --------------------------------------------------------------------------- +// HTTP Handlers & Main +// --------------------------------------------------------------------------- + +func NewMux(service *KimiService) *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + EnableCORS(w) + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "gateway": "k3r053n3", + "status": "online", + "model": service.defaultModel, + "backend": service.defaultBackend, + "reasoning_effort": service.defaultReason, + "socks5_proxy": service.socks5Proxy, + "endpoint": service.endpoint, + "routes": []string{"GET /models", "GET /v1/models", "POST /chat/completions", "POST /v1/chat/completions"}, + }) + }) + modelsH := func(w http.ResponseWriter, r *http.Request) { + EnableCORS(w) + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(ModelsResponse{Object: "list", Data: service.ListModels()}) + } + mux.HandleFunc("/models", modelsH) + mux.HandleFunc("/v1/models", modelsH) + + chatH := func(w http.ResponseWriter, r *http.Request) { + EnableCORS(w) + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + var req ChatCompletionRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON payload: "+err.Error(), http.StatusBadRequest) + return + } + if err := service.Chat(w, r, req); err != nil { + if w.Header().Get("Content-Type") != "text/event-stream" { + http.Error(w, "Upstream error: "+err.Error(), http.StatusBadGateway) + } + } + } + mux.HandleFunc("/chat/completions", chatH) + mux.HandleFunc("/v1/chat/completions", chatH) + return mux +} + +func resolveProxyEnv() string { + for _, k := range []string{"ALL_PROXY", "all_proxy", "SOCKS5_PROXY", "socks5_proxy", "SOCKS_PROXY", "socks_proxy"} { + if v := strings.TrimSpace(os.Getenv(k)); v != "" { + return v + } + } + return "" +} + +func main() { + port := flag.String("port", "8080", "Port to listen on") + endpoint := flag.String("endpoint", "https://cw-105-kimi-k3-gguf-demo.hf.space", "Root URL of the Kimi K3 Gradio space") + model := flag.String("model", "kimi-k3", "Exposed default model name") + backend := flag.String("backend", "direct:together", "Default backend (direct:together, direct:fireworks, hf:fireworks-ai, hf:together, hf:featherless-ai, hf:baseten)") + reasoning := flag.String("reasoning", "max", "Default reasoning effort (max, high, low, default)") + tokens := flag.Int("max-tokens", 8192, "Default max completion tokens (256-8192)") + temp := flag.Float64("temperature", 0.7, "Default temperature (0.0-1.5)") + socks := flag.String("socks", "", "SOCKS5 proxy URL (e.g. socks5://127.0.0.1:1080 or socks5://user:pass@host:port)") + proxy := flag.String("proxy", "", "Alias for -socks") + socks5 := flag.String("socks5", "", "Alias for -socks") + ua := flag.String("user-agent", DefaultUserAgent, "Custom User-Agent header") + uaShort := flag.String("ua", "", "Alias for -user-agent") + flag.Parse() + + ConfiguredUserAgent = *ua + if *uaShort != "" { + ConfiguredUserAgent = *uaShort + } + socksProxy := *socks + if socksProxy == "" { + socksProxy = *proxy + } + if socksProxy == "" { + socksProxy = *socks5 + } + if socksProxy == "" { + socksProxy = resolveProxyEnv() + } + + svc := NewKimiService(*endpoint, *model, *backend, *reasoning, socksProxy, *tokens, *temp) + fmt.Printf("k3r053n3 starting on port %s...\nTarget Endpoint: %s\nModel Name: %s\nBackend: %s\nReasoning Effort: %s\nMax Tokens: %d\nTemperature: %.2f\n", *port, svc.endpoint, svc.defaultModel, svc.defaultBackend, svc.defaultReason, svc.defaultTokens, svc.defaultTemp) + if svc.socks5Proxy != "" { + fmt.Printf("SOCKS5 Proxy: %s\n", svc.socks5Proxy) + } + fmt.Printf("Endpoints:\n GET http://localhost:%s/v1/models\n POST http://localhost:%s/v1/chat/completions\n", *port, *port) + + if err := http.ListenAndServe(":"+*port, NewMux(svc)); err != nil { + fmt.Fprintf(os.Stderr, "server failed: %v\n", err) + os.Exit(1) + } +}