Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/llm/autodetect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const PROVIDER_HANDLES_TEMPLATING: string[] = [
"xAI",
"minimax",
"groq",
"rodiumai",
"gemini",
"docker",
"nous",
Expand Down Expand Up @@ -123,6 +124,7 @@ const PROVIDER_SUPPORTS_IMAGES: string[] = [
"sagemaker",
"openrouter",
"clawrouter",
"rodiumai",
"venice",
"sambanova",
"vertexai",
Expand Down
3 changes: 3 additions & 0 deletions core/llm/fetchModels.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fetchRodiumAiModels } from "./fetchRodiumAiModels.js";
import { LLMClasses, llmFromProviderAndOptions } from "./llms/index.js";

export interface FetchedModel {
Expand Down Expand Up @@ -248,6 +249,8 @@ export async function fetchModels(
return fetchOllamaModels();
case "openrouter":
return fetchOpenRouterModels();
case "rodiumai":
return fetchRodiumAiModels(apiKey, apiBase);
case "anthropic":
return fetchAnthropicModels(apiKey);
case "gemini":
Expand Down
99 changes: 99 additions & 0 deletions core/llm/fetchRodiumAiModels.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
fetchRodiumAiModels,
getRodiumAiModelIcon,
} from "./fetchRodiumAiModels.js";

describe("fetchRodiumAiModels", () => {
const originalFetch = global.fetch;

afterEach(() => {
global.fetch = originalFetch;
jest.restoreAllMocks();
});

test("getRodiumAiModelIcon maps provider slugs and model ids", () => {
expect(getRodiumAiModelIcon("anthropic/claude-fable-5", "anthropic")).toBe(
"anthropic.png",
);
expect(getRodiumAiModelIcon("openai/gpt-5.4", "openai")).toBe("openai.png");
expect(getRodiumAiModelIcon("custom/unknown-model")).toBe("rodium.svg");
});

test("maps RodiumAi model extensions into FetchedModel", async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: async () => ({
object: "list",
data: [
{
id: "anthropic/claude-fable-5",
rodiumai_display_name: "Claude Fable 5",
rodiumai_description: "Anthropic creative model",
rodiumai_provider: { slug: "anthropic", name: "Anthropic" },
rodiumai_capabilities: {
context_window: 200000,
max_output_tokens: 8192,
supports_tools: true,
},
},
{
id: "openai/gpt-5.4",
rodiumai_display_name: "GPT-5.4",
rodiumai_provider: { slug: "openai", name: "OpenAI" },
rodiumai_capabilities: {
context_window: 1050000,
max_output_tokens: 128000,
supports_tools: true,
},
},
],
}),
}) as typeof fetch;

const models = await fetchRodiumAiModels("rd_sk_prod_test");

expect(global.fetch).toHaveBeenCalledWith(
expect.objectContaining({
href: "https://api.rodiumai.io/v1/models",
}),
expect.objectContaining({
headers: { Authorization: "Bearer rd_sk_prod_test" },
}),
);

expect(models).toHaveLength(2);
expect(models[0]).toEqual({
name: "Claude Fable 5",
modelId: "anthropic/claude-fable-5",
description: "Anthropic creative model",
icon: "anthropic.png",
contextLength: 200000,
maxTokens: 8192,
supportsTools: true,
});
expect(models[1]).toMatchObject({
name: "GPT-5.4",
modelId: "openai/gpt-5.4",
icon: "openai.png",
contextLength: 1050000,
maxTokens: 128000,
supportsTools: true,
});
});

test("returns an empty list when the RodiumAi API fails", async () => {
const consoleError = jest
.spyOn(console, "error")
.mockImplementation(() => {});

global.fetch = jest.fn().mockResolvedValue({
ok: false,
status: 503,
}) as typeof fetch;

const models = await fetchRodiumAiModels("rd_sk_prod_test");

expect(models).toEqual([]);
expect(consoleError).toHaveBeenCalled();
});
});
96 changes: 96 additions & 0 deletions core/llm/fetchRodiumAiModels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const RODIUMAI_DEFAULT_API_BASE = "https://api.rodiumai.io/v1/";

interface RodiumAiFetchedModel {
name: string;
modelId?: string;
description?: string;
icon?: string;
contextLength?: number;
maxTokens?: number;
supportsTools?: boolean;
}

const RODIUMAI_PROVIDER_ICON_MAP: Record<string, string> = {
openai: "openai.png",
anthropic: "anthropic.png",
google: "gemini.png",
gemini: "gemini.png",
deepseek: "deepseek.png",
mistral: "mistral.png",
meta: "meta.png",
moonshot: "moonshot.png",
xai: "xAI.png",
cohere: "cohere.png",
};

export function getRodiumAiModelIcon(
modelId: string,
providerSlug?: string,
): string {
if (providerSlug && RODIUMAI_PROVIDER_ICON_MAP[providerSlug]) {
return RODIUMAI_PROVIDER_ICON_MAP[providerSlug];
}

const lower = modelId.toLowerCase();
if (lower.includes("claude")) {
return "anthropic.png";
}
if (lower.includes("gpt") || lower.startsWith("openai/")) {
return "openai.png";
}
if (lower.includes("gemini") || lower.includes("gemma")) {
return "gemini.png";
}
if (lower.includes("deepseek")) {
return "deepseek.png";
}
if (lower.includes("mistral")) {
return "mistral.png";
}

return "rodium.svg";
}

export async function fetchRodiumAiModels(
apiKey?: string,
apiBase?: string,
): Promise<RodiumAiFetchedModel[]> {
try {
const base = apiBase || RODIUMAI_DEFAULT_API_BASE;
const url = new URL("models", base);
const headers: Record<string, string> = {};
if (apiKey) {
headers.Authorization = `Bearer ${apiKey}`;
}

const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(`Failed to fetch RodiumAi models: ${response.status}`);
}

const data = await response.json();
if (!data.data || !Array.isArray(data.data)) {
return [];
}

return data.data
.filter((m: any) => m.id)
.map((m: any) => {
const providerSlug: string | undefined = m.rodiumai_provider?.slug;
const capabilities = m.rodiumai_capabilities ?? {};

return {
name: m.rodiumai_display_name ?? m.id,
modelId: m.id,
description: m.rodiumai_description,
icon: getRodiumAiModelIcon(m.id, providerSlug),
contextLength: capabilities.context_window,
maxTokens: capabilities.max_output_tokens,
supportsTools: capabilities.supports_tools,
};
});
} catch (error) {
console.error("Error fetching RodiumAi models:", error);
return [];
}
}
Loading
Loading