Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,19 @@ GONKA_ADDRESS=gonka1your_address_here
GONKA_SOURCE_URL=http://node1.gonka.ai:8000

# Features

# Rewrites tool/function-call requests into plain prompts and converts the
# model's JSON response back to proper tool_calls format.
# Use this when the upstream model does not support native tool calling.
SIMULATE_TOOL_CALLS=true

# Forward tool_calls natively to the upstream node (Gonka nodes that support
# native tool calling). When enabled, SIMULATE_TOOL_CALLS is bypassed and
# array-format message content is automatically flattened to plain strings
# so the upstream node can unmarshal it correctly.
# Disabled by default; set to true only when the node supports native tools.
# NATIVE_TOOL_CALLS=false

# Privacy sanitization
#
# Strips sensitive data from messages before forwarding to the upstream LLM
Expand All @@ -31,22 +40,33 @@ SIMULATE_TOOL_CALLS=true
# docker compose --profile sanitize up -d

# Layer 1: enable sanitization at all (required for layers 2 and 3)
SANITIZE=true
SANITIZE=false

# Layer 2: NER sidecar - catches person names, organisations, locations
# Requires the sanitize-ner container from the sanitize Docker profile.
SANITIZE_NER=true
SANITIZE_NER=false
SANITIZE_NER_URL=http://sanitize-ner:8001

# Layer 3: local LLM classifier - catches API keys, passwords, credentials,
# and anything else contextually sensitive that NER would miss.
# Requires the ollama container from the sanitize Docker profile.
# Pull the model on first run:
# docker compose --profile sanitize run --rm ollama-init
SANITIZE_LLM=true
SANITIZE_LLM=false
SANITIZE_LLM_URL=http://ollama:11434
SANITIZE_LLM_MODEL=qwen3:4b-instruct-2507-q4_K_M
SANITIZE_LLM_THRESHOLD=0

# Retry behaviour for flaky upstream nodes (429 / 5xx responses).
#
# Strategy: how to pick the node for each retry attempt.
# same_node — keep hitting the same node until it responds
# other_nodes — rotate to a different node on each retry (default)
GONKA_RETRY_STRATEGY=same_node
#
# Maximum number of retries before giving up. 0 = unlimited (keep retrying
# until the request succeeds or the client disconnects).
GONKA_MAX_RETRIES=8

# Server
PORT=8080
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.env
/proxy
minimal-py/
*.exe
tmp/
.idea/
Expand Down
50 changes: 38 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A lightweight, Docker-based proxy that exposes the [Gonka AI](https://gonka.ai) decentralised inference network as a standard **OpenAI-compatible API**. Point any app that speaks the OpenAI protocol at this proxy and it just works - no SDK changes required.

(with Gonka network's Transfer Agent feature (v0.2.9+) applied!)
(with Gonka network's Transfer Agent feature (v0.2.9+) and v0.2.12 DevShards update applied!)

## Features

Expand All @@ -19,8 +19,8 @@ A lightweight, Docker-based proxy that exposes the [Gonka AI](https://gonka.ai)

```bash
# 1. Clone
git clone https://github.com/gonkalabs/gonka-proxy-go.git
cd gonka-proxy-go
git clone https://github.com/gonkalabs/opengnk.git
cd opengnk

# 2. Configure
cp .env.example .env
Expand Down Expand Up @@ -120,8 +120,9 @@ All configuration is via environment variables (loaded from `.env`):
| `GONKA_WALLETS` | No* | - | Comma-separated `privkey:address` pairs for multiple wallets (see below) |
| `GONKA_PRIVATE_KEY` | No* | - | Hex-encoded secp256k1 private key (single wallet) |
| `GONKA_ADDRESS` | No | Derived from key | Your bech32 account address (single wallet) |
| `GONKA_SOURCE_URL` | No | `http://node2.gonka.ai:8000` | Genesis node for endpoint discovery |
| `SIMULATE_TOOL_CALLS` | No | `false` | Enable tool/function-call simulation |
| `GONKA_SOURCE_URL` | No | `https://node4.gonka.ai` | Node4 gateway for endpoint discovery |
| `SIMULATE_TOOL_CALLS` | No | `false` | Enable tool/function-call simulation (for nodes without native support) |
| `NATIVE_TOOL_CALLS` | No | `false` | Forward tool calls natively; disables simulation and normalizes array content |
| `PORT` | No | `8080` | HTTP server port |

\* Either `GONKA_WALLETS` or `GONKA_PRIVATE_KEY` must be set. If both are set, `GONKA_WALLETS` takes priority.
Expand Down Expand Up @@ -171,6 +172,16 @@ gonka1gndhek2h2y5849wf6tmw6gnw9qn4vysgljed0u

Requests sent to non-whitelisted nodes will be rejected with `Transfer Agent not allowed`. The proxy handles this automatically - you don't need to pick nodes manually. If the whitelist changes in a future Gonka update, edit the `allowedTransferAgents` map in `internal/upstream/client.go`.

### DevShards node4 gateway

After the DevShards update, local OpenGNK uses node4 as the public gateway:

```env
GONKA_SOURCE_URL=https://node4.gonka.ai
```

Do not use `http://node4.gonka.ai:8000` for local discovery; that endpoint can hang. The proxy discovers whitelisted transfer-agent addresses from node4, then routes inference traffic through `https://node4.gonka.ai/v1` while still signing each request for the selected transfer-agent address.

## Using as an OpenAI drop-in

The proxy exposes the same API as OpenAI. Any library or application that supports a custom `base_url` will work.
Expand Down Expand Up @@ -299,19 +310,34 @@ SANITIZE_LLM=false

## Tool / function calling

Not all Gonka inference nodes currently support the OpenAI tool-calling protocol natively (`--enable-auto-tool-choice` is not set for them during node deployment!).
The proxy supports two modes depending on whether your target Gonka node has native tool-calling enabled (`--enable-auto-tool-choice`).

### Mode 1 — Native tool calls (recommended when supported)

Gonka now supports native tool calling on nodes deployed with `--enable-auto-tool-choice`. When the upstream node handles tools natively, enable this mode:

The proxy can simulate it.
```env
NATIVE_TOOL_CALLS=true
SIMULATE_TOOL_CALLS=false # simulation is bypassed when native mode is on
```

The proxy forwards your `tools` and `tool_calls` fields unchanged and automatically flattens any OpenAI-style array content (`[{"type":"text","text":"..."}]`) to plain strings, which Gonka nodes require. All message types are normalized — including `role: "tool"` messages and messages with `tool_calls` — so the upstream never receives a mixed content format.

### Mode 2 — Simulated tool calls (fallback for nodes without native support)

### Enable simulation
Not all nodes are deployed with native tool-calling enabled. The proxy can simulate it instead:

Set `SIMULATE_TOOL_CALLS=true` in your `.env` and restart:
```env
SIMULATE_TOOL_CALLS=true
```

Restart after changing either flag:

```bash
make stop && make run
```

### How it works
### How simulation works

1. Your app sends a standard OpenAI request with `tools` and `tool_choice`
2. The proxy strips those fields (which upstream would reject) and injects a system prompt that describes the available tools and asks the model to respond with structured JSON
Expand Down Expand Up @@ -341,13 +367,13 @@ response = client.chat.completions.create(
}],
)

# Works exactly like OpenAI:
# Works exactly like OpenAI (both modes):
tool_call = response.choices[0].message.tool_calls[0]
print(tool_call.function.name) # "get_weather"
print(tool_call.function.arguments) # '{"location": "Berlin"}'
```

The full round-trip (ask -> tool call -> tool result -> final answer) works exactly as it does with OpenAI.
The full round-trip (ask tool call tool result final answer) works exactly as it does with OpenAI in both modes.

## Endpoints

Expand Down
12 changes: 10 additions & 2 deletions cmd/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
Expand Down Expand Up @@ -48,7 +49,7 @@ func main() {
os.Exit(1)
}

client := upstream.New(cfg.SourceURL, pool)
client := upstream.New(cfg.SourceURL, pool, cfg.RetryStrategy, cfg.MaxRetries)

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
if err := client.DiscoverEndpoints(ctx); err != nil {
Expand Down Expand Up @@ -82,7 +83,7 @@ func main() {
slog.Info("sanitization enabled", "classifiers", len(classifiers))
}

handler := api.New(client, cfg.SimulateToolCalls, san)
handler := api.New(client, cfg.SimulateToolCalls, cfg.NativeToolCalls, san)

qm := quality.New()

Expand Down Expand Up @@ -113,11 +114,18 @@ func main() {
}
}()

maxRetriesLabel := "unlimited"
if cfg.MaxRetries > 0 {
maxRetriesLabel = fmt.Sprintf("%d", cfg.MaxRetries)
}
slog.Info("starting proxy server",
"addr", cfg.ListenAddr,
"wallets", pool.Len(),
"toolSim", cfg.SimulateToolCalls,
"nativeToolCalls", cfg.NativeToolCalls,
"sanitize", cfg.SanitizeEnabled,
"retryStrategy", cfg.RetryStrategy,
"maxRetries", maxRetriesLabel,
)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("server error", "err", err)
Expand Down
Loading