fokemal / server /src /__tests__ /lib /client-classifier.test.ts
automindy's picture
Sync to upstream v0.13.7, keep HF Space adaptations
dcedb25
Raw History Blame Contribute Delete
5.65 kB
import { describe, expect, it } from 'vitest';
import type { Request } from 'express';
import { classifyClientAgent } from '../../lib/client-classifier.js';
function request(path: string, headers: Record<string, string> = {}): Request {
return {
originalUrl: path,
headers,
} as unknown as Request;
}
describe('client agent classification', () => {
it('prefers distinctive headers and protocol paths', () => {
expect(classifyClientAgent(request('/v1/messages', {
'x-claude-code-session-id': 'session',
}))).toBe('claude-code');
expect(classifyClientAgent(request('/v1/responses'))).toBe('codex');
expect(classifyClientAgent(request('/v1beta/models/x:generateContent'))).toBe('gemini-cli');
expect(classifyClientAgent(request('/api/chat'))).toBe('ollama-client');
expect(classifyClientAgent(request('/v1/t/revocable/responses'))).toBe('codex');
expect(classifyClientAgent(request('/v1/t/revocable/api/chat'))).toBe('ollama-client');
});
it('recognizes common coding-agent user agents', () => {
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'opencode/1.2.3',
}))).toBe('opencode');
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'aider/0.80',
}))).toBe('aider');
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'Kilo-Code/4.2',
}))).toBe('kilo-code');
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'crush/0.8',
}))).toBe('crush');
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'deepseek-harness/0.3.1 (+https://github.com/deepseek-ai/deepseek-harness)',
}))).toBe('deepseek-harness');
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'atomcode/5.0.9',
}))).toBe('atomcode');
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'RooCode/3.50.4',
}))).toBe('roo');
});
it('recognizes OpenClaw by the static header its generated provider sends', () => {
// A custom provider in OpenClaw gets the bare openai-node UA, so the
// generator sets `headers: { User-Agent: 'openclaw' }`; the bare word and
// the `openclaw/<version>` form OpenClaw uses natively both count.
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'openclaw',
}))).toBe('openclaw');
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'openclaw/2026.9.4',
}))).toBe('openclaw');
// The unmodified SDK UA still files under openai-sdk, so a hand-written
// provider without the header is not misattributed.
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'OpenAI/JS 7.8.0',
}))).toBe('openai-sdk');
});
it('recognizes Hermes Agent by its header and its model-probe UA', () => {
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'hermes-agent',
}))).toBe('hermes-agent');
expect(classifyClientAgent(request('/v1/models', {
'user-agent': 'hermes-cli/0.21.3',
}))).toBe('hermes-agent');
});
it('recognizes Pi by its header and its native UA without catching other `pi` substrings', () => {
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'pi-coding-agent',
}))).toBe('pi');
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'pi/0.99.2 (darwin; node/v24.4.0; arm64)',
}))).toBe('pi');
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'OpenAI/JS 6.10.0',
}))).toBe('openai-sdk');
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'my-api-client/1.0',
}))).not.toBe('pi');
});
it('recognizes Reasonix by the UA it sends to every endpoint', () => {
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'Reasonix/2.29.0',
}))).toBe('reasonix');
// Its web_fetch tool is not a model request from the agent loop.
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'reasonix-web-fetch/1.0',
}))).not.toBe('reasonix');
});
it('separates MiMo Code from the OpenCode it derives from', () => {
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'mimo/0.4.0',
}))).toBe('mimo-code');
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'MiMo-Code/0.4.0 (opencode)',
}))).toBe('mimo-code');
expect(classifyClientAgent(request('/v1/chat/completions', {
'user-agent': 'opencode/1.2.3',
}))).toBe('opencode');
});
it('recognizes Claude Code by its real claude-cli user agent', () => {
expect(classifyClientAgent(request('/v1/messages', {
'user-agent': 'claude-cli/1.0.62 (external, cli)',
}))).toBe('claude-code');
expect(classifyClientAgent(request('/v1/messages', {
'x-session-id': 'affinity',
}))).toBe('claude-code');
expect(classifyClientAgent(request('/v1/messages'))).toBe('anthropic-sdk');
});
it('lets user agents beat wire-format fallbacks', () => {
// Qwen Code speaks the Gemini wire; it must not count as gemini-cli.
expect(classifyClientAgent(request('/v1beta/models/x:generateContent', {
'user-agent': 'QwenCode/0.5 qwen-code',
}))).toBe('qwen-code');
// A plain openai SDK on /v1/responses is not Codex.
expect(classifyClientAgent(request('/v1/responses', {
'user-agent': 'OpenAI/NodeJS/4.52.0',
}))).toBe('openai-sdk');
});
});