2026-04-26 04:10:01 +02:00
import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
import { mkdirSync , mkdtempSync , rmSync , writeFileSync } from 'fs'
2026-04-25 12:57:22 +02:00
import { join } from 'path'
import { tmpdir } from 'os'
2026-04-26 04:10:01 +02:00
let homeDir = ''
2026-05-12 20:56:04 +08:00
const originalHermesHome = process . env . HERMES_HOME
const originalLocalAppData = process . env . LOCALAPPDATA
const originalAppData = process . env . APPDATA
2026-04-26 04:10:01 +02:00
function hermesPath ( . . . parts : string [ ] ) {
return join ( homeDir , '.hermes' , . . . parts )
2026-04-25 12:57:22 +02:00
}
2026-04-26 04:10:01 +02:00
function writeConfig ( content : string ) {
mkdirSync ( hermesPath ( ) , { recursive : true } )
writeFileSync ( hermesPath ( 'config.yaml' ) , content )
2026-04-25 12:57:22 +02:00
}
2026-04-26 04:10:01 +02:00
function writeModelsCache ( data : Record < string , unknown > ) {
mkdirSync ( hermesPath ( ) , { recursive : true } )
writeFileSync ( hermesPath ( 'models_dev_cache.json' ) , JSON . stringify ( data ) )
2026-04-25 12:57:22 +02:00
}
2026-04-26 04:10:01 +02:00
async function loadModelContext() {
2026-05-12 20:56:04 +08:00
process . env . HERMES_HOME = hermesPath ( )
delete process . env . LOCALAPPDATA
delete process . env . APPDATA
2026-04-25 12:57:22 +02:00
vi . resetModules ( )
2026-04-26 04:10:01 +02:00
vi . doMock ( 'os' , async ( ) = > ( {
. . . ( await vi . importActual < typeof import ( 'os' ) > ( 'os' ) ) ,
homedir : ( ) = > homeDir ,
} ) )
2026-05-06 21:37:13 +08:00
// Mock getDb to return null to avoid "database is locked" errors in parallel tests
vi . doMock ( '../../packages/server/src/db/index' , async ( ) = > {
const actual = await vi . importActual < typeof import ( '../../packages/server/src/db/index' ) > ( '../../packages/server/src/db/index' )
return {
. . . actual ,
getDb : ( ) = > null ,
}
} )
2026-04-26 04:10:01 +02:00
return import ( '../../packages/server/src/services/hermes/model-context' )
2026-04-25 12:57:22 +02:00
}
2026-04-26 04:10:01 +02:00
describe ( 'getModelContextLength' , ( ) = > {
2026-04-25 12:57:22 +02:00
beforeEach ( ( ) = > {
2026-04-26 04:10:01 +02:00
homeDir = mkdtempSync ( join ( tmpdir ( ) , 'hwui-model-context-' ) )
2026-04-25 12:57:22 +02:00
} )
afterEach ( ( ) = > {
2026-04-26 04:10:01 +02:00
vi . doUnmock ( 'os' )
2026-05-12 20:56:04 +08:00
if ( originalHermesHome === undefined ) delete process . env . HERMES_HOME
else process . env . HERMES_HOME = originalHermesHome
if ( originalLocalAppData === undefined ) delete process . env . LOCALAPPDATA
else process . env . LOCALAPPDATA = originalLocalAppData
if ( originalAppData === undefined ) delete process . env . APPDATA
else process . env . APPDATA = originalAppData
2026-04-26 04:10:01 +02:00
if ( homeDir ) rmSync ( homeDir , { recursive : true , force : true } )
homeDir = ''
} )
it ( 'does not borrow a same-named model context from another provider when the configured provider is uncached' , async ( ) = > {
writeConfig ( ` model: \ n default: gpt-5.5 \ n provider: openai-codex \ n ` )
writeModelsCache ( {
openai : {
models : {
'gpt-5.5' : { limit : { context : 1_050_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
2026-05-26 19:35:48 +08:00
expect ( getModelContextLength ( ) ) . toBe ( 256 _000 )
2026-04-25 12:57:22 +02:00
} )
2026-04-26 04:10:01 +02:00
it ( 'does not scan other providers when the configured provider exists without that model' , async ( ) = > {
writeConfig ( ` model: \ n default: gpt-5.5 \ n provider: openai-codex \ n ` )
writeModelsCache ( {
'openai-codex' : {
models : {
2026-05-26 19:35:48 +08:00
'gpt-5.4' : { limit : { context : 256_000 } } ,
2026-04-26 04:10:01 +02:00
} ,
} ,
openai : {
models : {
'gpt-5.5' : { limit : { context : 1_050_000 } } ,
} ,
} ,
} )
2026-04-25 12:57:22 +02:00
2026-04-26 04:10:01 +02:00
const { getModelContextLength } = await loadModelContext ( )
2026-04-25 12:57:22 +02:00
2026-05-26 19:35:48 +08:00
expect ( getModelContextLength ( ) ) . toBe ( 256 _000 )
2026-04-25 12:57:22 +02:00
} )
2026-04-26 04:10:01 +02:00
it ( 'uses the configured provider cache entry when the provider matches' , async ( ) = > {
writeConfig ( ` model: \ n default: gpt-5.5 \ n provider: openai \ n ` )
writeModelsCache ( {
openai : {
models : {
'gpt-5.5' : { limit : { context : 1_050_000 } } ,
} ,
} ,
} )
2026-04-25 12:57:22 +02:00
2026-04-26 04:10:01 +02:00
const { getModelContextLength } = await loadModelContext ( )
2026-04-25 12:57:22 +02:00
2026-04-26 04:10:01 +02:00
expect ( getModelContextLength ( ) ) . toBe ( 1 _050_000 )
2026-04-25 12:57:22 +02:00
} )
2026-06-01 09:31:58 +08:00
it ( 'prefers requested provider model context_length over top-level default context_length' , async ( ) = > {
writeConfig ( ` model: \ n default: gpt-5.5 \ n provider: openai-codex \ n context_length: 272000 \ n \ nproviders: \ n qwen: \ n name: Qwen \ n default_model: qwen3.6-plus \ n models: \ n qwen3.6-plus: \ n context_length: 1048576 \ n ` )
const { getModelContextLength } = await loadModelContext ( )
expect ( getModelContextLength ( { provider : 'qwen' , model : 'qwen3.6-plus' } ) ) . toBe ( 1 _048_576 )
} )
it ( 'uses provider-level context_length when the requested model belongs to that provider' , async ( ) = > {
writeConfig ( ` model: \ n default: gpt-5.5 \ n provider: openai-codex \ n context_length: 272000 \ n \ nproviders: \ n qwen: \ n name: Qwen \ n default_model: qwen3.6-plus \ n models: \ n - qwen3.6-plus \ n context_length: 1048576 \ n ` )
const { getModelContextLength } = await loadModelContext ( )
expect ( getModelContextLength ( { provider : 'qwen' , model : 'qwen3.6-plus' } ) ) . toBe ( 1 _048_576 )
} )
2026-04-26 04:10:01 +02:00
it ( 'keeps legacy model-name cache lookup when no provider is configured' , async ( ) = > {
writeConfig ( ` model: \ n default: gpt-5.5 \ n ` )
writeModelsCache ( {
openai : {
models : {
'gpt-5.5' : { limit : { context : 1_050_000 } } ,
} ,
} ,
} )
2026-04-25 12:57:22 +02:00
2026-04-26 04:10:01 +02:00
const { getModelContextLength } = await loadModelContext ( )
2026-04-25 12:57:22 +02:00
expect ( getModelContextLength ( ) ) . toBe ( 1 _050_000 )
} )
2026-04-26 04:10:01 +02:00
it ( 'keeps providerless legacy lookup on global exact matches before prefixed suffix matches' , async ( ) = > {
writeConfig ( ` model: \ n default: gpt-5 \ n ` )
writeModelsCache ( {
vercel : {
models : {
'openai/gpt-5' : { limit : { context : 1_000_000 } } ,
} ,
} ,
openai : {
models : {
'gpt-5' : { limit : { context : 400_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
expect ( getModelContextLength ( ) ) . toBe ( 400 _000 )
} )
it ( 'maps WUI provider keys to model-cache provider keys before looking up limits' , async ( ) = > {
writeConfig ( ` model: \ n default: gemini-3.1-pro-preview \ n provider: gemini \ n ` )
writeModelsCache ( {
google : {
models : {
'gemini-3.1-pro-preview' : { limit : { context : 1_000_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
expect ( getModelContextLength ( ) ) . toBe ( 1 _000_000 )
} )
it ( 'uses gateway provider aliases with prefixed model names inside the aliased provider only' , async ( ) = > {
writeConfig ( ` model: \ n default: openai/gpt-5 \ n provider: ai-gateway \ n ` )
writeModelsCache ( {
vercel : {
models : {
'openai/gpt-5' : { limit : { context : 1_000_000 } } ,
} ,
} ,
openai : {
models : {
'gpt-5' : { limit : { context : 400_000 } } ,
} ,
} ,
} )
2026-04-25 12:57:22 +02:00
2026-04-26 04:10:01 +02:00
const { getModelContextLength } = await loadModelContext ( )
2026-04-25 12:57:22 +02:00
expect ( getModelContextLength ( ) ) . toBe ( 1 _000_000 )
} )
2026-05-06 09:16:44 +02:00
it ( 'resolves provider: custom through model.base_url before falling back to the default context length' , async ( ) = > {
writeConfig ( ` model: \ n default: deepseek-v4-pro \ n provider: custom \ n base_url: https://api.deepseek.com \ n ` )
writeModelsCache ( {
deepseek : {
models : {
'deepseek-v4-pro' : { limit : { context : 1_000_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
expect ( getModelContextLength ( ) ) . toBe ( 1 _000_000 )
} )
it ( 'resolves custom:name providers when the matched custom provider base_url points at a builtin provider' , async ( ) = > {
writeConfig ( ` model: \ n default: deepseek-v4-pro \ n provider: custom:deepseek \ n \ ncustom_providers: \ n - name: deepseek \ n base_url: https://api.deepseek.com \ n model: deepseek-v4-pro \ n ` )
writeModelsCache ( {
deepseek : {
models : {
'deepseek-v4-pro' : { limit : { context : 1_000_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
expect ( getModelContextLength ( ) ) . toBe ( 1 _000_000 )
} )
it ( 'prefers the builtin provider inferred from a matched custom provider base_url over an arbitrary custom provider name' , async ( ) = > {
writeConfig ( ` model: \ n default: shared-model \ n provider: custom:corp-proxy \ n \ ncustom_providers: \ n - name: corp-proxy \ n base_url: https://api.deepseek.com \ n model: shared-model \ n ` )
writeModelsCache ( {
deepseek : {
models : {
'shared-model' : { limit : { context : 1_000_000 } } ,
} ,
} ,
openai : {
models : {
'shared-model' : { limit : { context : 400_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
expect ( getModelContextLength ( ) ) . toBe ( 1 _000_000 )
} )
it ( 'does not trust a stale custom:name provider hint without a matching custom provider entry' , async ( ) = > {
writeConfig ( ` model: \ n default: deepseek-v4-pro \ n provider: custom:deepseek \ n ` )
writeModelsCache ( {
deepseek : {
models : {
'deepseek-v4-pro' : { limit : { context : 1_000_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
2026-05-26 19:35:48 +08:00
expect ( getModelContextLength ( ) ) . toBe ( 256 _000 )
2026-05-06 09:16:44 +02:00
} )
it ( 'does not trust custom:name alone when the matched custom provider entry points at an unknown proxy url' , async ( ) = > {
writeConfig ( ` model: \ n default: deepseek-v4-pro \ n provider: custom:deepseek \ n \ ncustom_providers: \ n - name: deepseek \ n base_url: https://proxy.example.com/v1 \ n model: deepseek-v4-pro \ n ` )
writeModelsCache ( {
deepseek : {
models : {
'deepseek-v4-pro' : { limit : { context : 1_000_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
2026-05-26 19:35:48 +08:00
expect ( getModelContextLength ( ) ) . toBe ( 256 _000 )
2026-05-06 09:16:44 +02:00
} )
it ( 'does not fall through to a unique global match after a resolved custom:name provider misses in its scoped cache provider' , async ( ) = > {
writeConfig ( ` model: \ n default: gpt-5.5 \ n provider: custom:deepseek \ n \ ncustom_providers: \ n - name: deepseek \ n base_url: https://api.deepseek.com \ n model: gpt-5.5 \ n ` )
writeModelsCache ( {
openai : {
models : {
'gpt-5.5' : { limit : { context : 400_000 } } ,
} ,
} ,
deepseek : {
models : {
'deepseek-v4-pro' : { limit : { context : 1_000_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
2026-05-26 19:35:48 +08:00
expect ( getModelContextLength ( ) ) . toBe ( 256 _000 )
2026-05-06 09:16:44 +02:00
} )
it ( 'allows a unique global model-name fallback for unresolved custom providers' , async ( ) = > {
writeConfig ( ` model: \ n default: deepseek-v4-pro \ n provider: custom \ n base_url: https://proxy.example.com/v1 \ n ` )
writeModelsCache ( {
deepseek : {
models : {
'deepseek-v4-pro' : { limit : { context : 1_000_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
expect ( getModelContextLength ( ) ) . toBe ( 1 _000_000 )
} )
it ( 'still allows the unique global fallback when provider: custom matches a custom provider entry that cannot be mapped to a builtin cache provider' , async ( ) = > {
writeConfig ( ` model: \ n default: deepseek-v4-pro \ n provider: custom \ n \ ncustom_providers: \ n - name: corp-proxy \ n base_url: https://proxy.example.com/v1 \ n model: deepseek-v4-pro \ n ` )
writeModelsCache ( {
deepseek : {
models : {
'deepseek-v4-pro' : { limit : { context : 1_000_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
expect ( getModelContextLength ( ) ) . toBe ( 1 _000_000 )
} )
it ( 'keeps the unresolved custom-provider fallback strict to exact or case-insensitive model-name matches' , async ( ) = > {
writeConfig ( ` model: \ n default: gpt-5 \ n provider: custom \ n base_url: https://proxy.example.com/v1 \ n ` )
writeModelsCache ( {
vercel : {
models : {
'openai/gpt-5' : { limit : { context : 1_000_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
2026-05-26 19:35:48 +08:00
expect ( getModelContextLength ( ) ) . toBe ( 256 _000 )
2026-05-06 09:16:44 +02:00
} )
it ( 'does not guess across multiple cache providers when a custom provider remains unresolved' , async ( ) = > {
writeConfig ( ` model: \ n default: shared-model \ n provider: custom \ n base_url: https://proxy.example.com/v1 \ n ` )
writeModelsCache ( {
deepseek : {
models : {
'shared-model' : { limit : { context : 1_000_000 } } ,
} ,
} ,
openai : {
models : {
'shared-model' : { limit : { context : 400_000 } } ,
} ,
} ,
} )
const { getModelContextLength } = await loadModelContext ( )
2026-05-26 19:35:48 +08:00
expect ( getModelContextLength ( ) ) . toBe ( 256 _000 )
2026-05-06 09:16:44 +02:00
} )
2026-04-25 12:57:22 +02:00
} )