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
38 changes: 38 additions & 0 deletions chainforge/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,44 @@ def proxy_image():
except Exception as e:
return jsonify({"error": f"Error fetching image: {str(e)}"}), 500

@app.route('/api/getModelsDotDev', methods=['GET'])
def get_models_dot_dev():
"""
Called at Initialization/Creation of a Prompt Node

Fetches the list of models from the models.dev API.
Returns a JSON response with the model names and their details.
and save the json file to the local disk in the FLOWS_DIR.
"""
MODELS_DOT_DEV_FILE = os.path.join(FLOWS_DIR, "models_dot_dev.json")

# If the models.dev file already exists, return its content
if os.path.isfile(MODELS_DOT_DEV_FILE):
try:
with open(MODELS_DOT_DEV_FILE, 'r', encoding='utf-8') as f:
models_data = json.load(f)
print(f"Returning cached models from {MODELS_DOT_DEV_FILE}")
return jsonify(models_data)
except Exception as e:
return jsonify({"error": f"Error reading cached models file: {str(e)}"}), 500

# If the file does not exist, fetch it from the API
try:
# Fetch the models from the API
response = py_requests.get("https://models.dev/api.json")

Copilot AI Aug 15, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTTP request lacks a timeout parameter, which could cause the application to hang indefinitely if the external API is unresponsive. Consider adding a timeout parameter like py_requests.get("https://models.dev/api.json", timeout=30).

Suggested change
response = py_requests.get("https://models.dev/api.json")
response = py_requests.get("https://models.dev/api.json", timeout=30)

Copilot uses AI. Check for mistakes.
if response.status_code != 200:
return jsonify({"error": f"Failed to fetch models: {response.status_code} {response.reason}"}), response.status_code

# Parse the JSON response
models_data = response.json()

with open(MODELS_DOT_DEV_FILE, 'w', encoding='utf-8') as f:
json.dump(models_data, f, indent=2, ensure_ascii=False)

return jsonify(models_data)

except Exception as e:
return jsonify({"error": f"Error fetching models: {str(e)}"}), 500

"""
SPIN UP SERVER
Expand Down
49 changes: 49 additions & 0 deletions chainforge/react-server/src/GlobalSettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
Dict,
JSONCompatible,
LLMSpec,
ModelOllama,
} from "./backend/typing";
import {
getGlobalConfig,
Expand Down Expand Up @@ -302,10 +303,54 @@ const GlobalSettingsModal = forwardRef<GlobalSettingsModalRef, object>(

console.log("Ollama models available:", models_available);
console.log("Loaded Ollama model list from backend.");

// Populate model informations for Ollama models
// This is used to display the model information in the UI Prompt Node
const modelsInfos: Record<string, ModelOllama> = {};
data.models.forEach((model_obj: Dict) => {
modelsInfos[model_obj.name] = {
name: model_obj.name,
format: model_obj.details.format,
families: model_obj.details.families,
parameter_size: model_obj.details.parameter_size,
quantization_level: model_obj.details.quantization_level,
size: (model_obj.size / 1000 ** 3).toFixed(2) + " GB",
};

fetch(
new Request(`${Ollama_BaseURL}/api/show`, {
method: "POST",
body: '{"model": "' + model_obj.name + '"}',
}),
)
.then((response) => {
if (!response.ok) {
throw new Error(
`Failed to fetch model details on ${Ollama_BaseURL}/api/show`,
);
}
return response.json();
})
.then((modelDetails: Dict) => {
modelsInfos[model_obj.name].capabilities =
modelDetails.capabilities;
});
});
LLMsProvidersInfos.ollama = {
id: "ollama",
name: "Ollama",
env: [],
npm: "",
doc: "https://ollama.com/search",
models: modelsInfos,
};
setLLMsProvidersInfos(LLMsProvidersInfos);
})
.catch((error) => {
console.error("Error trying to fetch Ollama models", error);
});

fetch(`${Ollama_BaseURL}/api/`);

Copilot AI Aug 15, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fetch call appears to serve no purpose - it has no error handling, doesn't use the response, and doesn't have any side effects. This looks like leftover debugging code that should be removed.

Suggested change
fetch(`${Ollama_BaseURL}/api/`);

Copilot uses AI. Check for mistakes.
});
}, [form, settings]);

Expand Down Expand Up @@ -362,6 +407,10 @@ const GlobalSettingsModal = forwardRef<GlobalSettingsModalRef, object>(
const setFavorites = useStore((state) => state.setFavorites);
const nodes = useStore((state) => state.nodes);
const setDataPropsForNode = useStore((state) => state.setDataPropsForNode);
const LLMsProvidersInfos = useStore((state) => state.LLMsProvidersInfos);
const setLLMsProvidersInfos = useStore(
(state) => state.setLLMsProvidersInfos,
);

const showAlert = useContext(AlertModalContext);

Expand Down
16 changes: 14 additions & 2 deletions chainforge/react-server/src/LLMListComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import { getDefaultModelSettings } from "./ModelSettingSchemas";
import useStore, { initLLMProviders, initLLMProviderMenu } from "./store";
import { Dict, JSONCompatible, LLMGroup, LLMSpec } from "./backend/typing";
import { ContextMenuItemOptions } from "mantine-contextmenu/dist/types";
import { deepcopy, ensureUniqueName } from "./backend/utils";
import {
deepcopy,
ensureUniqueName,
getModelsDotDevInfos,
} from "./backend/utils";
import NestedMenu, { NestedMenuItemProps } from "./NestedMenu";

// The LLM(s) to include by default on a PromptNode whenever one is created.
Expand Down Expand Up @@ -299,6 +303,9 @@ export const LLMListContainer = forwardRef<
forceUpdate();
};

// Get the LLMs infos from the store, which is fetched from `models.dev` website.
const LLMsInfos = useStore((state) => state.LLMsProvidersInfos);

// Selecting LLM models to prompt
const [llmItems, setLLMItems] = useState(
initLLMItems ||
Expand Down Expand Up @@ -453,7 +460,7 @@ export const LLMListContainer = forwardRef<
};
} else {
initModels.add(item.base_model);
return {
const res: NestedMenuItemProps = {
key: item.key ?? item.model,
title: `${item.emoji} ${item.name}`,
onClick: () => handleSelectModel(item),
Expand All @@ -466,6 +473,11 @@ export const LLMListContainer = forwardRef<
}
: undefined,
};
// if `LLMsInfos` is not empty dict, add tooltip
if (LLMsInfos && Object.keys(LLMsInfos).length > 0) {
res.tooltip = getModelsDotDevInfos(item, LLMsInfos);
}
return res;
}
};
const res = initLLMProviderMenu.map((i) => convert(i));
Expand Down
5 changes: 3 additions & 2 deletions chainforge/react-server/src/NestedMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { ReactNode, useMemo, useState } from "react";
import { Menu, Tooltip, Popover, ActionIcon } from "@mantine/core";
import { IconChevronRight, IconTrash } from "@tabler/icons-react";
import { ContextMenuItemOptions } from "mantine-contextmenu/dist/types";
import ReactMarkdown from "react-markdown";

const NESTED_MENU_STYLE = {
dropdown: { padding: "0px !important" },
Expand All @@ -19,9 +20,9 @@ export const MenuTooltip = ({
else
return (
<Tooltip
label={label}
label={<ReactMarkdown>{label}</ReactMarkdown>}
position="right"
width={200}
width={400}
multiline
withArrow
arrowSize={10}
Expand Down
23 changes: 23 additions & 0 deletions chainforge/react-server/src/PromptNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {
truncStr,
genDebounceFunc,
ensureUniqueName,
FLASK_BASE_URL,
} from "./backend/utils";
import LLMResponseInspectorDrawer from "./LLMResponseInspectorDrawer";
import CancelTracker from "./backend/canceler";
Expand Down Expand Up @@ -370,6 +371,10 @@ const PromptNode: React.FC<PromptNodeProps> = ({
// API Keys (set by user in popup GlobalSettingsModal)
const apiKeys = useStore((state) => state.apiKeys);

const LLMsProvidersInfos = useStore((state) => state.LLMsProvidersInfos);
const setLLMsProvidersInfos = useStore(
(state) => state.setLLMsProvidersInfos,
);
const [jsonResponses, setJSONResponses] = useState<LLMResponse[] | null>(
null,
);
Expand Down Expand Up @@ -578,6 +583,24 @@ const PromptNode: React.FC<PromptNodeProps> = ({

// On initialization
useEffect(() => {
// Fetch models from the models.dev API
fetch(`${FLASK_BASE_URL}api/getModelsDotDev`)
.then((res) => {
return res.json();
})
.then((data) => {
console.log("Fetched models from models.dev:", data);
if (data && !data.error) {
setLLMsProvidersInfos({
...data,
...LLMsProvidersInfos,
});
}
})
.catch((err) => {
console.error(err);
});

refreshTemplateHooks(promptText);

// Attempt to grab cache'd responses
Expand Down
67 changes: 67 additions & 0 deletions chainforge/react-server/src/backend/typing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,70 @@ export type RatingDict = Record<number, boolean | string | null | undefined>;
export interface FileWithContent extends FileWithPath {
content?: string;
}

// Typing for the LLM provider dictionnary fecthed from the models.dev website
// that is stored in the `LLMsInfos` variable in the Zustand store

export type Modality = "text" | "image" | "audio" | "video" | "pdf";

export interface Modalities {
input: Modality[];
output: Modality[];
}

export interface Cost {
input: number;
output: number;
cache_read?: number;
cache_write?: number;
}

export interface Limit {
context: number;
output: number;
}

export interface Model {
id: string;
name: string;
attachment: boolean;
reasoning: boolean;
temperature: boolean;
tool_call: boolean;
knowledge?: string;
release_date: string;
last_updated: string;
modalities: Modalities;
open_weights: boolean;
cost?: Cost;
limit: Limit;
}

export type CapabilityOllama =
| "completion"
| "tools"
| "thinking"
| "embedding"
| "vision";

export interface ModelOllama {
name: string;
format: string;
families: string[];
parameter_size: string;
quantization_level: string;
size: string;
capabilities?: CapabilityOllama[];
}

export interface LLMProvider {
id: string;
env: string[];
npm: string;
name: string;
doc: string;
api?: string;
models: Record<string, Model | ModelOllama>;
}

export type ModelDotDevInfos = Record<string, LLMProvider>;
Loading
Loading