diff --git a/src/integrations/runtimeMetadata.test.ts b/src/integrations/runtimeMetadata.test.ts new file mode 100644 index 0000000000..8527a5ebd2 --- /dev/null +++ b/src/integrations/runtimeMetadata.test.ts @@ -0,0 +1,90 @@ +import { describe, it, expect } from 'bun:test' +import { resolveOpenAIShimRuntimeContext } from '../integrations/runtimeMetadata' + +describe('resolveOpenAIShimRuntimeContext - segment-boundary heuristic', () => { + describe('DeepSeek models', () => { + it('should NOT infer preserveReasoningContent for custom aliases (false-positive case)', () => { + // my-deepseek-rag is a custom alias, NOT a provider path + // Should NOT trigger the DeepSeek detection + const result = resolveOpenAIShimRuntimeContext({ + model: 'my-deepseek-rag', + }) + // Custom aliases should NOT get preserveReasoningContent + expect(result.openaiShimConfig.preserveReasoningContent).toBeUndefined() + }) + + it('should infer preserveReasoningContent for openrouter/deepseek/... paths (true-positive case)', () => { + // openrouter/deepseek/deepseek-chat is a provider path with segments + // Should trigger the DeepSeek detection + const result = resolveOpenAIShimRuntimeContext({ + model: 'openrouter/deepseek/deepseek-chat', + }) + expect(result.openaiShimConfig.preserveReasoningContent).toBe(true) + expect(result.openaiShimConfig.reasoningContentFallback).toBe('') + }) + + it('should infer preserveReasoningContent for accounts/fireworks/... paths (true-positive case)', () => { + // accounts/fireworks/models/deepseek-v3 is a provider path with multiple segments + // Should trigger the DeepSeek detection + const result = resolveOpenAIShimRuntimeContext({ + model: 'accounts/fireworks/models/deepseek-v3', + }) + expect(result.openaiShimConfig.preserveReasoningContent).toBe(true) + expect(result.openaiShimConfig.reasoningContentFallback).toBe('') + }) + + it('should infer preserveReasoningContent for deepseek-chat directly (standard case)', () => { + const result = resolveOpenAIShimRuntimeContext({ + model: 'deepseek-chat', + }) + expect(result.openaiShimConfig.preserveReasoningContent).toBe(true) + }) + + it('should infer preserveReasoningContent for deepseek-coder (model name)', () => { + const result = resolveOpenAIShimRuntimeContext({ + model: 'deepseek-coder', + }) + expect(result.openaiShimConfig.preserveReasoningContent).toBe(true) + }) + }) + + describe('Kimi/Moonshot models', () => { + it('should NOT infer preserveReasoningContent for custom kimi aliases', () => { + // Custom alias should not trigger + const result = resolveOpenAIShimRuntimeContext({ + model: 'my-kimi-assistant', + }) + expect(result.openaiShimConfig.preserveReasoningContent).toBeUndefined() + }) + + it('should infer preserveReasoningContent for moonshot AI paths', () => { + const result = resolveOpenAIShimRuntimeContext({ + model: 'moonshot/moonshot-v1', + }) + expect(result.openaiShimConfig.preserveReasoningContent).toBe(true) + }) + + it('should infer preserveReasoningContent for kimi on moonshot paths', () => { + const result = resolveOpenAIShimRuntimeContext({ + model: 'moonshot/kimi-kpro', + }) + expect(result.openaiShimConfig.preserveReasoningContent).toBe(true) + }) + }) + + describe('Non-matching models', () => { + it('should return undefined for gpt-4o (negative case)', () => { + const result = resolveOpenAIShimRuntimeContext({ + model: 'gpt-4o', + }) + expect(result.openaiShimConfig.preserveReasoningContent).toBeUndefined() + }) + + it('should return undefined for claude models (negative case)', () => { + const result = resolveOpenAIShimRuntimeContext({ + model: 'claude-sonnet-4-20250514', + }) + expect(result.openaiShimConfig.preserveReasoningContent).toBeUndefined() + }) + }) +}) \ No newline at end of file diff --git a/src/integrations/runtimeMetadata.ts b/src/integrations/runtimeMetadata.ts index a3ab3c3971..b70ac29e8d 100644 --- a/src/integrations/runtimeMetadata.ts +++ b/src/integrations/runtimeMetadata.ts @@ -151,7 +151,11 @@ function inferRemoteModelOpenAIShimConfig( } } - if (normalizedModel.includes('deepseek')) { + // Segment-boundary-aware matcher: avoids false-positives like "my-deepseek-rag" + // while still catching aggregator paths e.g. "openrouter/deepseek/deepseek-chat". + const segments = normalizedModel.split('/') + const hasDeepseek = segments.some(s => s.startsWith('deepseek')) + if (hasDeepseek) { return { preserveReasoningContent: true, requireReasoningContentOnAssistantMessages: true, @@ -162,7 +166,10 @@ function inferRemoteModelOpenAIShimConfig( } } - if (normalizedModel.includes('kimi') || normalizedModel.includes('moonshot')) { + const hasKimiMoonshot = segments.some( + s => s.startsWith('kimi') || s.startsWith('moonshot'), + ) + if (hasKimiMoonshot) { return { preserveReasoningContent: true, requireReasoningContentOnAssistantMessages: true,