Skip to content

feat(headers, provider): custom header overrides and Moonshot Kimi provider#487

Closed
weselben wants to merge 5 commits into
ENTERPILOT:mainfrom
weselben:feat/kimi-provider-with-header-overwrites
Closed

feat(headers, provider): custom header overrides and Moonshot Kimi provider#487
weselben wants to merge 5 commits into
ENTERPILOT:mainfrom
weselben:feat/kimi-provider-with-header-overwrites

Conversation

@weselben

@weselben weselben commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

This PR adds per-provider HTTP header overrides and a new Moonshot Kimi provider.

Custom upstream / passthrough headers

A new HeaderOverridesConfig engine lets operators control how headers flow to upstream providers.

  • custom_upstream_headers: static map[string]string headers attached to every request for a provider.
  • passthrough_user_headers: forwards selected client headers upstream.
  • passthrough_user_headers_skip + passthrough_user_headers_skip_mode: configure a skip or allow list (default: skip).
  • A hard-coded security floor always strips credential headers (Authorization, Cookie, X-Api-Key, etc.), the internal X-GoModel-User-Path header, and any configured userPathAlias.

Example:

providers:
  kimi:
    api_key: ${KIMI_API_KEY}
    custom_upstream_headers:
      X-Title: "MyApp"
    passthrough_user_headers: true
    passthrough_user_headers_skip:
      - X-Trace-Id
    passthrough_user_headers_skip_mode: skip

The feature is wired into the factory and OpenAI-compatible adapter, and is applied by DeepSeek, Groq, Ollama, vLLM, xAI, and the new Kimi provider. The server middleware that captures passthrough headers is gated so the default hot path is unchanged when no provider opts in.

Moonshot Kimi provider

  • New internal/providers/kimi package wrapping the OpenAI-compatible client.
  • Default base URL: https://api.kimi.com/coding/v1.
  • Registered in the factory, config examples, Helm values, and provider overview docs.
  • Supports custom_upstream_headers and passthrough_user_headers via the shared override engine.
  • Unit tests, contract tests, golden files, and recordapi tooling for generating Kimi test fixtures.

Known Limitations

  • Header overrides are not yet implemented for OpenRouter or native SDK providers (Anthropic, Gemini, Vertex, Bedrock, etc.).
  • The passthrough path is only wired for OpenAI-compatible providers in this PR.

Commits

  • dbd8841 docs: add custom headers feature guide and Kimi provider docs
  • 50d0cc1 feat(headers): add custom upstream and passthrough header overrides
  • 3efecb3 tests(headers): add unit and provider tests for header overrides
  • c3d5214 feat(provider): add Moonshot Kimi provider
  • 0503eb7 tests(provider): add Kimi contract tests, golden fixtures and recordapi support

Future scope

The following native / non-OpenAI-compatible providers are not covered by the header override feature in this PR, but could be wired in later if the same configuration surface is desired:

  • Anthropic
  • Google Gemini / Vertex AI
  • Amazon Bedrock
  • Azure OpenAI
  • MiniMax
  • Xiaomi MiMo
  • OpenCode Go
  • Z.ai

Each provider uses its own SDK or native request path, so adding support requires a per-provider audit of how headers are constructed before forwarding. OpenRouter was intentionally left out of this PR because it already sets its own attribution headers (HTTP-Referer, X-OpenRouter-Title, etc.) and changing that behavior was out of scope. OpenAI-compatible providers (including Kimi, OpenAI, DeepSeek, Groq, Ollama, vLLM, xAI, Fireworks AI, and Oracle) already inherit the feature through the shared adapter.

Summary by CodeRabbit

  • New Features

    • Added support for the Kimi provider, including chat, streaming, models, and embeddings requests.
    • Added configurable upstream header handling, including custom headers and optional passthrough of selected user headers.
    • Expanded supported provider configuration to include additional Kimi settings and example setups.
  • Bug Fixes

    • Improved header filtering so sensitive or internal headers are not forwarded upstream.
    • Enabled passthrough header capture only when a provider requires it.

weselben added 5 commits July 6, 2026 00:04
Document per-provider custom upstream headers and passthrough user headers under docs/features/custom-headers.mdx. Add the Moonshot Kimi provider page and overview entry. Update CLAUDE.md and config/config.example.yaml references.
Introduce HeaderOverridesConfig, ApplyHeaderOverrides, and per-provider factory wiring. Support static custom headers and optional client-header passthrough with skip/allow lists and a security floor. Gate middleware registration on any provider enabling passthrough_user_headers.
Cover config parsing, factory wiring, core header override logic, server middleware capture, and provider-specific application across OpenAI-compatible providers.
Register the Kimi provider, add an OpenAI-compatible adapter, and enable configuration in examples, Helm values, and the default passthrough provider list.
…pi support

Add Kimi unit and contract tests, recorded API fixtures, golden files, and recordapi tooling for generating Kimi testdata.
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces configurable per-provider HTTP header overrides (static custom upstream headers and curated client header passthrough with skip/allow modes), wires this through provider config, factory, and individual providers (DeepSeek, Ollama, xAI, vLLM, OpenAI-compatible), adds server-side capture middleware, and integrates a new Kimi provider with docs, config examples, and contract tests.

Changes

Custom Upstream Headers & Kimi Provider Integration

Layer / File(s) Summary
Header override core primitives
internal/providers/headers.go, internal/providers/headers_test.go
Introduces HeaderOverridesConfig, context-based passthrough header storage, credential/internal header blocking, and skip/allow forwarding logic with extensive tests.
Provider config schema and env parsing
config/providers.go, config/providers_test.go, internal/providers/config.go, internal/providers/config_test.go, config/config.example.yaml, config/config_example_test.go, config/config_test.go, config/server.go
Extends RawProviderConfig/ProviderConfig with header/passthrough fields, adds env var parsing (PASSTHROUGH_USER_HEADERS), and updates example config and docs comments.
Provider factory wiring
internal/providers/factory.go, internal/providers/factory_test.go, internal/providers/init.go
Adds SetUserPathHeader, buildHeaderOverrides, ProviderOptions fields, and InitResult.ResolvedProviders.
OpenAI-compatible adapter wiring
internal/providers/openai/compatible_provider.go, ...compatible_provider_test.go, internal/providers/groq/groq_test.go
Propagates HeaderOverrides/UserPathAlias through the shared compatible client and adds Groq outbound header verification.
Per-provider header support
internal/providers/deepseek/*, internal/providers/ollama/*, internal/providers/xai/*, internal/providers/vllm/*, tests/contract/xai_test.go
Adds header override/alias fields and applies overrides in request construction across DeepSeek, Ollama, xAI, and vLLM (including vLLM's root passthrough client), with matching test updates.
Server passthrough middleware & app wiring
internal/server/http.go, internal/server/passthrough_headers.go, internal/server/*_test.go, internal/app/app.go, internal/app/app_test.go
Adds PassthroughHeaderCapture middleware gated by PassthroughUserHeadersEnabled, computed via anyProviderHasPassthroughUserHeaders.
Kimi provider implementation
internal/providers/kimi/*, run/providers.go, run/providers_test.go, run/lifecycle_test.go
Adds the Kimi provider (OpenAI-compatible) and registers it in the default factory.
Kimi contract tests & recordapi support
tests/contract/kimi_test.go, tests/contract/testdata/..., cmd/recordapi/*
Adds Kimi replay-based contract tests, golden fixtures, and recordapi CLI support for Kimi and the new embeddings endpoint.
Documentation & deployment config
CLAUDE.md, docs/*, helm/values.yaml
Documents custom headers feature and Kimi provider across docs, navigation, and Helm values.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PassthroughHeaderCapture
  participant Authenticator
  participant ProviderFactory
  participant Provider

  Client->>PassthroughHeaderCapture: HTTP request with headers
  PassthroughHeaderCapture->>PassthroughHeaderCapture: FilterIncomingHeaders(userPathAlias)
  PassthroughHeaderCapture->>Authenticator: request with filtered headers in context
  Authenticator->>ProviderFactory: authenticated request
  ProviderFactory->>Provider: Create(cfg, opts with HeaderOverrides)
  Provider->>Provider: ApplyHeaderOverrides(request, cfg, alias)
  Provider-->>Client: upstream response
Loading

Suggested reviewers: SantiagoDePolonia

Poem

A rabbit hopped through headers new,
Skipped the creds, let safe ones through,
Kimi joined the burrow band,
With config flags on every hand,
Passthrough hops from door to door — 🐇
Ship it now, and hop for more!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: header override support and the new Moonshot Kimi provider.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

This PR needs a small compile fix before merging.

A missing brace in a changed test file prevents the app package tests from compiling. The blocking issue is contained to one file.

internal/app/app_test.go

T-Rex T-Rex Logs

What T-Rex did

  • Reproduced the syntax parse failure during a focused Go test run after restoring the missing brace.
  • The compiler reported the error location internal/app/app_test.go:649:6 with expected '(', found TestAnyProviderHasPassthroughUserHeaders_True, confirming the unterminated staticRewriter.Rewrite syntax.
  • In a separate run, the proof log shows the test completed successfully in the foreground without Docker, live credentials, or external network requirements, with EXIT_CODE: 0.

View all artifacts

T-Rex Ran code and verified through T-Rex

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Client
participant Server as Echo middleware
participant Router as GoModel router
participant Provider as OpenAI-compatible provider
participant Upstream as Upstream API

Client->>Server: HTTP request with headers
Server->>Server: RequestSnapshotCapture
alt any provider enables passthrough_user_headers
    Server->>Server: PassthroughHeaderCapture filters credential/user-path headers
    Server->>Router: request context includes passthrough headers
else no passthrough provider configured
    Server->>Router: request context unchanged
end
Router->>Provider: resolved provider request
Provider->>Provider: Set auth/provider defaults
alt passthrough_user_headers
    Provider->>Provider: ApplyHeaderOverrides from captured headers
else custom_upstream_headers
    Provider->>Provider: ApplyHeaderOverrides from static headers
end
Provider->>Upstream: outbound provider request
Upstream-->>Client: normalized response
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Client
participant Server as Echo middleware
participant Router as GoModel router
participant Provider as OpenAI-compatible provider
participant Upstream as Upstream API

Client->>Server: HTTP request with headers
Server->>Server: RequestSnapshotCapture
alt any provider enables passthrough_user_headers
    Server->>Server: PassthroughHeaderCapture filters credential/user-path headers
    Server->>Router: request context includes passthrough headers
else no passthrough provider configured
    Server->>Router: request context unchanged
end
Router->>Provider: resolved provider request
Provider->>Provider: Set auth/provider defaults
alt passthrough_user_headers
    Provider->>Provider: ApplyHeaderOverrides from captured headers
else custom_upstream_headers
    Provider->>Provider: ApplyHeaderOverrides from static headers
end
Provider->>Upstream: outbound provider request
Upstream-->>Client: normalized response
Loading

Reviews (1): Last reviewed commit: "tests(provider): add Kimi contract tests..." | Re-trigger Greptile

Comment thread internal/app/app_test.go
Comment on lines 647 to +649
func (r *staticRewriter) Rewrite(context.Context, ext.Input) (*ext.Result, error) {
return nil, nil
func TestAnyProviderHasPassthroughUserHeaders_True(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Restore missing brace
staticRewriter.Rewrite is never closed before the new test declaration, so this file will not parse and the package cannot compile. The changed code starts func TestAnyProviderHasPassthroughUserHeaders_True immediately after return nil, nil inside the previous function body.

Artifacts

Repro: focused Go test output showing app_test.go syntax parse failure

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
internal/providers/openai/compatible_provider.go (2)

56-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the duplicated header-override application logic.

Both NewCompatibleProvider and NewCompatibleProviderWithHTTPClient build an identical request-customization closure (cfg.SetHeaders(...) then providers.ApplyHeaderOverrides(...)). A shared helper would prevent divergence if this logic changes again.

♻️ Proposed helper extraction
+func applyRequestHeaders(req *http.Request, cfg CompatibleProviderConfig, apiKey string) {
+	if cfg.SetHeaders != nil {
+		cfg.SetHeaders(req, apiKey)
+	}
+	if cfg.HeaderOverrides != nil {
+		providers.ApplyHeaderOverrides(req, *cfg.HeaderOverrides, cfg.UserPathAlias)
+	}
+}

Then both constructors' closures become func(req *http.Request) { applyRequestHeaders(req, cfg, apiKey) }.

Also applies to: 78-85

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/openai/compatible_provider.go` around lines 56 - 63, The
request header customization logic is duplicated in both compatible provider
constructors, so extract it into a shared helper to keep behavior consistent.
Add a helper used by NewCompatibleProvider and
NewCompatibleProviderWithHTTPClient that applies cfg.SetHeaders and
providers.ApplyHeaderOverrides in the same order, then have each llmclient.New
closure delegate to that helper with req, cfg, and apiKey.

67-87: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Propagate headerOverrides into the compatible client
NewWithHTTPClient stores these on Provider, but openai.NewCompatibleProviderWithHTTPClient(...) never receives them, so vLLM /v1/chat/completions-style requests skip custom headers when this constructor is used. Pass headerOverrides and userPathAlias into the CompatibleProviderConfig here as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/openai/compatible_provider.go` around lines 67 - 87,
`NewCompatibleProviderWithHTTPClient` is not forwarding the compatible client’s
custom header settings, so requests built through `CompatibleProvider` can miss
overrides. Update the `CompatibleProviderConfig` flow in
`NewCompatibleProviderWithHTTPClient` so it carries both `HeaderOverrides` and
`UserPathAlias`, and ensure the existing request setup in
`llmclient.NewWithHTTPClient` continues to apply them via
`providers.ApplyHeaderOverrides` alongside `SetHeaders`.
internal/app/app.go (1)

173-190: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Move SetUserPathHeader before providers.Init. providers.Init builds the long-lived providers through factory.Create, and Create snapshots UserPathHeader during construction. Leaving this after init leaves custom Server.UserPathHeader deployments with an empty alias, so the internal identity header can bypass the blocked-header floor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/app/app.go` around lines 173 - 190, Move the
cfg.Factory.SetUserPathHeader call to before providers.Init in app
initialization so factory.Create snapshots the configured Server.UserPathHeader
during provider construction; update the init flow in app startup around
providers.Init and anyProviderHasPassthroughUserHeaders so custom user-path
aliases are applied before long-lived providers are built.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/app/app_test.go`:
- Around line 647-677: The staticRewriter.Rewrite method is missing its closing
brace before the new test functions, which breaks compilation. Close the Rewrite
function immediately after the return nil, nil statement, then keep the new
anyProviderHasPassthroughUserHeaders tests below it so the existing
staticRewriter and test declarations remain properly separated.

In `@internal/providers/config.go`:
- Around line 424-426: The `overlayProviderEnvVars` handling for
`PassthroughUserHeaders` is currently enable-only, so an env var cannot force
the field back to false once YAML has enabled it. Update the config merge logic
in `overlayProviderEnvVars` (and any related boolean parsing in `parseBool`) so
the env var can explicitly override the YAML value in both directions if that is
the intended behavior, or otherwise document and enforce the one-way semantics
consistently for `PassthroughUserHeaders`.
- Around line 729-737: Validate PassthroughUserHeadersSkipMode before it is
persisted instead of only normalizing empty input in defaultPassthroughSkipMode.
Update the config path in internal/providers/config.go where this value is
assigned so unknown or misspelled modes are rejected or at least warned about,
and ensure shouldForward only receives approved values like skip or only rather
than silently accepting arbitrary strings. Keep the existing empty-value
fallback behavior, but add explicit validation at the
PassthroughUserHeadersSkipMode storage/parse point.

In `@internal/providers/headers.go`:
- Around line 88-97: The ignored custom headers message in the passthrough
branch of applyPassthroughHeaders should be elevated from debug to warn so
operators can see misconfigurations in production. Update the slog call in
internal/providers/headers.go to use a warning level while keeping the same
context message, and adjust the corresponding expectation in headers_test.go,
including TestApplyHeaderOverrides_PassthroughMode_LogsIgnoredCustomHeaders, to
assert the new warning level instead of slog.LevelDebug.

In `@run/lifecycle_test.go`:
- Around line 149-176: The current TestMain_SetUserPathHeaderWiring only checks
that providers.NewProviderFactory().SetUserPathHeader(...) does not panic, which
is a weak assertion because SetUserPathHeader appears to just store the value.
Update the test to verify observable behavior: after calling SetUserPathHeader
on the ProviderFactory, assert the header is actually retained or used through a
getter or downstream effect tied to that factory, or remove the test if no
meaningful assertion is available yet.

In `@tests/contract/kimi_test.go`:
- Around line 27-56: The TestKimiReplayChatCompletion subtests for "params" and
"tools" currently duplicate the "basic" request and only swap fixture names, so
they do not exercise request translation or tool-call handling. Update the test
cases in TestKimiReplayChatCompletion to send distinct core.ChatRequest inputs
for the "params" and "tools" cases by setting provider-specific fields such as
Temperature, TopP, and Tools, and use fixtures that actually reflect those
behaviors (for example, a response with tool_calls) so the replay tests validate
the Kimi provider’s parameter mapping and response parsing.

---

Outside diff comments:
In `@internal/app/app.go`:
- Around line 173-190: Move the cfg.Factory.SetUserPathHeader call to before
providers.Init in app initialization so factory.Create snapshots the configured
Server.UserPathHeader during provider construction; update the init flow in app
startup around providers.Init and anyProviderHasPassthroughUserHeaders so custom
user-path aliases are applied before long-lived providers are built.

In `@internal/providers/openai/compatible_provider.go`:
- Around line 56-63: The request header customization logic is duplicated in
both compatible provider constructors, so extract it into a shared helper to
keep behavior consistent. Add a helper used by NewCompatibleProvider and
NewCompatibleProviderWithHTTPClient that applies cfg.SetHeaders and
providers.ApplyHeaderOverrides in the same order, then have each llmclient.New
closure delegate to that helper with req, cfg, and apiKey.
- Around line 67-87: `NewCompatibleProviderWithHTTPClient` is not forwarding the
compatible client’s custom header settings, so requests built through
`CompatibleProvider` can miss overrides. Update the `CompatibleProviderConfig`
flow in `NewCompatibleProviderWithHTTPClient` so it carries both
`HeaderOverrides` and `UserPathAlias`, and ensure the existing request setup in
`llmclient.NewWithHTTPClient` continues to apply them via
`providers.ApplyHeaderOverrides` alongside `SetHeaders`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b2be12b8-ddba-416c-90c0-875d52322990

📥 Commits

Reviewing files that changed from the base of the PR and between daa836c and 0503eb7.

📒 Files selected for processing (57)
  • CLAUDE.md
  • cmd/recordapi/main.go
  • cmd/recordapi/main_test.go
  • config/config.example.yaml
  • config/config_example_test.go
  • config/config_test.go
  • config/providers.go
  • config/providers_test.go
  • config/server.go
  • docs/docs.json
  • docs/features/custom-headers.mdx
  • docs/providers/kimi.mdx
  • docs/providers/overview.mdx
  • helm/values.yaml
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/providers/config.go
  • internal/providers/config_test.go
  • internal/providers/deepseek/deepseek.go
  • internal/providers/deepseek/deepseek_test.go
  • internal/providers/factory.go
  • internal/providers/factory_test.go
  • internal/providers/groq/groq_test.go
  • internal/providers/headers.go
  • internal/providers/headers_test.go
  • internal/providers/init.go
  • internal/providers/kimi/kimi.go
  • internal/providers/kimi/kimi_test.go
  • internal/providers/ollama/ollama.go
  • internal/providers/ollama/ollama_test.go
  • internal/providers/openai/compatible_provider.go
  • internal/providers/openai/compatible_provider_test.go
  • internal/providers/vllm/vllm.go
  • internal/providers/vllm/vllm_test.go
  • internal/providers/xai/xai.go
  • internal/providers/xai/xai_test.go
  • internal/server/http.go
  • internal/server/http_test.go
  • internal/server/passthrough_headers.go
  • internal/server/passthrough_headers_test.go
  • run/lifecycle_test.go
  • run/providers.go
  • run/providers_test.go
  • tests/contract/kimi_test.go
  • tests/contract/testdata/golden/kimi/chat_completion.golden.json
  • tests/contract/testdata/golden/kimi/chat_completion_stream.golden.json
  • tests/contract/testdata/golden/kimi/chat_with_params.golden.json
  • tests/contract/testdata/golden/kimi/chat_with_tools.golden.json
  • tests/contract/testdata/golden/kimi/embeddings.golden.json
  • tests/contract/testdata/golden/kimi/models.golden.json
  • tests/contract/testdata/kimi/chat_completion.json
  • tests/contract/testdata/kimi/chat_completion_stream.txt
  • tests/contract/testdata/kimi/chat_with_params.json
  • tests/contract/testdata/kimi/chat_with_tools.json
  • tests/contract/testdata/kimi/embeddings.json
  • tests/contract/testdata/kimi/models.json
  • tests/contract/xai_test.go

Comment thread internal/app/app_test.go
Comment on lines 647 to 677
func (r *staticRewriter) Rewrite(context.Context, ext.Input) (*ext.Result, error) {
return nil, nil
func TestAnyProviderHasPassthroughUserHeaders_True(t *testing.T) {
resolved := map[string]providers.ProviderConfig{
"openai": {Type: "openai", PassthroughUserHeaders: false},
"anthropic": {Type: "anthropic", PassthroughUserHeaders: true},
"groq": {Type: "groq", PassthroughUserHeaders: false},
}
if !anyProviderHasPassthroughUserHeaders(resolved) {
t.Fatal("expected true when at least one provider has passthrough_user_headers=true")
}
}

func TestAnyProviderHasPassthroughUserHeaders_False(t *testing.T) {
resolved := map[string]providers.ProviderConfig{
"openai": {Type: "openai", PassthroughUserHeaders: false},
"anthropic": {Type: "anthropic", PassthroughUserHeaders: false},
}
if anyProviderHasPassthroughUserHeaders(resolved) {
t.Fatal("expected false when no provider has passthrough_user_headers=true")
}
}

func TestAnyProviderHasPassthroughUserHeaders_EmptyMap(t *testing.T) {
if anyProviderHasPassthroughUserHeaders(nil) {
t.Fatal("expected false for nil resolved provider map")
}
if anyProviderHasPassthroughUserHeaders(map[string]providers.ProviderConfig{}) {
t.Fatal("expected false for empty resolved provider map")
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -B2 -A2 'func \(r \*staticRewriter\) Rewrite' internal/app/app_test.go
rg -n -B2 -A2 'func TestAnyProviderHasPassthroughUserHeaders_True' internal/app/app_test.go

Repository: ENTERPILOT/GoModel

Length of output: 704


🏁 Script executed:

#!/bin/bash
sed -n '640,660p' internal/app/app_test.go

Repository: ENTERPILOT/GoModel

Length of output: 892


Close staticRewriter.Rewrite before the new tests internal/app/app_test.go:647-649 — add the missing } after return nil, nil; without it, the package won’t compile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/app/app_test.go` around lines 647 - 677, The staticRewriter.Rewrite
method is missing its closing brace before the new test functions, which breaks
compilation. Close the Rewrite function immediately after the return nil, nil
statement, then keep the new anyProviderHasPassthroughUserHeaders tests below it
so the existing staticRewriter and test declarations remain properly separated.

Comment on lines +424 to +426
if values.PassthroughUserHeaders {
existing.PassthroughUserHeaders = true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Env override for PassthroughUserHeaders is enable-only.

overlayProviderEnvVars only sets existing.PassthroughUserHeaders = true when the env var is truthy; there's no path for the env var to force it back to false when YAML has already enabled it. Combined with parseBool treating false/unset identically, an operator cannot use the env var alone to disable passthrough for a provider that has it enabled via YAML — they'd need to edit the YAML. This may be intentional (mirrors the "env var alone shouldn't discover a provider" design), but worth confirming this is the desired operational behavior, since passthrough-header forwarding is security-sensitive and ops may want a fast env-only kill switch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/config.go` around lines 424 - 426, The
`overlayProviderEnvVars` handling for `PassthroughUserHeaders` is currently
enable-only, so an env var cannot force the field back to false once YAML has
enabled it. Update the config merge logic in `overlayProviderEnvVars` (and any
related boolean parsing in `parseBool`) so the env var can explicitly override
the YAML value in both directions if that is the intended behavior, or otherwise
document and enforce the one-way semantics consistently for
`PassthroughUserHeaders`.

Comment on lines +729 to +737
// defaultPassthroughSkipMode returns "skip" when the raw value is empty, and
// otherwise passes through the operator-supplied mode unchanged.
func defaultPassthroughSkipMode(raw string) string {
if strings.TrimSpace(raw) == "" {
return "skip"
}
return raw
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file first.
ast-grep outline internal/providers/config.go --view expanded || true
ast-grep outline internal/providers/headers.go --view expanded || true
ast-grep outline internal/providers/config_test.go --view expanded || true

# Locate the fields, defaults, and any exact-mode matching.
rg -n "PassthroughUserHeadersSkipMode|defaultPassthroughSkipMode|skip mode|only|skip" internal/providers/config.go internal/providers/headers.go internal/providers/config_test.go

Repository: ENTERPILOT/GoModel

Length of output: 16499


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '724,739p' internal/providers/config.go
printf '\n---\n'
sed -n '128,149p' internal/providers/headers.go
printf '\n---\n'
sed -n '90,150p' internal/providers/config_test.go

Repository: ENTERPILOT/GoModel

Length of output: 3767


Validate PassthroughUserHeadersSkipMode before storing it. defaultPassthroughSkipMode only fills the empty case; any typo or unsupported value is preserved and later treated as skip by shouldForward, which can silently widen an intended only/allow-list setup. Reject or warn on unknown values instead of accepting them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/config.go` around lines 729 - 737, Validate
PassthroughUserHeadersSkipMode before it is persisted instead of only
normalizing empty input in defaultPassthroughSkipMode. Update the config path in
internal/providers/config.go where this value is assigned so unknown or
misspelled modes are rejected or at least warned about, and ensure shouldForward
only receives approved values like skip or only rather than silently accepting
arbitrary strings. Keep the existing empty-value fallback behavior, but add
explicit validation at the PassthroughUserHeadersSkipMode storage/parse point.

Comment on lines +88 to +97
if cfg.PassthroughUserHeaders {
// Log if custom headers ignored due to passthrough
if len(cfg.CustomUpstreamHeaders) > 0 {
slog.Debug("custom_upstream_headers ignored because passthrough_user_headers is active")
}
applyPassthroughHeaders(req, cfg, userPathAlias)
} else if len(cfg.CustomUpstreamHeaders) > 0 {
applyStaticHeaders(req, cfg.CustomUpstreamHeaders, userPathAlias)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider slog.Warn instead of slog.Debug for the ignored-config log.

This log is the only signal that an operator's custom_upstream_headers config is being silently dropped because passthrough_user_headers is active. Debug logging is typically disabled in production, so this misconfiguration (which can cause required upstream headers to silently go missing) would go unnoticed without enabling debug logs to investigate.

♻️ Proposed fix
 	if cfg.PassthroughUserHeaders {
 		// Log if custom headers ignored due to passthrough
 		if len(cfg.CustomUpstreamHeaders) > 0 {
-			slog.Debug("custom_upstream_headers ignored because passthrough_user_headers is active")
+			slog.Warn("custom_upstream_headers ignored because passthrough_user_headers is active")
 		}

Note: headers_test.go asserts slog.LevelDebug specifically (e.g. TestApplyHeaderOverrides_PassthroughMode_LogsIgnoredCustomHeaders), so that assertion would need updating alongside this change.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if cfg.PassthroughUserHeaders {
// Log if custom headers ignored due to passthrough
if len(cfg.CustomUpstreamHeaders) > 0 {
slog.Debug("custom_upstream_headers ignored because passthrough_user_headers is active")
}
applyPassthroughHeaders(req, cfg, userPathAlias)
} else if len(cfg.CustomUpstreamHeaders) > 0 {
applyStaticHeaders(req, cfg.CustomUpstreamHeaders, userPathAlias)
}
}
if cfg.PassthroughUserHeaders {
// Log if custom headers ignored due to passthrough
if len(cfg.CustomUpstreamHeaders) > 0 {
slog.Warn("custom_upstream_headers ignored because passthrough_user_headers is active")
}
applyPassthroughHeaders(req, cfg, userPathAlias)
} else if len(cfg.CustomUpstreamHeaders) > 0 {
applyStaticHeaders(req, cfg.CustomUpstreamHeaders, userPathAlias)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/headers.go` around lines 88 - 97, The ignored custom
headers message in the passthrough branch of applyPassthroughHeaders should be
elevated from debug to warn so operators can see misconfigurations in
production. Update the slog call in internal/providers/headers.go to use a
warning level while keeping the same context message, and adjust the
corresponding expectation in headers_test.go, including
TestApplyHeaderOverrides_PassthroughMode_LogsIgnoredCustomHeaders, to assert the
new warning level instead of slog.LevelDebug.

Comment thread run/lifecycle_test.go
Comment on lines +149 to +176
func TestMain_SetUserPathHeaderWiring(t *testing.T) {
// Verify SetUserPathHeader exists and can be called without panic
factory := providers.NewProviderFactory()

// Test that SetUserPathHeader can be called with various valid headers
headers := []string{
"X-GoModel-User-Path",
"X-Tenant-Path",
"X-Custom-Header",
"", // empty string should not panic
}

for _, header := range headers {
didPanic := false
func() {
defer func() {
if r := recover(); r != nil {
didPanic = true
t.Errorf("SetUserPathHeader(%q) panicked: %v", header, r)
}
}()
factory.SetUserPathHeader(header)
}()
if didPanic {
continue
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Weak assertion — test can never fail meaningfully.

SetUserPathHeader (per internal/providers/factory.go) unconditionally stores the header with no validation, so it can never panic; this test only proves that trivial fact and doesn't verify the header is actually retained/used. Consider asserting the stored value is later reflected somewhere observable (e.g., via a getter or downstream effect), or drop it if there's no such assertion possible yet.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@run/lifecycle_test.go` around lines 149 - 176, The current
TestMain_SetUserPathHeaderWiring only checks that
providers.NewProviderFactory().SetUserPathHeader(...) does not panic, which is a
weak assertion because SetUserPathHeader appears to just store the value. Update
the test to verify observable behavior: after calling SetUserPathHeader on the
ProviderFactory, assert the header is actually retained or used through a getter
or downstream effect tied to that factory, or remove the test if no meaningful
assertion is available yet.

Comment on lines +27 to +56
func TestKimiReplayChatCompletion(t *testing.T) {
testCases := []struct {
name string
fixturePath string
}{
{name: "basic", fixturePath: "kimi/chat_completion.json"},
{name: "params", fixturePath: "kimi/chat_with_params.json"},
{name: "tools", fixturePath: "kimi/chat_with_tools.json"},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
provider := newKimiReplayProvider(t, map[string]replayRoute{
replayKey(http.MethodPost, "/chat/completions"): jsonFixtureRoute(t, tc.fixturePath),
})

resp, err := provider.ChatCompletion(context.Background(), &core.ChatRequest{
Model: "kimi-k2-0711",
Messages: []core.Message{{
Role: "user",
Content: "hello",
}},
})
require.NoError(t, err)
require.NotNil(t, resp)

compareGoldenJSON(t, goldenPathForFixture(tc.fixturePath), resp)
})
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

"params"/"tools" subtests don't test params or tools.

All three subtests send an identical core.ChatRequest (same model, same plain message) — only the mocked fixture path differs by name. Their fixtures (chat_with_params.json, chat_with_tools.json) are also byte-identical to chat_completion.json, with no tool_calls, no distinguishing usage/finish_reason, nothing param-specific. As written, the "params" and "tools" cases are duplicates of "basic" in disguise and don't verify request translation or tool-call parsing.

Consider sending requests with actual differentiating fields (e.g. Tools, Temperature/TopP) for the "params"/"tools" cases, and recording distinct upstream fixtures (e.g. a response containing tool_calls) to genuinely exercise that code path.

Based on path instructions: "Tests should cover request translation, response normalization, error handling, default configuration, and provider-specific parameter mapping."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/contract/kimi_test.go` around lines 27 - 56, The
TestKimiReplayChatCompletion subtests for "params" and "tools" currently
duplicate the "basic" request and only swap fixture names, so they do not
exercise request translation or tool-call handling. Update the test cases in
TestKimiReplayChatCompletion to send distinct core.ChatRequest inputs for the
"params" and "tools" cases by setting provider-specific fields such as
Temperature, TopP, and Tools, and use fixtures that actually reflect those
behaviors (for example, a response with tool_calls) so the replay tests validate
the Kimi provider’s parameter mapping and response parsing.

Source: Path instructions

@SantiagoDePolonia

SantiagoDePolonia commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

After a discussion with Fable 5 — summarized by AI.

This needs a round of changes before it can merge. Findings below, ordered by severity.

Blockers

1. The branch doesn't compile and CI is red.
The new tests in internal/app/app_test.go were inserted inside the body of staticRewriter.Rewrite — its closing brace is missing, so the whole package fails to parse (app_test.go:649: expected '(', found TestAnyProviderHasPassthroughUserHeaders_True). Both the Unit Tests and lint checks are failing on this. Additionally, five new files are missing trailing newlines (config/providers_test.go, internal/providers/kimi/kimi.go, internal/providers/kimi/kimi_test.go, internal/server/passthrough_headers.go, tests/contract/kimi_test.go) and several struct blocks are not gofmt-aligned (config/providers.go, internal/providers/factory.go, internal/providers/openai/compatible_provider.go). Please run gofmt and go test ./... locally before the next push.

2. The passthrough engine bypasses the tagging do_not_pass guarantee.
PassthroughHeaderCapture runs before the tagging middleware and snapshots all non-credential headers into the context; ApplyHeaderOverrides never consults the tagging strip set. Consequences:

  • On translated routes, a tagging header the operator explicitly marked do_not_pass is now forwarded upstream whenever a provider enables passthrough_user_headers (these routes previously never forwarded client headers).
  • On /p/ passthrough routes it's worse: in llmclient.buildRequest, the header setter (which now applies the context capture) runs before the per-request headers built by passthrough_support.go, and only keys present in req.Headers get deleted. Headers that passthrough_support deliberately stripped (do-not-pass headers, hop-by-hop Connection-listed headers) are re-added by the override engine.

The engine needs to respect the tagging strip set (core.TaggingStripHeadersFromContext) and the hop-by-hop set.

3. Forwarding Accept-Encoding breaks response decoding.
Go's HTTP transport only transparently decompresses gzip when it set Accept-Encoding. The default passthrough mode (skip + empty list) forwards the client's Accept-Encoding: gzip, deflate, br, … (which nearly every SDK sends), and config/config.example.yaml even recommends Accept-Encoding: "gzip, deflate, br" as a static custom header. Either way the upstream responds compressed and llmclient feeds compressed bytes to json.Unmarshal — every translated call (chat and embeddings) fails. The replay tests can't catch this because the mock never compresses. Please add a hard blocklist for transport/content-negotiation headers (Accept-Encoding, Connection, TE, Keep-Alive, Transfer-Encoding, Content-Length, Host) and remove Accept-Encoding/Content-Type from the example config.

Design changes

4. The feature applies to far more providers than the PR and docs claim.
Because NewCompatibleProvider auto-injects opts.HeaderOverrides, every compatible-adapter provider inherits it: OpenRouter, Azure, MiniMax, Xiaomi, Z.ai, Bailian, Oracle, OpenCode-Go, Fireworks — not just the six listed. docs/features/custom-headers.mdx says OpenRouter "ignores" the config; in reality passthrough can clobber OpenRouter's attribution headers via the delete-then-add rule — exactly what the PR description says it wanted to avoid. Either gate the auto-inject to the intended providers or (better) embrace the wider coverage and fix the docs, with a test that OpenRouter attribution headers survive.

5. custom_upstream_headers and passthrough_user_headers should compose, not be mutually exclusive.
With passthrough enabled, static headers are silently dropped (debug-level log only). The PR description's own Kimi example sets both — under the current semantics the static identity headers it demonstrates would never be sent. Suggested order: apply statics first, then passthrough (client values winning per header), or the reverse — but pick one and document it.

6. Default-open forwarding is a risky default.
skip mode with an empty list forwards all non-credential client headers (traceparent, X-Forwarded-For, internal tracing/baggage headers…). Consider making an empty configuration forward nothing (require an explicit allowlist), or at minimum add a prominent doc warning.

7. SetUserPathHeader is called after all providers are constructed.
In internal/app/app.go, cfg.Factory.SetUserPathHeader(...) runs after providers.Init has already created every provider, so opts.UserPathHeader is always "" at provider level — the per-provider alias blocking is dead code (currently masked by the server-side capture filter). Set it before Init, or pass it through the provider config.

8. The claimed passthrough defaults don't match the code.
The updated comment in config/server.go and CLAUDE.md claim the default ENABLED_PASSTHROUGH_PROVIDERS now includes bailian and kimi, but defaultEnabledPassthroughProviders in internal/server/passthrough_support.go is unchanged — /p/kimi/... doesn't work out of the box. Update the actual default or the docs, not just the comments.

Kimi provider: naming, default URL, and docs accuracy

We looked at Kimi's official documentation. There are two distinct products:

  • Moonshot open platformhttps://api.moonshot.ai/v1, pay-as-you-go, real model catalog (kimi-k2-thinking, kimi-k2-turbo-preview, …), documented endpoints /v1/chat/completions + /v1/files*, no /v1/embeddings.
  • Kimi Code (formerly Kimi for Coding) — https://api.kimi.com/coding/v1, a kimi.com membership benefit with its own API-key realm (keys are not interchangeable with platform keys), exactly one documented model (kimi-for-coding, a stable alias), and quota semantics (weekly refresh + rolling 5-hour rate window) instead of per-token billing.

The wire dialect is the same OpenAI-compatible format, so a separate provider type isn't justified — but defaulting a type named kimi to the membership-gated coding endpoint will surprise anyone holding a platform key. Please either:

  • (preferred) default the kimi type to https://api.moonshot.ai/v1 and document Kimi Code as a configured instance via the existing env-suffix convention (KIMI_CODE_BASE_URL=https://api.kimi.com/coding/v1, KIMI_CODE_API_KEY, KIMI_CODE_MODELS=kimi-for-coding → provider kimi-code), or
  • if this provider is deliberately Kimi Code only, name the type kimi-code to match the official product name.

Related docs/tooling fixes:

  • Remove the spoofed client-identity headers (User-Agent: KimiCLI/0.76, X-Title: Kimi CLI, X-Stainless-*, Http-Referer) from cmd/recordapi/main.go and config/config.example.yaml. Kimi officially supports third-party tools with nothing but the base URL and API key — these headers are unnecessary, and shipping impersonation examples isn't something this repo should endorse.
  • Remove the "ZooCode identity headers" section from docs/providers/kimi.mdx — we could find no such feature in Kimi's documentation (X-ZooCode-User/X-ZooCode-Plan/X-ZooCode-Team appear to be fabricated).
  • Embeddings: bge_m3_embed works today but is undocumented upstream — mark it as such ("may break without notice") and drop the nonexistent kimi-embedding example (the docs currently use both IDs inconsistently).
  • Pricing/limits: note that Kimi Code has no per-token pricing, so usage-cost tracking will show zero and the cost load-balancing strategy can't price it; given the rolling 5-hour quota window, recommend a conservative per-provider resilience.retry override so 429 retries don't burn quota.

Smaller cleanups

  • InitResult.ResolvedProviders duplicates the full credential-bearing provider map just so app.go can compute one boolean — expose a bool (or compute it inside Init) instead.
  • internal/providers/vllm/vllm.go: the new headerOverrides/userPathAlias struct fields appear unused (the closures already capture them).
  • docs/features/custom-headers.mdx: the allowlist example uses an anthropic: provider block, which the same page's Limitations section says ignores the feature.

Suggestion: split the PR

With the spoofed headers removed, the Kimi provider doesn't depend on the header-override feature at all. Splitting this into (a) the Kimi provider and (b) the header-override engine would make both much easier to land — (a) is close to ready once naming/docs are settled; (b) needs the security and composition rework above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants