mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
🐛 fix: handle Kimi code thinking mode (#15725)
This commit is contained in:
@@ -11,6 +11,14 @@ import {
|
||||
params,
|
||||
} from './index';
|
||||
|
||||
const { loadModelsMock } = vi.hoisted(() => ({
|
||||
loadModelsMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@lobechat/business-model-bank/model-config', () => ({
|
||||
loadModels: loadModelsMock,
|
||||
}));
|
||||
|
||||
const defaultOpenAIBaseURL = 'https://api.moonshot.cn/v1';
|
||||
const anthropicBaseURL = 'https://api.moonshot.cn/anthropic';
|
||||
|
||||
@@ -18,6 +26,10 @@ const anthropicBaseURL = 'https://api.moonshot.cn/anthropic';
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
beforeEach(() => {
|
||||
loadModelsMock.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
describe('LobeMoonshotAI', () => {
|
||||
const createRuntime = ({
|
||||
baseURL,
|
||||
@@ -390,6 +402,18 @@ describe('LobeMoonshotOpenAI', () => {
|
||||
expect(payload.temperature).toBe(1);
|
||||
});
|
||||
|
||||
it('should always enable thinking for kimi-k2.7-code', async () => {
|
||||
await instance.chat({
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
model: 'kimi-k2.7-code',
|
||||
thinking: { budget_tokens: 0, type: 'disabled' },
|
||||
});
|
||||
|
||||
const payload = getLastRequestPayload();
|
||||
expect(payload.thinking).toEqual({ type: 'enabled' });
|
||||
expect(payload.temperature).toBe(1);
|
||||
});
|
||||
|
||||
it('should force reasoning_content on assistant messages', async () => {
|
||||
await instance.chat({
|
||||
messages: [
|
||||
@@ -662,6 +686,21 @@ describe('LobeMoonshotAnthropicAI', () => {
|
||||
expect(payload.temperature).toBe(1);
|
||||
});
|
||||
|
||||
it('should always enable thinking for kimi-k2.7-code', async () => {
|
||||
await instance.chat({
|
||||
messages: [{ content: 'Hello', role: 'user' }],
|
||||
model: 'kimi-k2.7-code',
|
||||
thinking: { budget_tokens: 0, type: 'disabled' },
|
||||
});
|
||||
|
||||
const payload = getLastRequestPayload();
|
||||
expect(payload.thinking).toEqual({
|
||||
budget_tokens: 1024,
|
||||
type: 'enabled',
|
||||
});
|
||||
expect(payload.temperature).toBe(1);
|
||||
});
|
||||
|
||||
it('should force thinking block on assistant messages', async () => {
|
||||
await instance.chat({
|
||||
messages: [
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { CreateRouterRuntimeOptions } from '../../core/RouterRuntime';
|
||||
import { createRouterRuntime } from '../../core/RouterRuntime';
|
||||
import type { ChatStreamPayload } from '../../types';
|
||||
import { getModelPropertyWithFallback } from '../../utils/getFallbackModelProperty';
|
||||
import { isKimiNativeThinkingModel, isKimiThinkingToggleModel } from '../../utils/kimiModelId';
|
||||
import { MODEL_LIST_CONFIGS, processModelList } from '../../utils/modelParse';
|
||||
|
||||
export interface MoonshotModelCard {
|
||||
@@ -30,14 +31,6 @@ type MoonshotSDKType = 'anthropic' | 'openai';
|
||||
|
||||
// Shared constants and helpers
|
||||
const MOONSHOT_SEARCH_TOOL = { function: { name: '$web_search' }, type: 'builtin_function' } as any;
|
||||
/**
|
||||
* Matches kimi-k2.N models (K2.5, K2.6, ...) that expose a thinking toggle via
|
||||
* `payload.thinking.type`. Assumes every future kimi-k2.N release keeps the same
|
||||
* toggle contract and param constraints; if Moonshot diverges, introduce an
|
||||
* explicit allowlist instead of widening this prefix.
|
||||
*/
|
||||
const isKimiThinkingToggleModel = (model: string) => model.startsWith('kimi-k2.');
|
||||
const isKimiNativeThinkingModel = (model: string) => model.startsWith('kimi-k2-thinking');
|
||||
const isEmptyContent = (content: any) =>
|
||||
content === '' || content === null || content === undefined;
|
||||
const hasValidReasoning = (reasoning: any) => reasoning?.content && !reasoning?.signature;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
isKimiNativeThinkingModel,
|
||||
isKimiThinkingToggleModel,
|
||||
parseKimiModelId,
|
||||
} from './kimiModelId';
|
||||
|
||||
describe('parseKimiModelId', () => {
|
||||
it('should parse Kimi K2 minor-version ids', () => {
|
||||
expect(parseKimiModelId('kimi-k2.6')).toEqual({
|
||||
family: 'k',
|
||||
majorVersion: 2,
|
||||
minorVersion: 6,
|
||||
normalizedModelId: 'kimi-k2.6',
|
||||
source: 'moonshot',
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse Kimi K2 variant ids', () => {
|
||||
expect(parseKimiModelId('kimi-k2.7-code')).toEqual({
|
||||
family: 'k',
|
||||
majorVersion: 2,
|
||||
minorVersion: 7,
|
||||
normalizedModelId: 'kimi-k2.7-code',
|
||||
source: 'moonshot',
|
||||
variant: 'code',
|
||||
});
|
||||
|
||||
expect(parseKimiModelId('kimi-k2-thinking-turbo')).toEqual({
|
||||
family: 'k',
|
||||
majorVersion: 2,
|
||||
normalizedModelId: 'kimi-k2-thinking-turbo',
|
||||
source: 'moonshot',
|
||||
variant: 'thinking-turbo',
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse OpenRouter Kimi ids', () => {
|
||||
expect(parseKimiModelId('moonshotai/kimi-k2.7-code')).toEqual({
|
||||
family: 'k',
|
||||
majorVersion: 2,
|
||||
minorVersion: 7,
|
||||
normalizedModelId: 'kimi-k2.7-code',
|
||||
source: 'openRouter',
|
||||
variant: 'code',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for non-Kimi ids', () => {
|
||||
expect(parseKimiModelId('claude-sonnet-4-5')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isKimiThinkingToggleModel', () => {
|
||||
it('should return true for Kimi K2 models with switchable thinking', () => {
|
||||
expect(isKimiThinkingToggleModel('kimi-k2.5')).toBe(true);
|
||||
expect(isKimiThinkingToggleModel('kimi-k2.6')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for always-thinking and legacy Kimi K2 ids', () => {
|
||||
expect(isKimiThinkingToggleModel('kimi-k2.7-code')).toBe(false);
|
||||
expect(isKimiThinkingToggleModel('kimi-k2-thinking')).toBe(false);
|
||||
expect(isKimiThinkingToggleModel('kimi-k2-turbo-preview')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isKimiNativeThinkingModel', () => {
|
||||
it('should return true for native thinking Kimi models', () => {
|
||||
expect(isKimiNativeThinkingModel('kimi-k2-thinking')).toBe(true);
|
||||
expect(isKimiNativeThinkingModel('kimi-k2-thinking-turbo')).toBe(true);
|
||||
expect(isKimiNativeThinkingModel('kimi-k2.7-code')).toBe(true);
|
||||
expect(isKimiNativeThinkingModel('kimi-k2.8-code')).toBe(true);
|
||||
expect(isKimiNativeThinkingModel('kimi-k2.8-code-preview')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for switchable Kimi K2 models', () => {
|
||||
expect(isKimiNativeThinkingModel('kimi-k2.5')).toBe(false);
|
||||
expect(isKimiNativeThinkingModel('kimi-k2.6')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
export type KimiModelIdSource = 'moonshot' | 'openRouter';
|
||||
|
||||
export interface ParsedKimiModelId {
|
||||
family: 'k';
|
||||
majorVersion: number;
|
||||
minorVersion?: number;
|
||||
normalizedModelId: string;
|
||||
source: KimiModelIdSource;
|
||||
variant?: string;
|
||||
}
|
||||
|
||||
interface ExtractedKimiModelId {
|
||||
normalizedModelId: string;
|
||||
source: KimiModelIdSource;
|
||||
}
|
||||
|
||||
const KIMI_MODEL_PATTERN =
|
||||
/^kimi-k(\d+)(?:\.(\d+))?(?:-([a-z][a-z0-9]*(?:-[a-z0-9]+)*))?(?:\b|[-.:])/;
|
||||
|
||||
const extractKimiModelId = (model: string): ExtractedKimiModelId | undefined => {
|
||||
const normalized = model.trim().toLowerCase();
|
||||
if (!normalized) return;
|
||||
|
||||
if (normalized.startsWith('moonshotai/')) {
|
||||
return { normalizedModelId: normalized.slice('moonshotai/'.length), source: 'openRouter' };
|
||||
}
|
||||
|
||||
if (normalized.startsWith('kimi-')) {
|
||||
return { normalizedModelId: normalized, source: 'moonshot' };
|
||||
}
|
||||
};
|
||||
|
||||
const parseMinorVersion = (value: string | undefined): Pick<ParsedKimiModelId, 'minorVersion'> => {
|
||||
if (!value || !/^\d{1,2}$/.test(value)) return {};
|
||||
|
||||
return {
|
||||
minorVersion: Number(value),
|
||||
};
|
||||
};
|
||||
|
||||
export const parseKimiModelId = (model: string): ParsedKimiModelId | undefined => {
|
||||
const extracted = extractKimiModelId(model);
|
||||
if (!extracted) return;
|
||||
|
||||
const match = KIMI_MODEL_PATTERN.exec(extracted.normalizedModelId);
|
||||
if (!match) return;
|
||||
|
||||
const [, majorVersion, minorVersion, variant] = match;
|
||||
|
||||
return {
|
||||
family: 'k',
|
||||
majorVersion: Number(majorVersion),
|
||||
normalizedModelId: extracted.normalizedModelId,
|
||||
source: extracted.source,
|
||||
...(variant ? { variant } : {}),
|
||||
...parseMinorVersion(minorVersion),
|
||||
};
|
||||
};
|
||||
|
||||
const hasVariant = (parsed: ParsedKimiModelId, variant: string): boolean =>
|
||||
parsed.variant === variant || !!parsed.variant?.startsWith(`${variant}-`);
|
||||
|
||||
export const isKimiNativeThinkingModel = (model: string): boolean => {
|
||||
const parsed = parseKimiModelId(model);
|
||||
if (!parsed) return false;
|
||||
|
||||
if (parsed.majorVersion !== 2) return false;
|
||||
if (hasVariant(parsed, 'thinking')) return true;
|
||||
|
||||
return (
|
||||
hasVariant(parsed, 'code') && parsed.minorVersion !== undefined && parsed.minorVersion >= 7
|
||||
);
|
||||
};
|
||||
|
||||
export const isKimiThinkingToggleModel = (model: string): boolean => {
|
||||
const parsed = parseKimiModelId(model);
|
||||
if (!parsed) return false;
|
||||
|
||||
return (
|
||||
parsed.majorVersion === 2 &&
|
||||
parsed.minorVersion !== undefined &&
|
||||
!isKimiNativeThinkingModel(model)
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user