add hermes agent provider

This commit is contained in:
2026-05-04 21:52:39 -07:00
parent 195e157e1a
commit 8b580fd3e1
27 changed files with 359 additions and 83 deletions

View File

@@ -95,6 +95,7 @@ const PROVIDER_FALLBACK_MODELS: Record<Provider, string[]> = {
openai: ["gpt-4.1-mini"],
anthropic: ["claude-3-5-sonnet-latest"],
xai: ["grok-3-mini"],
"hermes-agent": ["hermes-agent"],
};
const EMPTY_MODEL_CATALOG: ModelCatalogResponse["providers"] = {
@@ -103,6 +104,9 @@ const EMPTY_MODEL_CATALOG: ModelCatalogResponse["providers"] = {
xai: { models: [], loadedAt: null, error: null },
};
const BASE_PROVIDERS: Provider[] = ["openai", "anthropic", "xai"];
const ALL_PROVIDERS: Provider[] = [...BASE_PROVIDERS, "hermes-agent"];
const MODEL_PREFERENCES_STORAGE_KEY = "sybil:modelPreferencesByProvider";
const QUICK_QUESTION_MODEL_SELECTION_STORAGE_KEY = "sybil:quickQuestionModelSelection";
@@ -117,6 +121,7 @@ const EMPTY_MODEL_PREFERENCES: ProviderModelPreferences = {
openai: null,
anthropic: null,
xai: null,
"hermes-agent": null,
};
const EMPTY_ACTIVE_RUNS: ActiveRunsState = {
chats: {},
@@ -193,6 +198,10 @@ function getModelOptions(catalog: ModelCatalogResponse["providers"], provider: P
return PROVIDER_FALLBACK_MODELS[provider];
}
function getVisibleProviders(catalog: ModelCatalogResponse["providers"]) {
return ALL_PROVIDERS.filter((provider) => provider !== "hermes-agent" || catalog[provider] !== undefined);
}
function getReplyScrollBufferHeight() {
if (typeof window === "undefined") return REPLY_SCROLL_BUFFER_MIN;
return Math.min(
@@ -308,6 +317,8 @@ function loadStoredModelPreferences() {
openai: typeof parsed.openai === "string" && parsed.openai.trim() ? parsed.openai.trim() : null,
anthropic: typeof parsed.anthropic === "string" && parsed.anthropic.trim() ? parsed.anthropic.trim() : null,
xai: typeof parsed.xai === "string" && parsed.xai.trim() ? parsed.xai.trim() : null,
"hermes-agent":
typeof parsed["hermes-agent"] === "string" && parsed["hermes-agent"].trim() ? parsed["hermes-agent"].trim() : null,
};
} catch {
return EMPTY_MODEL_PREFERENCES;
@@ -315,17 +326,19 @@ function loadStoredModelPreferences() {
}
function normalizeStoredProvider(value: unknown): Provider {
return value === "anthropic" || value === "xai" || value === "openai" ? value : "openai";
return value === "anthropic" || value === "xai" || value === "openai" || value === "hermes-agent" ? value : "openai";
}
function normalizeStoredModelPreferences(value: unknown): ProviderModelPreferences {
if (!value || typeof value !== "object" || Array.isArray(value)) return EMPTY_MODEL_PREFERENCES;
const parsed = value as Partial<Record<Provider, unknown>>;
return {
openai: typeof parsed.openai === "string" && parsed.openai.trim() ? parsed.openai.trim() : null,
anthropic: typeof parsed.anthropic === "string" && parsed.anthropic.trim() ? parsed.anthropic.trim() : null,
xai: typeof parsed.xai === "string" && parsed.xai.trim() ? parsed.xai.trim() : null,
};
return {
openai: typeof parsed.openai === "string" && parsed.openai.trim() ? parsed.openai.trim() : null,
anthropic: typeof parsed.anthropic === "string" && parsed.anthropic.trim() ? parsed.anthropic.trim() : null,
xai: typeof parsed.xai === "string" && parsed.xai.trim() ? parsed.xai.trim() : null,
"hermes-agent":
typeof parsed["hermes-agent"] === "string" && parsed["hermes-agent"].trim() ? parsed["hermes-agent"].trim() : null,
};
}
function loadStoredQuickQuestionModelSelection(): QuickQuestionModelSelection {
@@ -354,6 +367,7 @@ function getProviderLabel(provider: Provider | null | undefined) {
if (provider === "openai") return "OpenAI";
if (provider === "anthropic") return "Anthropic";
if (provider === "xai") return "xAI";
if (provider === "hermes-agent") return "Hermes Agent";
return "";
}
@@ -963,6 +977,7 @@ export default function App() {
const providerModelOptions = useMemo(() => getModelOptions(modelCatalog, provider), [modelCatalog, provider]);
const quickProviderModelOptions = useMemo(() => getModelOptions(modelCatalog, quickProvider), [modelCatalog, quickProvider]);
const providerOptions = useMemo(() => getVisibleProviders(modelCatalog), [modelCatalog]);
useEffect(() => {
if (model.trim()) return;
@@ -2512,9 +2527,11 @@ export default function App() {
}}
disabled={isActiveSelectionSending}
>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="xai">xAI</option>
{providerOptions.map((candidate) => (
<option key={candidate} value={candidate}>
{getProviderLabel(candidate)}
</option>
))}
</select>
<ModelCombobox
options={providerModelOptions}
@@ -2758,9 +2775,11 @@ export default function App() {
disabled={isQuickQuestionSending || isConvertingQuickQuestion}
aria-label="Quick question provider"
>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="xai">xAI</option>
{providerOptions.map((candidate) => (
<option key={candidate} value={candidate}>
{getProviderLabel(candidate)}
</option>
))}
</select>
<ModelCombobox
options={quickProviderModelOptions}

View File

@@ -127,7 +127,7 @@ export type CompletionRequestMessage = {
attachments?: ChatAttachment[];
};
export type Provider = "openai" | "anthropic" | "xai";
export type Provider = "openai" | "anthropic" | "xai" | "hermes-agent";
export type ProviderModelInfo = {
models: string[];
@@ -136,7 +136,7 @@ export type ProviderModelInfo = {
};
export type ModelCatalogResponse = {
providers: Record<Provider, ProviderModelInfo>;
providers: Partial<Record<Provider, ProviderModelInfo>>;
};
export type ActiveRunsResponse = {