kimi-code/packages/node-sdk/test/runtime-provider-identity.test.ts
Kai 821847cb4b
feat(managed-kimi-code): route anthropic protocol via beta api (#1186)
* feat(managed-kimi-code): route anthropic protocol via beta api

- kosong: add betaApi option to use client.beta.messages.create
- agent-core: thread alias betaApi into the anthropic provider config
- oauth: route managed models on the anthropic protocol through the beta Messages API

* feat(providers): add KIMI_CODE_CUSTOM_HEADERS support

- Add KIMI_CODE_CUSTOM_HEADERS env var for custom outbound LLM headers
- Send User-Agent to non-Kimi providers
- Forward Kimi identity headers to model catalog fetches
- Support defaultHeaders in Google GenAI provider

* feat(agent-core): add protocol attrs to turn and api error telemetry

- Add type/protocol/alias to api_error for per-protocol error attribution

- Add turn_ended event with reason/duration/mode/type/protocol

- Add type/protocol to turn_interrupted

* chore(oauth): remove hardcoded internal dev endpoint from shared OAuth base URLs

---------

Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
2026-06-29 14:24:01 +08:00

186 lines
5.3 KiB
TypeScript

import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import type { KimiConfig } from '@moonshot-ai/agent-core';
import { createKimiDefaultHeaders, KIMI_CODE_PLATFORM } from '@moonshot-ai/kimi-code-oauth';
import { ProviderManager } from '../../agent-core/src/session/provider-manager';
import { SDKRpcClient } from '#/index';
import { TEST_IDENTITY } from './test-identity';
const tempDirs: string[] = [];
function resolveRuntimeProvider(options: {
readonly config: KimiConfig;
readonly model?: string;
readonly kimiRequestHeaders?: Record<string, string>;
}) {
const manager = new ProviderManager({
config: options.config,
kimiRequestHeaders: options.kimiRequestHeaders,
});
const model = options.model ?? options.config.defaultModel;
if (model === undefined) {
throw new Error('No model selected');
}
return manager.resolveProviderConfig(model);
}
async function makeTempDir(): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'kimi-sdk-provider-identity-'));
tempDirs.push(dir);
return dir;
}
afterEach(async () => {
for (const dir of tempDirs.splice(0)) {
await rm(dir, { recursive: true, force: true });
}
});
describe('runtime provider identity headers', () => {
it('preserves the host user agent suffix in SDK RPC headers', async () => {
const homeDir = await makeTempDir();
const client = new SDKRpcClient({
homeDir,
identity: {
...TEST_IDENTITY,
userAgentSuffix: 'web-runtime',
},
});
const core = client.core as unknown as {
readonly kimiRequestHeaders?: Record<string, string>;
};
try {
expect(core.kimiRequestHeaders).toMatchObject({
'User-Agent': 'kimi-code-cli/0.0.0-test (web-runtime)',
'X-Msh-Version': '0.0.0-test',
});
} finally {
await client.close();
}
});
it('adds kimi-code-cli User-Agent and complete X-Msh headers to the default Kimi provider', async () => {
const homeDir = await makeTempDir();
const kimiRequestHeaders = createKimiDefaultHeaders({ homeDir, ...TEST_IDENTITY });
const resolved = resolveRuntimeProvider({
config: {
defaultModel: 'kimi-model',
providers: {
kimi: {
type: 'kimi',
apiKey: 'test-key',
},
},
models: {
'kimi-model': {
provider: 'kimi',
model: 'kimi-model',
maxContextSize: 1000,
},
},
},
kimiRequestHeaders,
});
expect(resolved.provider).toMatchObject({
type: 'kimi',
defaultHeaders: expect.objectContaining({
'User-Agent': 'kimi-code-cli/0.0.0-test',
'X-Msh-Platform': KIMI_CODE_PLATFORM,
'X-Msh-Version': '0.0.0-test',
'X-Msh-Device-Name': expect.any(String),
'X-Msh-Device-Model': expect.any(String),
'X-Msh-Os-Version': expect.any(String),
'X-Msh-Device-Id': expect.stringMatching(/^[0-9a-f-]+$/),
}),
});
});
it('lets Kimi provider customHeaders override default identity headers', async () => {
const homeDir = await makeTempDir();
const kimiRequestHeaders = createKimiDefaultHeaders({ homeDir, ...TEST_IDENTITY });
const config: KimiConfig = {
providers: {
kimi: {
type: 'kimi',
apiKey: 'test-key',
customHeaders: {
'User-Agent': 'Custom/1',
'X-Msh-Version': 'override-version',
},
},
},
defaultProvider: 'kimi',
defaultModel: 'kimi-model',
models: {
'kimi-model': {
provider: 'kimi',
model: 'kimi-model',
maxContextSize: 1000,
},
},
};
const resolved = resolveRuntimeProvider({
config,
kimiRequestHeaders,
});
expect(resolved.provider).toMatchObject({
type: 'kimi',
defaultHeaders: expect.objectContaining({
'User-Agent': 'Custom/1',
'X-Msh-Version': 'override-version',
'X-Msh-Platform': KIMI_CODE_PLATFORM,
}),
});
});
it('applies only the User-Agent (no device identity headers) to non-Kimi providers', async () => {
const homeDir = await makeTempDir();
const kimiRequestHeaders = createKimiDefaultHeaders({ homeDir, ...TEST_IDENTITY });
const config: KimiConfig = {
providers: {
openai: {
type: 'openai',
baseUrl: 'https://example.test/v1',
apiKey: 'sk-test',
},
},
defaultProvider: 'openai',
defaultModel: 'gpt-test',
models: {
'gpt-test': {
provider: 'openai',
model: 'gpt-test',
maxContextSize: 1000,
},
},
};
const resolved = resolveRuntimeProvider({
config,
kimiRequestHeaders,
});
expect(resolved.provider).toMatchObject({
type: 'openai',
model: 'gpt-test',
defaultHeaders: {
'User-Agent': `kimi-code-cli/${TEST_IDENTITY.version}`,
},
});
// Device identity headers (`X-Msh-*`) stay Kimi-only — must not leak to
// third-party providers.
const headers = (resolved.provider as { defaultHeaders?: Record<string, string> })
.defaultHeaders;
expect(headers).toBeDefined();
expect(headers).not.toHaveProperty('X-Msh-Platform');
});
});