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
This commit is contained in:
Vincent Koc 2026-03-12 13:30:07 -04:00 committed by GitHub
parent 33ba3ce951
commit 86135d5889
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 147 additions and 2 deletions

View File

@ -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.

View File

@ -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: {

View File

@ -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,
};
}

View File

@ -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();
}
});
});

View File

@ -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,

View File

@ -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,
};
}),

View File

@ -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<string, string> }).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",