Skip to content
Closed
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
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ record-api:
-output=tests/contract/testdata/openai/models.json
@echo "Done! Golden files saved to tests/contract/testdata/"

record-api-kimi:
@echo "Recording Kimi chat completion..."
go run ./cmd/recordapi -provider=kimi -endpoint=chat \
-output=tests/contract/testdata/kimi/chat_completion.json
@echo "Recording Kimi models..."
go run ./cmd/recordapi -provider=kimi -endpoint=models \
-output=tests/contract/testdata/kimi/models.json
@echo "Done! Golden files saved to tests/contract/testdata/"

swagger:
go run github.com/swaggo/swag/v2/cmd/swag init --generalInfo main.go \
--dir cmd/gomodel,internal \
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
</p>

<p align="center">
A fast and lightweight AI gateway written in Go, providing unified OpenAI-compatible and Anthropic-compatible APIs for OpenAI, Anthropic, Gemini, DeepSeek, xAI, Groq, OpenRouter, Z.ai, Azure OpenAI, Oracle, Ollama, and more.
A fast and lightweight AI gateway written in Go, providing unified OpenAI-compatible and Anthropic-compatible APIs for OpenAI, Anthropic, Gemini, DeepSeek, xAI, Groq, OpenRouter, Z.ai, Kimi, Azure OpenAI, Oracle, Ollama, and more.
</p>

<a href="docs/dashboard.gif">
Expand Down Expand Up @@ -64,9 +64,9 @@ curl http://localhost:8080/v1/chat/completions \
### Supported LLM Providers

GoModel supports OpenAI, Anthropic, Google Gemini, Vertex AI, DeepSeek, Groq,
OpenRouter, Z.ai, xAI (Grok), Alibaba Cloud Model Studio (Bailian), MiniMax,
Xiaomi MiMo, OpenCode Go, Azure OpenAI, Oracle, Ollama, vLLM, Amazon Bedrock,
and all OpenAI-compatible providers.
OpenRouter, Z.ai, xAI (Grok), Alibaba Cloud Model Studio (Bailian), Kimi,
MiniMax, Xiaomi MiMo, OpenCode Go, Azure OpenAI, Oracle, Ollama, vLLM, Amazon
Bedrock, and all OpenAI-compatible providers.

See the [Providers Overview](./docs/providers/overview.mdx) for the full
per-provider feature matrix (chat, `/responses`, embeddings, files, batches,
Expand Down
2 changes: 2 additions & 0 deletions cmd/gomodel/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"gomodel/internal/providers/deepseek"
"gomodel/internal/providers/gemini"
"gomodel/internal/providers/groq"
"gomodel/internal/providers/kimi"
"gomodel/internal/providers/minimax"
"gomodel/internal/providers/ollama"
"gomodel/internal/providers/openai"
Expand Down Expand Up @@ -147,6 +148,7 @@ func main() {
factory.Add(gemini.Registration)
factory.Add(vertex.Registration)
factory.Add(groq.Registration)
factory.Add(kimi.Registration)
factory.Add(minimax.Registration)
factory.Add(ollama.Registration)
factory.Add(opencodego.Registration)
Expand Down
59 changes: 53 additions & 6 deletions cmd/recordapi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package main

import (
"bytes"
"compress/gzip"
"flag"
"fmt"
"io"
Expand All @@ -21,7 +22,10 @@ import (
"github.com/goccy/go-json"
)

const oracleDefaultModel = "openai.gpt-oss-120b"
const (
oracleDefaultModel = "openai.gpt-oss-120b"
kimiDefaultModel = "kimi-for-coding"
)

// Provider configurations
var providerConfigs = map[string]struct {
Expand All @@ -30,6 +34,7 @@ var providerConfigs = map[string]struct {
envKey string
authHeader string
contentType string
setHeaders func(*http.Request)
}{
"openai": {
baseURL: "https://api.openai.com",
Expand Down Expand Up @@ -61,6 +66,13 @@ var providerConfigs = map[string]struct {
authHeader: "Authorization",
contentType: "application/json",
},
"kimi": {
baseURL: "https://api.kimi.com/coding",
envKey: "KIMI_API_KEY",
authHeader: "Authorization",
contentType: "application/json",
setHeaders: setKimiHeaders,
},
"oracle": {
baseURLEnv: "ORACLE_BASE_URL",
envKey: "ORACLE_API_KEY",
Expand Down Expand Up @@ -155,7 +167,7 @@ func providerSupportsResponses(provider string) bool {
}

func main() {
provider := flag.String("provider", "openai", "Provider to test (openai, anthropic, gemini, groq, xai, oracle)")
provider := flag.String("provider", "openai", "Provider to test (openai, anthropic, gemini, groq, xai, kimi, oracle)")
endpoint := flag.String("endpoint", "chat", "Endpoint to test (chat, chat_stream, models, responses, responses_stream)")
output := flag.String("output", "", "Output file path (required)")
model := flag.String("model", "", "Override model in request")
Expand Down Expand Up @@ -203,12 +215,12 @@ func main() {
if eConfig.requestBody != nil {
reqBody := eConfig.requestBody

// Oracle's OpenAI-compatible endpoint expects OCI-hosted model IDs,
// so use a provider-specific default instead of the generic gpt-4o-mini fixture.
if *model != "" {
reqBody["model"] = *model
} else if *provider == "oracle" {
reqBody["model"] = oracleDefaultModel
} else if *provider == "kimi" {
reqBody["model"] = kimiDefaultModel
}

// Adjust request for different providers
Expand Down Expand Up @@ -250,6 +262,11 @@ func main() {
req.Header.Set("anthropic-version", "2023-06-01")
}

// Apply provider-specific header overrides
if pConfig.setHeaders != nil {
pConfig.setHeaders(req)
}

// Send request
client := &http.Client{Timeout: 60 * time.Second}
fmt.Printf("Sending request to %s %s...\n", eConfig.method, url)
Expand All @@ -263,8 +280,18 @@ func main() {

fmt.Printf("Response status: %d %s\n", resp.StatusCode, resp.Status)

// Read response body
body, err := io.ReadAll(resp.Body)
// Decompress if the server honored an explicit Accept-Encoding header.
var respReader io.Reader = resp.Body
if resp.Header.Get("Content-Encoding") == "gzip" {
gr, err := gzip.NewReader(resp.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating gzip reader: %v\n", err)
os.Exit(1)
}
defer gr.Close()
respReader = gr
}
body, err := io.ReadAll(respReader)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading response: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -311,6 +338,26 @@ func main() {
}
}

// setKimiHeaders installs the client-identity header bundle Kimi's coding
// endpoint expects from an OpenAI-SDK-style client.
func setKimiHeaders(req *http.Request) {
req.Header.Set("Accept", "application/json")
req.Header.Set("Accept-Encoding", "gzip, deflate")
req.Header.Set("Accept-Language", "*")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Http-Referer", "https://github.com/Zoo-Code-Org/Zoo-Code")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("User-Agent", "ZooCode/3.62.0")
req.Header.Set("X-Stainless-Arch", "x64")
req.Header.Set("X-Stainless-Lang", "js")
req.Header.Set("X-Stainless-Os", "Linux")
req.Header.Set("X-Stainless-Package-Version", "5.12.2")
req.Header.Set("X-Stainless-Retry-Count", "0")
req.Header.Set("X-Stainless-Runtime", "node")
req.Header.Set("X-Stainless-Runtime-Version", "v22.22.1")
req.Header.Set("X-Title", "Zoo Code")
}

// adjustForAnthropic converts OpenAI-style request to Anthropic format
func adjustForAnthropic(req map[string]any) map[string]any {
result := make(map[string]any)
Expand Down
27 changes: 27 additions & 0 deletions config/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ providers:
openai:
type: openai
api_key: "sk-..."
# Optional per-provider header fields (same for every provider):
# passthrough_user_headers: false # false default (kimi defaults true); true forwards inbound headers
# passthrough_user_headers_skip: {} # optional list of header names to exclude from passthrough
# custom_upstream_headers: {} # static header map, sent on every request; mutually exclusive with passthrough
# Per-provider resilience overrides (optional).
# Only specified fields override the global defaults above.
# resilience:
Expand Down Expand Up @@ -283,6 +287,29 @@ providers:
# Optional: use GLM Coding Plan endpoint instead of the general endpoint.
# base_url: "https://api.z.ai/api/coding/paas/v4"

kimi:
type: kimi
# base_url: "https://api.kimi.com/coding/v1" # default
api_key: "${KIMI_API_KEY}"
# Kimi defaults passthrough_user_headers to true: inbound headers (minus a
# skip list) are forwarded automatically. custom_upstream_headers and
# passthrough_user_headers are mutually exclusive. See docs/features/provider-header-passthrough.mdx.
#
# The Kimi coding endpoint expects a recognizable client identity. With
# passthrough on, a caller sending those headers is forwarded as-is.
# Alternatively, disable passthrough and ship a static identity bundle.
# Use values truthful for your client and compliant with Kimi's terms.
# passthrough_user_headers: false
# passthrough_user_headers_skip:
# - X-MyOrg-Trace
# custom_upstream_headers:
# Http-Referer: "https://github.com/Zoo-Code-Org/Zoo-Code"
# User-Agent: "ZooCode/3.62.0"
# X-Stainless-Lang: "js"
# X-Stainless-Os: "Linux"
# X-Stainless-Runtime: "node"
# X-Title: "Zoo Code"

xiaomi:
type: xiaomi
api_key: "..."
Expand Down
33 changes: 18 additions & 15 deletions config/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ package config
// overrides, credential filtering, or resilience merging. Exported so the
// providers package can resolve it into a fully-configured ProviderConfig.
type RawProviderConfig struct {
Type string `yaml:"type"`
APIKey string `yaml:"api_key"`
BaseURL string `yaml:"base_url"`
APIVersion string `yaml:"api_version"`
Backend string `yaml:"backend"`
AuthType string `yaml:"auth_type"`
APIMode string `yaml:"api_mode"`
VertexProject string `yaml:"vertex_project"`
VertexLocation string `yaml:"vertex_location"`
ServiceAccountFile string `yaml:"service_account_file"`
ServiceAccountJSON string `yaml:"service_account_json"`
ServiceAccountJSONBase64 string `yaml:"service_account_json_base64"`
GCPScope string `yaml:"gcp_scope"`
Models []RawProviderModel `yaml:"models"`
Resilience *RawResilienceConfig `yaml:"resilience"`
Type string `yaml:"type"`
APIKey string `yaml:"api_key"`
BaseURL string `yaml:"base_url"`
APIVersion string `yaml:"api_version"`
Backend string `yaml:"backend"`
AuthType string `yaml:"auth_type"`
APIMode string `yaml:"api_mode"`
VertexProject string `yaml:"vertex_project"`
VertexLocation string `yaml:"vertex_location"`
ServiceAccountFile string `yaml:"service_account_file"`
ServiceAccountJSON string `yaml:"service_account_json"`
ServiceAccountJSONBase64 string `yaml:"service_account_json_base64"`
GCPScope string `yaml:"gcp_scope"`
Models []RawProviderModel `yaml:"models"`
CustomUpstreamHeaders map[string]string `yaml:"custom_upstream_headers"`
PassthroughUserHeaders *bool `yaml:"passthrough_user_headers"`
PassthroughUserHeadersSkip []string `yaml:"passthrough_user_headers_skip"`
Resilience *RawResilienceConfig `yaml:"resilience"`
}
2 changes: 2 additions & 0 deletions docs/advanced/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ Set these to automatically register providers. No YAML configuration required.

Most providers can use a custom base URL via `<PROVIDER>_BASE_URL` (for example `OPENAI_BASE_URL`). DeepSeek defaults to `https://api.deepseek.com`; set `DEEPSEEK_BASE_URL` only for a compatible proxy or alternate DeepSeek endpoint. OpenRouter defaults to `https://openrouter.ai/api/v1` and can be overridden with `OPENROUTER_BASE_URL`. Z.ai defaults to `https://api.z.ai/api/paas/v4`; set `ZAI_BASE_URL=https://api.z.ai/api/coding/paas/v4` for the GLM Coding Plan endpoint. vLLM defaults to `http://localhost:8000/v1` when `VLLM_API_KEY` is set, but keyless deployments should set `VLLM_BASE_URL` explicitly to register the provider. Azure uses `AZURE_BASE_URL` for its deployment base URL and accepts an optional `AZURE_API_VERSION` override; otherwise it defaults to `2024-10-21`. Oracle requires `ORACLE_BASE_URL` because its OpenAI-compatible endpoint is region-specific.

YAML provider blocks also accept `custom_upstream_headers` and `passthrough_user_headers` for tuning outbound headers; see the [Provider Header Passthrough feature page](/features/provider-header-passthrough) for the schema, defaults, env-var pattern, and worked examples.

Every provider type also accepts a comma-separated configured model list via
`<PROVIDER>_MODELS`, for example `OPENROUTER_MODELS`, `ORACLE_MODELS`,
`AZURE_MODELS`, or `VLLM_MODELS`. By default,
Expand Down
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"features/virtual-models",
"features/user-path",
"features/passthrough-api",
"features/provider-header-passthrough",
"features/budgets",
"features/cost-tracking",
"features/cache",
Expand Down
118 changes: 118 additions & 0 deletions docs/features/provider-header-passthrough.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: "Provider Header Passthrough"
description: "Tune outbound request headers per provider — pick one: static custom_upstream_headers OR inbound passthrough. Kimi defaults to passthrough on."
icon: "shuffle"
---

## Overview

Each provider block accepts two optional, **mutually exclusive** header fields.
Setting both fails config-load validation.

| Field | Purpose |
| --- | --- |
| `custom_upstream_headers` | Static map of header name → value, written on every outbound request. YAML-only. |
| `passthrough_user_headers` | Boolean. When `true`, every non-skipped inbound header is forwarded onto the outbound request. |

Pick the path that fits the upstream:

- **Forward what the caller sent** → `passthrough_user_headers: true`. Defaults
to `true` for Kimi, `false` for every other provider. Drop specific keys the
upstream rejects with `passthrough_user_headers_skip`.
- **Write a static bundle** → `passthrough_user_headers: false` +
`custom_upstream_headers`. The outbound request gets only the static headers
+ provider-set auth, never anything the caller sent.

Provider-set authentication headers (e.g. `Authorization: Bearer …`) are
written by the provider factory before any of this runs; the feature never
touches them.

Override precedence, highest wins: env var > YAML field > provider-type
default.

```bash
export KIMI_PASSTHROUGH_USER_HEADERS=false
```

This is unrelated to the [HTTP passthrough API](/features/passthrough-api) at
`/p/{provider}/...`, which proxies provider-native bodies verbatim.

## Static custom headers (passthrough off)

`passthrough_user_headers: false` plus a `custom_upstream_headers` map writes
the same bundle on every request — useful for stable client identity, tracing,
or any upstream-specific static key the inbound call shouldn't influence.

```yaml
providers:
<provider-type>:
api_key: "<your-key>"
passthrough_user_headers: false
custom_upstream_headers:
User-Agent: "my-app/1.0"
X-Trace-Source: "gomodel"
```

Header names are canonicalized with `http.CanonicalHeaderKey`, so
`x-trace-source` and `X-Trace-Source` are the same key. There is no env-var
equivalent — keep static bundles in `config.yaml`.

## Inbound header passthrough (custom headers off)

`passthrough_user_headers: true` forwards every non-skipped inbound header
onto the outbound request. Provider auth headers win over inbound ones;
cookies and `X-Forwarded-*` are never forwarded.

```yaml
providers:
<provider-type>:
api_key: "<your-key>"
passthrough_user_headers: true
```

### Per-provider skip additions

When the upstream rejects a header the always-on floor doesn't already cover,
list it under `passthrough_user_headers_skip` (or the
`<PROVIDER>_PASSTHROUGH_USER_HEADERS_SKIP` env var, comma-separated; env wins
over YAML).

```yaml
providers:
<provider-type>:
api_key: "<your-key>"
passthrough_user_headers: true
passthrough_user_headers_skip:
- X-MyOrg-Trace
- X-Internal-Id
```

```bash
export KIMI_PASSTHROUGH_USER_HEADERS_SKIP="X-MyOrg-Trace, X-Internal-Id"
```

Keys are matched with `http.CanonicalHeaderKey`, so `x-myorg-trace` and
`X-MyOrg-Trace` are the same.

## Skip list

The always-on floor drops the following from inbound passthrough on every
provider, and is not per-provider configurable — only additions via
`passthrough_user_headers_skip` are:

- **Credential headers**: `Authorization`, `X-Api-Key`
- **Transport-managed / hop-by-hop (RFC 7230)**: `Host`, `Content-Length`,
`Connection`, `Keep-Alive`, `Proxy-Authenticate`, `Proxy-Authorization`,
`Te`, `Trailer`, `Transfer-Encoding`, `Upgrade`
- **Cookies and forwarding context**: `Cookie`, `Set-Cookie`, `Forwarded`,
every `X-Forwarded-*` prefix

The skip list has no effect when `passthrough_user_headers: false`.

## See also

- [Kimi provider guide](/providers/kimi) — Kimi's passthrough default and the
client-identity headers its coding endpoint expects.
- [Configuration reference](/advanced/configuration) — full `config.yaml`
schema.
- [Providers overview](/providers/overview) — comparison matrix.
Loading