chore: bootstrap hermes-ui skeleton from 0.6.29 snapshot
Import the client, server, skills, tests, and build scripts as the starting point for the rebuild, then trim the surfaces this project does not ship: Electron desktop packaging, the marketing website, and the ESP32 firmware tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows. Rename the package and CLI entry points to hermes-ui, drop the prepare hook so npm ci no longer triggers a build, and drop the removed tsconfig and nodemon references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs so the documented package map and hard rules match this repository. reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
mockReadFile,
|
||||
mockFetchProviderModels,
|
||||
mockReadConfigYamlForProfile,
|
||||
mockReadText,
|
||||
mockUpdateText,
|
||||
mockListProfileNamesFromDisk,
|
||||
mockGetProfileDir,
|
||||
mockReadAppConfig,
|
||||
mockResolveCopilotOAuthToken,
|
||||
mockGlobalFetch,
|
||||
} = vi.hoisted(() => ({
|
||||
mockReadFile: vi.fn(),
|
||||
mockFetchProviderModels: vi.fn(),
|
||||
mockReadConfigYamlForProfile: vi.fn(),
|
||||
mockReadText: vi.fn(),
|
||||
mockUpdateText: vi.fn(),
|
||||
mockListProfileNamesFromDisk: vi.fn(),
|
||||
mockGetProfileDir: vi.fn(),
|
||||
mockReadAppConfig: vi.fn(),
|
||||
mockResolveCopilotOAuthToken: vi.fn(),
|
||||
mockGlobalFetch: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('fs/promises', () => ({
|
||||
readFile: mockReadFile,
|
||||
}))
|
||||
|
||||
vi.mock('../../packages/server/src/config', () => ({
|
||||
config: { appHome: '/app-home' },
|
||||
}))
|
||||
|
||||
vi.mock('../../packages/server/src/shared/providers', () => ({
|
||||
PROVIDER_PRESETS: [
|
||||
{
|
||||
value: 'openrouter',
|
||||
label: 'OpenRouter',
|
||||
base_url: 'https://openrouter.ai/api/v1',
|
||||
models: ['openrouter/fallback'],
|
||||
},
|
||||
{
|
||||
value: 'deepseek',
|
||||
label: 'DeepSeek',
|
||||
base_url: 'https://api.deepseek.com/v1',
|
||||
models: ['deepseek-chat'],
|
||||
},
|
||||
{
|
||||
value: 'openai-codex',
|
||||
label: 'OpenAI Codex',
|
||||
base_url: 'https://chatgpt.com/backend-api/codex',
|
||||
models: ['gpt-5.5', 'gpt-5.4-mini'],
|
||||
},
|
||||
{
|
||||
value: 'xai-oauth',
|
||||
label: 'xAI Grok OAuth',
|
||||
base_url: 'https://api.x.ai/v1',
|
||||
models: ['grok-4.3'],
|
||||
},
|
||||
{
|
||||
value: 'copilot',
|
||||
label: 'GitHub Copilot',
|
||||
base_url: 'https://api.githubcopilot.com',
|
||||
models: ['gpt-5.5', 'claude-sonnet-4.6'],
|
||||
},
|
||||
{
|
||||
value: 'nous',
|
||||
label: 'Nous Portal',
|
||||
base_url: 'https://inference-api.nousresearch.com/v1',
|
||||
models: ['anthropic/claude-opus-4.8'],
|
||||
},
|
||||
{
|
||||
value: 'google-gemini-cli',
|
||||
label: 'Google Gemini OAuth',
|
||||
base_url: 'cloudcode-pa://google',
|
||||
models: ['gemini-3.1-pro-preview', 'gemini-3-pro-preview'],
|
||||
},
|
||||
{
|
||||
value: 'claude-oauth',
|
||||
label: 'Claude OAuth',
|
||||
base_url: 'https://api.anthropic.com',
|
||||
models: ['claude-sonnet-4-6', 'claude-opus-4-7'],
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
vi.mock('../../packages/server/src/services/config-helpers', () => ({
|
||||
PROVIDER_ENV_MAP: {
|
||||
openrouter: { api_key_env: 'OPENROUTER_API_KEY', base_url_env: 'OPENROUTER_BASE_URL' },
|
||||
deepseek: { api_key_env: 'DEEPSEEK_API_KEY', base_url_env: 'DEEPSEEK_BASE_URL' },
|
||||
'openai-codex': { api_key_env: '', base_url_env: '' },
|
||||
'xai-oauth': { api_key_env: '', base_url_env: '' },
|
||||
copilot: { api_key_env: 'GITHUB_TOKEN', base_url_env: '' },
|
||||
nous: { api_key_env: '', base_url_env: '' },
|
||||
'google-gemini-cli': { api_key_env: '', base_url_env: '' },
|
||||
'claude-oauth': { api_key_env: '', base_url_env: '' },
|
||||
},
|
||||
fetchProviderModels: mockFetchProviderModels,
|
||||
readConfigYamlForProfile: mockReadConfigYamlForProfile,
|
||||
}))
|
||||
|
||||
vi.mock('../../packages/server/src/services/app-config', () => ({
|
||||
readAppConfig: mockReadAppConfig,
|
||||
}))
|
||||
|
||||
vi.mock('../../packages/server/src/services/hermes/copilot-models', () => ({
|
||||
resolveCopilotOAuthToken: mockResolveCopilotOAuthToken,
|
||||
}))
|
||||
|
||||
vi.mock('../../packages/server/src/services/hermes/hermes-profile', () => ({
|
||||
getProfileDir: mockGetProfileDir,
|
||||
listProfileNamesFromDisk: mockListProfileNamesFromDisk,
|
||||
}))
|
||||
|
||||
vi.mock('../../packages/server/src/services/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../packages/server/src/services/safe-file-store', () => ({
|
||||
safeFileStore: {
|
||||
readText: mockReadText,
|
||||
updateText: mockUpdateText,
|
||||
},
|
||||
}))
|
||||
|
||||
describe('model catalog cache', () => {
|
||||
let cacheText = ''
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
cacheText = ''
|
||||
mockListProfileNamesFromDisk.mockReturnValue(['default', 'team'])
|
||||
mockGetProfileDir.mockImplementation((profile: string) => `/hermes/${profile}`)
|
||||
mockReadText.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' }))
|
||||
mockUpdateText.mockImplementation(async (_path: string, updater: (current: string) => string) => {
|
||||
cacheText = updater(cacheText)
|
||||
})
|
||||
mockReadAppConfig.mockResolvedValue({})
|
||||
mockResolveCopilotOAuthToken.mockResolvedValue('')
|
||||
mockFetchProviderModels.mockResolvedValue([])
|
||||
mockGlobalFetch.mockResolvedValue({ ok: false, status: 404, json: async () => ({}) })
|
||||
vi.stubGlobal('fetch', mockGlobalFetch)
|
||||
mockReadFile.mockImplementation(async (path: string) => {
|
||||
if (path === '/hermes/default/.env') return 'OPENROUTER_API_KEY=default-openrouter\n'
|
||||
if (path === '/hermes/team/.env') {
|
||||
return [
|
||||
'OPENROUTER_API_KEY=team-openrouter',
|
||||
'DEEPSEEK_API_KEY=team-deepseek',
|
||||
].join('\n')
|
||||
}
|
||||
throw Object.assign(new Error('missing'), { code: 'ENOENT' })
|
||||
})
|
||||
mockReadConfigYamlForProfile.mockImplementation(async (profile: string) => {
|
||||
if (profile === 'default') {
|
||||
return {
|
||||
custom_providers: [
|
||||
{ name: 'Shared Local', base_url: 'https://custom.local/v1', api_key: 'custom-a', model: 'local-a' },
|
||||
],
|
||||
}
|
||||
}
|
||||
return {
|
||||
custom_providers: [
|
||||
{ name: 'Shared Local', base_url: 'https://custom.local/v1', api_key: 'custom-b', model: 'local-b' },
|
||||
],
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('refreshes providers from all profiles and deduplicates identical catalogs', async () => {
|
||||
const { refreshConfiguredProviderModelCatalogs, providerModelCatalogKey } = await import(
|
||||
'../../packages/server/src/services/hermes/model-catalog-cache'
|
||||
)
|
||||
|
||||
await refreshConfiguredProviderModelCatalogs({ force: true })
|
||||
|
||||
expect(mockFetchProviderModels).toHaveBeenCalledTimes(3)
|
||||
expect(mockFetchProviderModels).toHaveBeenCalledWith('https://openrouter.ai/api/v1', 'default-openrouter', true)
|
||||
expect(mockFetchProviderModels).toHaveBeenCalledWith('https://api.deepseek.com/v1', 'team-deepseek', false)
|
||||
expect(mockFetchProviderModels).toHaveBeenCalledWith('https://custom.local/v1', 'custom-a', false)
|
||||
|
||||
const cache = JSON.parse(cacheText)
|
||||
expect(cache.providers[providerModelCatalogKey('openrouter', 'https://openrouter.ai/api/v1', true)]).toMatchObject({
|
||||
provider: 'openrouter',
|
||||
models: ['openrouter/fallback'],
|
||||
profiles: ['default', 'team'],
|
||||
})
|
||||
expect(cache.providers[providerModelCatalogKey('deepseek', 'https://api.deepseek.com/v1')]).toMatchObject({
|
||||
provider: 'deepseek',
|
||||
models: ['deepseek-chat'],
|
||||
profiles: ['team'],
|
||||
})
|
||||
expect(cache.providers[providerModelCatalogKey('custom:shared-local', 'https://custom.local/v1')]).toMatchObject({
|
||||
provider: 'custom:shared-local',
|
||||
models: ['local-a', 'local-b'],
|
||||
profiles: ['default', 'team'],
|
||||
})
|
||||
})
|
||||
|
||||
it('adds authorized providers to the catalog cache and fetches live models for compatible auth providers', async () => {
|
||||
mockListProfileNamesFromDisk.mockReturnValue(['default'])
|
||||
mockReadAppConfig.mockResolvedValue({ copilotEnabled: true })
|
||||
mockResolveCopilotOAuthToken.mockResolvedValue('gho-copilot')
|
||||
mockGlobalFetch.mockImplementation(async (url: string) => {
|
||||
if (url === 'https://chatgpt.com/backend-api/codex/models?client_version=1.0.0') {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
models: [
|
||||
{ slug: 'hidden-codex', visibility: 'hidden', priority: 0 },
|
||||
{ slug: 'gpt-5.4-mini', priority: 20 },
|
||||
{ slug: 'gpt-5.5', priority: 10 },
|
||||
],
|
||||
}),
|
||||
}
|
||||
}
|
||||
if (url === 'https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota') {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
buckets: [
|
||||
{ modelId: 'gemini-3.1-pro-preview' },
|
||||
{ modelId: 'gemini-3-flash-preview' },
|
||||
],
|
||||
}),
|
||||
}
|
||||
}
|
||||
return { ok: false, status: 404, json: async () => ({}) }
|
||||
})
|
||||
mockFetchProviderModels.mockImplementation(async (baseUrl: string, apiKey: string) => {
|
||||
if (baseUrl === 'https://inference-api.nousresearch.com/v1' && apiKey === 'nous-agent-key') {
|
||||
return ['nous/live-a', 'nous/live-b']
|
||||
}
|
||||
if (baseUrl === 'https://api.x.ai/v1' && apiKey === 'xai-access-token') {
|
||||
return ['grok-live-a', 'grok-live-b']
|
||||
}
|
||||
return []
|
||||
})
|
||||
mockReadFile.mockImplementation(async (path: string) => {
|
||||
if (path === '/hermes/default/.env') return ''
|
||||
if (path === '/hermes/default/auth.json') {
|
||||
return JSON.stringify({
|
||||
providers: {
|
||||
'openai-codex': { tokens: { access_token: 'codex-token' } },
|
||||
'google-gemini-cli': {
|
||||
access_token: 'gemini-access-token',
|
||||
refresh_token: 'gemini-refresh-token',
|
||||
base_url: 'cloudcode-pa://google',
|
||||
},
|
||||
'claude-oauth': {
|
||||
tokens: {
|
||||
access_token: 'claude-access-token',
|
||||
refresh_token: 'claude-refresh-token',
|
||||
},
|
||||
base_url: 'https://api.anthropic.com',
|
||||
},
|
||||
},
|
||||
credential_pool: {
|
||||
'xai-oauth': [{ access_token: 'xai-access-token' }],
|
||||
nous: [{
|
||||
agent_key: 'nous-agent-key',
|
||||
inference_base_url: 'https://inference-api.nousresearch.com/v1',
|
||||
}],
|
||||
},
|
||||
})
|
||||
}
|
||||
throw Object.assign(new Error('missing'), { code: 'ENOENT' })
|
||||
})
|
||||
mockReadConfigYamlForProfile.mockResolvedValue({})
|
||||
|
||||
const { refreshConfiguredProviderModelCatalogs, providerModelCatalogKey } = await import(
|
||||
'../../packages/server/src/services/hermes/model-catalog-cache'
|
||||
)
|
||||
|
||||
await refreshConfiguredProviderModelCatalogs({ force: true })
|
||||
|
||||
expect(mockFetchProviderModels).toHaveBeenCalledTimes(2)
|
||||
expect(mockFetchProviderModels).toHaveBeenCalledWith('https://inference-api.nousresearch.com/v1', 'nous-agent-key', false)
|
||||
expect(mockFetchProviderModels).toHaveBeenCalledWith('https://api.x.ai/v1', 'xai-access-token', false)
|
||||
const cache = JSON.parse(cacheText)
|
||||
expect(cache.providers[providerModelCatalogKey('openai-codex', 'https://chatgpt.com/backend-api/codex')]).toMatchObject({
|
||||
provider: 'openai-codex',
|
||||
models: ['gpt-5.5', 'gpt-5.4-mini'],
|
||||
source: 'live',
|
||||
profiles: ['default'],
|
||||
})
|
||||
expect(cache.providers[providerModelCatalogKey('xai-oauth', 'https://api.x.ai/v1')]).toMatchObject({
|
||||
provider: 'xai-oauth',
|
||||
models: ['grok-live-a', 'grok-live-b'],
|
||||
source: 'live',
|
||||
profiles: ['default'],
|
||||
})
|
||||
expect(cache.providers[providerModelCatalogKey('copilot', 'https://api.githubcopilot.com')]).toMatchObject({
|
||||
provider: 'copilot',
|
||||
models: ['gpt-5.5', 'claude-sonnet-4.6'],
|
||||
source: 'fallback',
|
||||
profiles: ['default'],
|
||||
})
|
||||
expect(cache.providers[providerModelCatalogKey('nous', 'https://inference-api.nousresearch.com/v1')]).toMatchObject({
|
||||
provider: 'nous',
|
||||
models: ['nous/live-a', 'nous/live-b'],
|
||||
source: 'live',
|
||||
profiles: ['default'],
|
||||
})
|
||||
expect(cache.providers[providerModelCatalogKey('google-gemini-cli', 'cloudcode-pa://google')]).toMatchObject({
|
||||
provider: 'google-gemini-cli',
|
||||
models: ['gemini-3.1-pro-preview', 'gemini-3-flash-preview'],
|
||||
source: 'live',
|
||||
profiles: ['default'],
|
||||
})
|
||||
expect(cache.providers[providerModelCatalogKey('claude-oauth', 'https://api.anthropic.com')]).toMatchObject({
|
||||
provider: 'claude-oauth',
|
||||
models: ['claude-sonnet-4-6', 'claude-opus-4-7'],
|
||||
source: 'fallback',
|
||||
profiles: ['default'],
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user