mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 23:32:12 +03:00
fix(cli-tools): enable Apply for compatible providers (#9250)
* fix(cli-tools): resolve models for compatible providers Keep the CLI tools Apply flow usable when a dynamic OpenAI-compatible or Anthropic-compatible connection has no static catalog entry. Resolve its public prefix, connection default model, and prefix-backed catalog entries before gating the cards. Co-authored-by: lazysaltyfish <7127935+lazysaltyfish@users.noreply.github.com> Inspired-by: https://github.com/decolua/9router/pull/2995 * chore(changelog): fragment for #9250 --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: lazysaltyfish <7127935+lazysaltyfish@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
2cb77bbca7
commit
f11d883f22
1
changelog.d/fixes/9250-cli-compatible-provider-apply.md
Normal file
1
changelog.d/fixes/9250-cli-compatible-provider-apply.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(cli-tools):** keep Apply enabled for active OpenAI-compatible and Anthropic-compatible providers without static catalog entries. (thanks @lazysaltyfish)
|
||||
@@ -133,10 +133,55 @@ export default function ToolDetailClient({ toolId, category }: ToolDetailClientP
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (providerModels.length === 0) {
|
||||
const prefix =
|
||||
typeof conn.providerSpecificData?.prefix === "string" &&
|
||||
conn.providerSpecificData.prefix.trim()
|
||||
? conn.providerSpecificData.prefix.trim()
|
||||
: alias;
|
||||
const fallbackModels: Array<{ id: string; name: string }> = [];
|
||||
const addFallbackModel = (model: any) => {
|
||||
const id = typeof model?.id === "string" ? model.id.trim() : "";
|
||||
if (!id || fallbackModels.some((candidate) => candidate.id === id)) return;
|
||||
fallbackModels.push({
|
||||
id,
|
||||
name: typeof model?.name === "string" && model.name.trim() ? model.name.trim() : id,
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof conn.defaultModel === "string" && conn.defaultModel.trim()) {
|
||||
addFallbackModel({ id: conn.defaultModel });
|
||||
}
|
||||
if (Array.isArray(conn.providerSpecificData?.customModels)) {
|
||||
conn.providerSpecificData.customModels.forEach(addFallbackModel);
|
||||
}
|
||||
if (fallbackModels.length === 0 && conn.testStatus === "active") {
|
||||
addFallbackModel({ id: "model-id", name: `${prefix}/model-id` });
|
||||
}
|
||||
|
||||
fallbackModels.forEach((model) => {
|
||||
const modelValue = `${prefix}/${model.id}`;
|
||||
if (seenModels.has(modelValue)) return;
|
||||
seenModels.add(modelValue);
|
||||
models.push({
|
||||
value: modelValue,
|
||||
label: modelValue,
|
||||
provider: conn.provider,
|
||||
alias: prefix,
|
||||
connectionName: conn.name,
|
||||
modelId: model.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const activeAliases = new Set(
|
||||
activeProviders.map((c) => PROVIDER_ID_TO_ALIAS[c.provider] || c.provider)
|
||||
activeProviders.flatMap((connection) => {
|
||||
const alias = PROVIDER_ID_TO_ALIAS[connection.provider] || connection.provider;
|
||||
const prefix = connection.providerSpecificData?.prefix;
|
||||
return typeof prefix === "string" && prefix.trim() ? [alias, prefix.trim()] : [alias];
|
||||
})
|
||||
);
|
||||
const activeProviderIds = new Set(activeProviders.map((c) => c.provider));
|
||||
dynamicModels.forEach((dm) => {
|
||||
|
||||
@@ -106,7 +106,13 @@ vi.mock("@/shared/constants/models", () => ({
|
||||
|
||||
// Stub specialized cards — render a testid so we can identify which was rendered
|
||||
vi.mock("../../../src/app/(dashboard)/dashboard/cli-code/components/index", () => ({
|
||||
ClaudeToolCard: () => <div data-testid="ClaudeToolCard" />,
|
||||
ClaudeToolCard: ({ hasActiveProviders, availableModels }: any) => (
|
||||
<div
|
||||
data-testid="ClaudeToolCard"
|
||||
data-has-active-providers={String(hasActiveProviders)}
|
||||
data-available-models={JSON.stringify(availableModels)}
|
||||
/>
|
||||
),
|
||||
CodexToolCard: () => <div data-testid="CodexToolCard" />,
|
||||
DroidToolCard: () => <div data-testid="DroidToolCard" />,
|
||||
OpenClawToolCard: () => <div data-testid="OpenClawToolCard" />,
|
||||
@@ -127,9 +133,8 @@ vi.mock("../../../src/app/(dashboard)/dashboard/cli-code/components/CliproxyapiT
|
||||
|
||||
// ── Import after mocks ────────────────────────────────────────────────────────
|
||||
|
||||
const { default: ToolDetailClient } = await import(
|
||||
"@/app/(dashboard)/dashboard/cli-code/components/ToolDetailClient"
|
||||
);
|
||||
const { default: ToolDetailClient } =
|
||||
await import("@/app/(dashboard)/dashboard/cli-code/components/ToolDetailClient");
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -153,7 +158,10 @@ beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
mockFetch.mockClear();
|
||||
mockFetch.mockReset().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ connections: [], keys: [], data: [], cloudEnabled: false }),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -185,6 +193,93 @@ describe("ToolDetailClient", () => {
|
||||
expect(container.querySelector("[data-testid='CustomCliCard']")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("keeps Apply available for an active dynamic compatible provider", async () => {
|
||||
mockFetch.mockImplementation(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/providers") {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
connections: [
|
||||
{
|
||||
provider: "openai-compatible-chat-node-123",
|
||||
name: "Kimi gateway",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
defaultModel: "Kimi-K3",
|
||||
providerSpecificData: { prefix: "kimi-gateway" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ keys: [], data: [], cloudEnabled: false }),
|
||||
};
|
||||
});
|
||||
|
||||
const container = renderDetail("claude", "code");
|
||||
await act(async () => {});
|
||||
|
||||
const card = container.querySelector("[data-testid='ClaudeToolCard']");
|
||||
expect(card?.getAttribute("data-has-active-providers")).toBe("true");
|
||||
expect(JSON.parse(card?.getAttribute("data-available-models") || "[]")).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: "kimi-gateway/Kimi-K3",
|
||||
provider: "openai-compatible-chat-node-123",
|
||||
modelId: "Kimi-K3",
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts compatible-provider models published under the connection prefix", async () => {
|
||||
mockFetch.mockImplementation(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/providers") {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
connections: [
|
||||
{
|
||||
provider: "anthropic-compatible-node-456",
|
||||
name: "Claude gateway",
|
||||
isActive: true,
|
||||
providerSpecificData: { prefix: "claude-gateway" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (url === "/v1/models") {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ data: [{ id: "claude-gateway/claude-sonnet" }] }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ keys: [], cloudEnabled: false }),
|
||||
};
|
||||
});
|
||||
|
||||
const container = renderDetail("claude", "code");
|
||||
await act(async () => {});
|
||||
|
||||
const card = container.querySelector("[data-testid='ClaudeToolCard']");
|
||||
expect(card?.getAttribute("data-has-active-providers")).toBe("true");
|
||||
expect(JSON.parse(card?.getAttribute("data-available-models") || "[]")).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: "claude-gateway/claude-sonnet",
|
||||
modelId: "claude-sonnet",
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it("renders DefaultToolCard for unknown tool (forge, configType:custom)", async () => {
|
||||
const container = renderDetail("forge", "code");
|
||||
await act(async () => {});
|
||||
|
||||
Reference in New Issue
Block a user