From 86135d5889dfa8e22aceb5ed36ca657e689a1174 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 12 Mar 2026 13:30:07 -0400 Subject: [PATCH] Kimi Coding: set default subscription user agent (#44248) * Providers: set default Kimi coding user agent * Tests: cover Kimi coding header overrides * Changelog: note Kimi coding user agent * Tests: satisfy Kimi provider fixture type * Update CHANGELOG.md * Providers: preserve Kimi headers through models merge --- CHANGELOG.md | 1 + src/agents/models-config.merge.test.ts | 34 +++++++++++++++++ src/agents/models-config.merge.ts | 29 ++++++++++++++- ...odels-config.providers.kimi-coding.test.ts | 30 +++++++++++++++ src/agents/models-config.providers.static.ts | 4 ++ src/agents/models-config.providers.ts | 14 ++++++- src/agents/pi-embedded-runner/model.test.ts | 37 +++++++++++++++++++ 7 files changed, 147 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 483fd2a1bfe..a3b5426231c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Models/Kimi Coding: send the built-in `User-Agent: claude-code/0.1.0` header by default for `kimi-coding` while still allowing explicit provider headers to override it, so Kimi Code subscription auth can work without a local header-injection proxy. (#30099) Thanks @Amineelfarssi and @vincentkoc. - Security/plugins: disable implicit workspace plugin auto-load so cloned repositories cannot execute workspace plugin code without an explicit trust decision. (`GHSA-99qw-6mr3-36qr`)(#44174) Thanks @lintsinghua and @vincentkoc. - Moonshot CN API: respect explicit `baseUrl` (api.moonshot.cn) in implicit provider resolution so platform.moonshot.cn API keys authenticate correctly instead of returning HTTP 401. (#33637) Thanks @chengzhichao-xydt. - Kimi Coding/provider config: respect explicit `models.providers["kimi-coding"].baseUrl` when resolving the implicit provider so custom Kimi Coding endpoints no longer get overwritten by the built-in default. (#36353) Thanks @2233admin. diff --git a/src/agents/models-config.merge.test.ts b/src/agents/models-config.merge.test.ts index 60c3624c3c1..42aa6216aa4 100644 --- a/src/agents/models-config.merge.test.ts +++ b/src/agents/models-config.merge.test.ts @@ -66,6 +66,40 @@ describe("models-config merge helpers", () => { }); }); + it("preserves implicit provider headers when explicit config adds extra headers", () => { + const merged = mergeProviderModels( + { + api: "anthropic-messages", + headers: { "User-Agent": "claude-code/0.1.0" }, + models: [ + { + id: "k2p5", + name: "Kimi for Coding", + input: ["text", "image"], + reasoning: true, + }, + ], + } as ProviderConfig, + { + api: "anthropic-messages", + headers: { "X-Kimi-Tenant": "tenant-a" }, + models: [ + { + id: "k2p5", + name: "Kimi for Coding", + input: ["text", "image"], + reasoning: true, + }, + ], + } as ProviderConfig, + ); + + expect(merged.headers).toEqual({ + "User-Agent": "claude-code/0.1.0", + "X-Kimi-Tenant": "tenant-a", + }); + }); + it("replaces stale baseUrl when model api surface changes", () => { const merged = mergeWithExistingProviderSecrets({ nextProviders: { diff --git a/src/agents/models-config.merge.ts b/src/agents/models-config.merge.ts index e227ee413d5..da4f0e8a005 100644 --- a/src/agents/models-config.merge.ts +++ b/src/agents/models-config.merge.ts @@ -39,8 +39,27 @@ export function mergeProviderModels( ): ProviderConfig { const implicitModels = Array.isArray(implicit.models) ? implicit.models : []; const explicitModels = Array.isArray(explicit.models) ? explicit.models : []; + const implicitHeaders = + implicit.headers && typeof implicit.headers === "object" && !Array.isArray(implicit.headers) + ? implicit.headers + : undefined; + const explicitHeaders = + explicit.headers && typeof explicit.headers === "object" && !Array.isArray(explicit.headers) + ? explicit.headers + : undefined; if (implicitModels.length === 0) { - return { ...implicit, ...explicit }; + return { + ...implicit, + ...explicit, + ...(implicitHeaders || explicitHeaders + ? { + headers: { + ...implicitHeaders, + ...explicitHeaders, + }, + } + : {}), + }; } const implicitById = new Map( @@ -93,6 +112,14 @@ export function mergeProviderModels( return { ...implicit, ...explicit, + ...(implicitHeaders || explicitHeaders + ? { + headers: { + ...implicitHeaders, + ...explicitHeaders, + }, + } + : {}), models: mergedModels, }; } diff --git a/src/agents/models-config.providers.kimi-coding.test.ts b/src/agents/models-config.providers.kimi-coding.test.ts index 4467dfc4cab..91ca62f34e2 100644 --- a/src/agents/models-config.providers.kimi-coding.test.ts +++ b/src/agents/models-config.providers.kimi-coding.test.ts @@ -26,6 +26,7 @@ describe("kimi-coding implicit provider (#22409)", () => { const provider = buildKimiCodingProvider(); expect(provider.api).toBe("anthropic-messages"); expect(provider.baseUrl).toBe("https://api.kimi.com/coding/"); + expect(provider.headers).toEqual({ "User-Agent": "claude-code/0.1.0" }); expect(provider.models).toBeDefined(); expect(provider.models.length).toBeGreaterThan(0); expect(provider.models[0].id).toBe("k2p5"); @@ -65,4 +66,33 @@ describe("kimi-coding implicit provider (#22409)", () => { envSnapshot.restore(); } }); + + it("merges explicit kimi-coding headers on top of the built-in user agent", async () => { + const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); + const envSnapshot = captureEnv(["KIMI_API_KEY"]); + process.env.KIMI_API_KEY = "test-key"; + + try { + const providers = await resolveImplicitProvidersForTest({ + agentDir, + explicitProviders: { + "kimi-coding": { + baseUrl: "https://api.kimi.com/coding/", + api: "anthropic-messages", + headers: { + "User-Agent": "custom-kimi-client/1.0", + "X-Kimi-Tenant": "tenant-a", + }, + models: buildKimiCodingProvider().models, + }, + }, + }); + expect(providers?.["kimi-coding"]?.headers).toEqual({ + "User-Agent": "custom-kimi-client/1.0", + "X-Kimi-Tenant": "tenant-a", + }); + } finally { + envSnapshot.restore(); + } + }); }); diff --git a/src/agents/models-config.providers.static.ts b/src/agents/models-config.providers.static.ts index c525cb32f53..c0361d02762 100644 --- a/src/agents/models-config.providers.static.ts +++ b/src/agents/models-config.providers.static.ts @@ -95,6 +95,7 @@ const MOONSHOT_DEFAULT_COST = { }; const KIMI_CODING_BASE_URL = "https://api.kimi.com/coding/"; +const KIMI_CODING_USER_AGENT = "claude-code/0.1.0"; const KIMI_CODING_DEFAULT_MODEL_ID = "k2p5"; const KIMI_CODING_DEFAULT_CONTEXT_WINDOW = 262144; const KIMI_CODING_DEFAULT_MAX_TOKENS = 32768; @@ -308,6 +309,9 @@ export function buildKimiCodingProvider(): ProviderConfig { return { baseUrl: KIMI_CODING_BASE_URL, api: "anthropic-messages", + headers: { + "User-Agent": KIMI_CODING_USER_AGENT, + }, models: [ { id: KIMI_CODING_DEFAULT_MODEL_ID, diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 0c8b961afe1..86f52c3a871 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -667,12 +667,24 @@ const SIMPLE_IMPLICIT_PROVIDER_LOADERS: ImplicitProviderLoader[] = [ }; }), withApiKey("kimi-coding", async ({ apiKey, explicitProvider }) => { + const builtInProvider = buildKimiCodingProvider(); const explicitBaseUrl = explicitProvider?.baseUrl; + const explicitHeaders = isRecord(explicitProvider?.headers) + ? (explicitProvider.headers as ProviderConfig["headers"]) + : undefined; return { - ...buildKimiCodingProvider(), + ...builtInProvider, ...(typeof explicitBaseUrl === "string" && explicitBaseUrl.trim() ? { baseUrl: explicitBaseUrl.trim() } : {}), + ...(explicitHeaders + ? { + headers: { + ...builtInProvider.headers, + ...explicitHeaders, + }, + } + : {}), apiKey, }; }), diff --git a/src/agents/pi-embedded-runner/model.test.ts b/src/agents/pi-embedded-runner/model.test.ts index 062369d9a96..7c3279e314a 100644 --- a/src/agents/pi-embedded-runner/model.test.ts +++ b/src/agents/pi-embedded-runner/model.test.ts @@ -915,6 +915,43 @@ describe("resolveModel", () => { }); }); + it("lets provider config override registry-found kimi user agent headers", () => { + mockDiscoveredModel({ + provider: "kimi-coding", + modelId: "k2p5", + templateModel: { + ...buildForwardCompatTemplate({ + id: "k2p5", + name: "Kimi for Coding", + provider: "kimi-coding", + api: "anthropic-messages", + baseUrl: "https://api.kimi.com/coding/", + }), + headers: { "User-Agent": "claude-code/0.1.0" }, + }, + }); + + const cfg = { + models: { + providers: { + "kimi-coding": { + headers: { + "User-Agent": "custom-kimi-client/1.0", + "X-Kimi-Tenant": "tenant-a", + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + const result = resolveModel("kimi-coding", "k2p5", "/tmp/agent", cfg); + expect(result.error).toBeUndefined(); + expect((result.model as unknown as { headers?: Record }).headers).toEqual({ + "User-Agent": "custom-kimi-client/1.0", + "X-Kimi-Tenant": "tenant-a", + }); + }); + it("does not override when no provider config exists", () => { mockDiscoveredModel({ provider: "anthropic",