Skip to content

Commit 1e8c995

Browse files
committed
Merge pull request #5568 from menloresearch/fix/min_p-validation-on-model-load
fix: min p validation on model load
1 parent 4229b9f commit 1e8c995

File tree

3 files changed

+278
-10
lines changed

3 files changed

+278
-10
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { ModelManager } from './manager'
2+
import { Model, ModelEvent } from '../../types'
3+
import { events } from '../events'
4+
5+
jest.mock('../events', () => ({
6+
events: {
7+
emit: jest.fn(),
8+
},
9+
}))
10+
11+
Object.defineProperty(global, 'window', {
12+
value: {
13+
core: {},
14+
},
15+
writable: true,
16+
})
17+
18+
describe('ModelManager', () => {
19+
let modelManager: ModelManager
20+
let mockModel: Model
21+
22+
beforeEach(() => {
23+
jest.clearAllMocks()
24+
;(global.window as any).core = {}
25+
modelManager = new ModelManager()
26+
mockModel = {
27+
id: 'test-model-1',
28+
name: 'Test Model',
29+
version: '1.0.0',
30+
} as Model
31+
})
32+
33+
describe('constructor', () => {
34+
it('should set itself on window.core.modelManager when window exists', () => {
35+
expect((global.window as any).core.modelManager).toBe(modelManager)
36+
})
37+
})
38+
39+
describe('register', () => {
40+
it('should register a new model', () => {
41+
modelManager.register(mockModel)
42+
43+
expect(modelManager.models.has('test-model-1')).toBe(true)
44+
expect(modelManager.models.get('test-model-1')).toEqual(mockModel)
45+
expect(events.emit).toHaveBeenCalledWith(ModelEvent.OnModelsUpdate, {})
46+
})
47+
48+
it('should merge existing model with new model data', () => {
49+
const existingModel: Model = {
50+
id: 'test-model-1',
51+
name: 'Existing Model',
52+
description: 'Existing description',
53+
} as Model
54+
55+
const updatedModel: Model = {
56+
id: 'test-model-1',
57+
name: 'Updated Model',
58+
version: '2.0.0',
59+
} as Model
60+
61+
modelManager.register(existingModel)
62+
modelManager.register(updatedModel)
63+
64+
const registeredModel = modelManager.models.get('test-model-1')
65+
expect(registeredModel).toEqual({
66+
id: 'test-model-1',
67+
name: 'Existing Model',
68+
description: 'Existing description',
69+
version: '2.0.0',
70+
})
71+
expect(events.emit).toHaveBeenCalledTimes(2)
72+
})
73+
})
74+
75+
describe('get', () => {
76+
it('should retrieve a registered model by id', () => {
77+
modelManager.register(mockModel)
78+
79+
const retrievedModel = modelManager.get('test-model-1')
80+
expect(retrievedModel).toEqual(mockModel)
81+
})
82+
83+
it('should return undefined for non-existent model', () => {
84+
const retrievedModel = modelManager.get('non-existent-model')
85+
expect(retrievedModel).toBeUndefined()
86+
})
87+
88+
it('should return correctly typed model', () => {
89+
modelManager.register(mockModel)
90+
91+
const retrievedModel = modelManager.get<Model>('test-model-1')
92+
expect(retrievedModel?.id).toBe('test-model-1')
93+
expect(retrievedModel?.name).toBe('Test Model')
94+
})
95+
})
96+
97+
describe('instance', () => {
98+
it('should create a new instance when none exists on window.core', () => {
99+
;(global.window as any).core = {}
100+
101+
const instance = ModelManager.instance()
102+
expect(instance).toBeInstanceOf(ModelManager)
103+
expect((global.window as any).core.modelManager).toBe(instance)
104+
})
105+
106+
it('should return existing instance when it exists on window.core', () => {
107+
const existingManager = new ModelManager()
108+
;(global.window as any).core.modelManager = existingManager
109+
110+
const instance = ModelManager.instance()
111+
expect(instance).toBe(existingManager)
112+
})
113+
})
114+
115+
describe('models property', () => {
116+
it('should initialize with empty Map', () => {
117+
expect(modelManager.models).toBeInstanceOf(Map)
118+
expect(modelManager.models.size).toBe(0)
119+
})
120+
121+
it('should maintain multiple models', () => {
122+
const model1: Model = { id: 'model-1', name: 'Model 1' } as Model
123+
const model2: Model = { id: 'model-2', name: 'Model 2' } as Model
124+
125+
modelManager.register(model1)
126+
modelManager.register(model2)
127+
128+
expect(modelManager.models.size).toBe(2)
129+
expect(modelManager.models.get('model-1')).toEqual(model1)
130+
expect(modelManager.models.get('model-2')).toEqual(model2)
131+
})
132+
})
133+
})

core/src/browser/models/utils.test.ts

Lines changed: 143 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,33 @@ describe('validationRules', () => {
152152
expect(validationRules.text_model('true')).toBe(false)
153153
expect(validationRules.text_model(1)).toBe(false)
154154
})
155+
156+
it('should validate repeat_last_n correctly', () => {
157+
expect(validationRules.repeat_last_n(5)).toBe(true)
158+
expect(validationRules.repeat_last_n(-5)).toBe(true)
159+
expect(validationRules.repeat_last_n(0)).toBe(true)
160+
expect(validationRules.repeat_last_n(1.5)).toBe(true)
161+
expect(validationRules.repeat_last_n('5')).toBe(false)
162+
expect(validationRules.repeat_last_n(null)).toBe(false)
163+
})
164+
165+
it('should validate repeat_penalty correctly', () => {
166+
expect(validationRules.repeat_penalty(1.1)).toBe(true)
167+
expect(validationRules.repeat_penalty(0.9)).toBe(true)
168+
expect(validationRules.repeat_penalty(0)).toBe(true)
169+
expect(validationRules.repeat_penalty(-1)).toBe(true)
170+
expect(validationRules.repeat_penalty('1.1')).toBe(false)
171+
expect(validationRules.repeat_penalty(null)).toBe(false)
172+
})
173+
174+
it('should validate min_p correctly', () => {
175+
expect(validationRules.min_p(0.1)).toBe(true)
176+
expect(validationRules.min_p(0)).toBe(true)
177+
expect(validationRules.min_p(-0.1)).toBe(true)
178+
expect(validationRules.min_p(1.5)).toBe(true)
179+
expect(validationRules.min_p('0.1')).toBe(false)
180+
expect(validationRules.min_p(null)).toBe(false)
181+
})
155182
})
156183

157184
it('should normalize invalid values for keys not listed in validationRules', () => {
@@ -192,18 +219,125 @@ describe('normalizeValue', () => {
192219
expect(normalizeValue('cpu_threads', '4')).toBe(4)
193220
expect(normalizeValue('cpu_threads', 0)).toBe(0)
194221
})
195-
})
196222

197-
it('should handle invalid values correctly by falling back to originParams', () => {
198-
const modelParams = { temperature: 'invalid', token_limit: -1 }
199-
const originParams = { temperature: 0.5, token_limit: 100 }
200-
expect(extractInferenceParams(modelParams as any, originParams)).toEqual(originParams)
223+
it('should handle edge cases for normalization', () => {
224+
expect(normalizeValue('ctx_len', -5.7)).toBe(-6)
225+
expect(normalizeValue('token_limit', 'abc')).toBeNaN()
226+
expect(normalizeValue('max_tokens', null)).toBe(0)
227+
expect(normalizeValue('ngl', undefined)).toBeNaN()
228+
expect(normalizeValue('n_parallel', Infinity)).toBe(Infinity)
229+
expect(normalizeValue('cpu_threads', -Infinity)).toBe(-Infinity)
230+
})
231+
232+
it('should not normalize non-integer parameters', () => {
233+
expect(normalizeValue('temperature', 1.5)).toBe(1.5)
234+
expect(normalizeValue('top_p', 0.9)).toBe(0.9)
235+
expect(normalizeValue('stream', true)).toBe(true)
236+
expect(normalizeValue('prompt_template', 'template')).toBe('template')
237+
})
201238
})
202239

203-
it('should return an empty object when no modelParams are provided', () => {
204-
expect(extractModelLoadParams()).toEqual({})
240+
describe('extractInferenceParams', () => {
241+
it('should handle invalid values correctly by falling back to originParams', () => {
242+
const modelParams = { temperature: 'invalid', token_limit: -1 }
243+
const originParams = { temperature: 0.5, token_limit: 100 }
244+
expect(extractInferenceParams(modelParams as any, originParams)).toEqual(originParams)
245+
})
246+
247+
it('should return an empty object when no modelParams are provided', () => {
248+
expect(extractInferenceParams()).toEqual({})
249+
})
250+
251+
it('should extract and normalize valid inference parameters', () => {
252+
const modelParams = {
253+
temperature: 1.5,
254+
token_limit: 100.7,
255+
top_p: 0.9,
256+
stream: true,
257+
max_tokens: 50.3,
258+
invalid_param: 'should_be_ignored'
259+
}
260+
261+
const result = extractInferenceParams(modelParams as any)
262+
expect(result).toEqual({
263+
temperature: 1.5,
264+
token_limit: 100,
265+
top_p: 0.9,
266+
stream: true,
267+
max_tokens: 50
268+
})
269+
})
270+
271+
it('should handle parameters without validation rules', () => {
272+
const modelParams = { engine: 'llama' }
273+
const result = extractInferenceParams(modelParams as any)
274+
expect(result).toEqual({ engine: 'llama' })
275+
})
276+
277+
it('should skip invalid values when no origin params provided', () => {
278+
const modelParams = { temperature: 'invalid', top_p: 0.8 }
279+
const result = extractInferenceParams(modelParams as any)
280+
expect(result).toEqual({ top_p: 0.8 })
281+
})
205282
})
206283

207-
it('should return an empty object when no modelParams are provided', () => {
208-
expect(extractInferenceParams()).toEqual({})
284+
describe('extractModelLoadParams', () => {
285+
it('should return an empty object when no modelParams are provided', () => {
286+
expect(extractModelLoadParams()).toEqual({})
287+
})
288+
289+
it('should extract and normalize valid model load parameters', () => {
290+
const modelParams = {
291+
ctx_len: 2048.5,
292+
ngl: 12.7,
293+
embedding: true,
294+
n_parallel: 4.2,
295+
cpu_threads: 8.9,
296+
prompt_template: 'template',
297+
llama_model_path: '/path/to/model',
298+
vision_model: false,
299+
invalid_param: 'should_be_ignored'
300+
}
301+
302+
const result = extractModelLoadParams(modelParams as any)
303+
expect(result).toEqual({
304+
ctx_len: 2048,
305+
ngl: 12,
306+
embedding: true,
307+
n_parallel: 4,
308+
cpu_threads: 8,
309+
prompt_template: 'template',
310+
llama_model_path: '/path/to/model',
311+
vision_model: false
312+
})
313+
})
314+
315+
it('should handle parameters without validation rules', () => {
316+
const modelParams = {
317+
engine: 'llama',
318+
pre_prompt: 'System:',
319+
system_prompt: 'You are helpful',
320+
model_path: '/path'
321+
}
322+
const result = extractModelLoadParams(modelParams as any)
323+
expect(result).toEqual({
324+
engine: 'llama',
325+
pre_prompt: 'System:',
326+
system_prompt: 'You are helpful',
327+
model_path: '/path'
328+
})
329+
})
330+
331+
it('should fall back to origin params for invalid values', () => {
332+
const modelParams = { ctx_len: -1, ngl: 'invalid' }
333+
const originParams = { ctx_len: 2048, ngl: 12 }
334+
const result = extractModelLoadParams(modelParams as any, originParams)
335+
expect(result).toEqual({})
336+
})
337+
338+
it('should skip invalid values when no origin params provided', () => {
339+
const modelParams = { ctx_len: -1, embedding: true }
340+
const result = extractModelLoadParams(modelParams as any)
341+
expect(result).toEqual({ embedding: true })
342+
})
209343
})

core/src/browser/models/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ export const validationRules: { [key: string]: (value: any) => boolean } = {
1717
presence_penalty: (value: any) => typeof value === 'number' && value >= 0 && value <= 1,
1818
repeat_last_n: (value: any) => typeof value === 'number',
1919
repeat_penalty: (value: any) => typeof value === 'number',
20+
min_p: (value: any) => typeof value === 'number',
2021

2122
ctx_len: (value: any) => Number.isInteger(value) && value >= 0,
22-
ngl: (value: any) => Number.isInteger(value),
23+
ngl: (value: any) => Number.isInteger(value) && value >= 0,
2324
embedding: (value: any) => typeof value === 'boolean',
2425
n_parallel: (value: any) => Number.isInteger(value) && value >= 0,
2526
cpu_threads: (value: any) => Number.isInteger(value) && value >= 0,

0 commit comments

Comments
 (0)