mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
fix(cli): show OpenCode Free in Hermes Agent picker (#3240)
Show OpenCode Free in the Hermes Agent model picker via alwaysIncludeProviders. Integrated into release/v3.8.11. Thanks @wilsonicdev.
This commit is contained in:
@@ -23,6 +23,8 @@ const HERMES_ROLES: Role[] = [
|
||||
{ id: "approval", label: "Approval", description: "Safety and approval decisions" },
|
||||
];
|
||||
|
||||
const HERMES_AGENT_ZERO_CONFIG_PROVIDERS = ["opencode"];
|
||||
|
||||
export default function HermesAgentToolCard({
|
||||
tool,
|
||||
isExpanded = false,
|
||||
@@ -517,6 +519,7 @@ export default function HermesAgentToolCard({
|
||||
}}
|
||||
showCombos={true}
|
||||
activeProviders={activeProviders}
|
||||
alwaysIncludeProviders={HERMES_AGENT_ZERO_CONFIG_PROVIDERS}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -37,6 +37,7 @@ type ModelSelectModalProps = {
|
||||
addedModelValues?: string[];
|
||||
multiSelect?: boolean;
|
||||
showCombos?: boolean;
|
||||
alwaysIncludeProviders?: string[] | null;
|
||||
};
|
||||
|
||||
export default function ModelSelectModal({
|
||||
@@ -51,6 +52,7 @@ export default function ModelSelectModal({
|
||||
addedModelValues = [],
|
||||
multiSelect = false,
|
||||
showCombos = true,
|
||||
alwaysIncludeProviders = [],
|
||||
}: ModelSelectModalProps) {
|
||||
const t = useTranslations("common");
|
||||
const resolvedTitle = title ?? t("selectModel");
|
||||
@@ -111,6 +113,11 @@ export default function ModelSelectModal({
|
||||
() => ({ ...OAUTH_PROVIDERS, ...NOAUTH_PROVIDERS, ...APIKEY_PROVIDERS }),
|
||||
[]
|
||||
);
|
||||
const alwaysIncludeProvidersKey = Array.isArray(alwaysIncludeProviders)
|
||||
? alwaysIncludeProviders
|
||||
.filter((providerId) => typeof providerId === "string" && providerId)
|
||||
.join("\0")
|
||||
: "";
|
||||
|
||||
// Group models by provider with priority order
|
||||
const groupedModels = useMemo(() => {
|
||||
@@ -118,10 +125,14 @@ export default function ModelSelectModal({
|
||||
|
||||
// Get all active provider IDs from connections
|
||||
const activeConnectionIds = activeProviders.map((p) => p.provider);
|
||||
const explicitProviderIds = alwaysIncludeProvidersKey
|
||||
? alwaysIncludeProvidersKey.split("\0")
|
||||
: [];
|
||||
|
||||
// Only show connected providers (including both standard and custom)
|
||||
const providerIdsToShow = new Set([
|
||||
...activeConnectionIds, // Only connected providers
|
||||
...activeConnectionIds, // Connected providers
|
||||
...explicitProviderIds, // Zero-config providers required by specific clients
|
||||
]);
|
||||
|
||||
// Sort by PROVIDER_ORDER
|
||||
@@ -262,7 +273,14 @@ export default function ModelSelectModal({
|
||||
});
|
||||
|
||||
return groups;
|
||||
}, [activeProviders, modelAliases, allProviders, providerNodes, customModels]);
|
||||
}, [
|
||||
activeProviders,
|
||||
alwaysIncludeProvidersKey,
|
||||
modelAliases,
|
||||
allProviders,
|
||||
providerNodes,
|
||||
customModels,
|
||||
]);
|
||||
|
||||
// Filter combos by search query
|
||||
const filteredCombos = useMemo(() => {
|
||||
|
||||
76
tests/unit/ui/hermes-agent-opencode-free.test.tsx
Normal file
76
tests/unit/ui/hermes-agent-opencode-free.test.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
// @vitest-environment jsdom
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
let lastModelSelectProps: any = null;
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/components", () => ({
|
||||
Card: ({ children }: any) => <div>{children}</div>,
|
||||
Button: ({ children, onClick, disabled, ...props }: any) => (
|
||||
<button type="button" onClick={onClick} disabled={disabled} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
ModelSelectModal: (props: any) => {
|
||||
lastModelSelectProps = props;
|
||||
return <div data-testid="ModelSelectModal" />;
|
||||
},
|
||||
}));
|
||||
|
||||
const { default: HermesAgentToolCard } =
|
||||
await import("@/app/(dashboard)/dashboard/cli-code/components/HermesAgentToolCard");
|
||||
|
||||
const containers: HTMLElement[] = [];
|
||||
|
||||
function renderCard() {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
containers.push(container);
|
||||
|
||||
const root = createRoot(container);
|
||||
act(() => {
|
||||
root.render(
|
||||
<HermesAgentToolCard
|
||||
tool={{ name: "Hermes Agent", description: "Hermes Agent" }}
|
||||
isExpanded={false}
|
||||
baseUrl="http://localhost:3000"
|
||||
apiKeys={[{ id: "key-1" }]}
|
||||
activeProviders={[]}
|
||||
batchStatus={null}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
lastModelSelectProps = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (containers.length > 0) {
|
||||
containers.pop()?.remove();
|
||||
}
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
describe("HermesAgentToolCard", () => {
|
||||
it("keeps OpenCode Free available in the model picker even with no active connections", async () => {
|
||||
renderCard();
|
||||
await act(async () => {});
|
||||
|
||||
expect(lastModelSelectProps).toBeTruthy();
|
||||
expect(lastModelSelectProps.activeProviders).toEqual([]);
|
||||
expect(lastModelSelectProps.alwaysIncludeProviders).toContain("opencode");
|
||||
});
|
||||
});
|
||||
88
tests/unit/ui/model-select-modal-zero-config.test.tsx
Normal file
88
tests/unit/ui/model-select-modal-zero-config.test.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// @vitest-environment jsdom
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
const { default: ModelSelectModal } = await import("@/shared/components/ModelSelectModal");
|
||||
|
||||
const containers: HTMLElement[] = [];
|
||||
|
||||
async function renderModal(props: Partial<React.ComponentProps<typeof ModelSelectModal>> = {}) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
containers.push(container);
|
||||
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<ModelSelectModal
|
||||
isOpen={true}
|
||||
onClose={() => {}}
|
||||
onSelect={() => {}}
|
||||
showCombos={false}
|
||||
activeProviders={[]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {});
|
||||
return container;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string) => {
|
||||
if (url === "/api/provider-nodes") {
|
||||
return { ok: true, json: async () => ({ nodes: [] }) };
|
||||
}
|
||||
if (url === "/api/provider-models") {
|
||||
return { ok: true, json: async () => ({ models: {} }) };
|
||||
}
|
||||
if (url === "/api/combos") {
|
||||
return { ok: true, json: async () => ({ combos: [] }) };
|
||||
}
|
||||
return { ok: true, json: async () => ({}) };
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (containers.length > 0) {
|
||||
containers.pop()?.remove();
|
||||
}
|
||||
document.body.innerHTML = "";
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("ModelSelectModal zero-config providers", () => {
|
||||
it("shows OpenCode Free models when explicitly included without an active connection", async () => {
|
||||
const container = await renderModal({ alwaysIncludeProviders: ["opencode"] });
|
||||
|
||||
expect(container.textContent).toContain("OpenCode Free");
|
||||
expect(container.textContent).toContain("Big Pickle");
|
||||
});
|
||||
|
||||
it("does not show OpenCode Free by default without an active connection", async () => {
|
||||
const container = await renderModal();
|
||||
|
||||
expect(container.textContent).not.toContain("OpenCode Free");
|
||||
expect(container.textContent).not.toContain("Big Pickle");
|
||||
});
|
||||
|
||||
it("treats null explicit provider lists as empty", async () => {
|
||||
const container = await renderModal({ alwaysIncludeProviders: null });
|
||||
|
||||
expect(container.textContent).not.toContain("OpenCode Free");
|
||||
expect(container.textContent).not.toContain("Big Pickle");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user