Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b834b3b1fd | ||
|
|
85423e6600 | ||
|
|
033d1ac2a3 | ||
|
|
6c23940081 | ||
|
|
2aefda956a | ||
|
|
cb8d164e71 | ||
|
|
6b8d59999e | ||
|
|
3a49fdade5 | ||
|
|
de0c0d6b3f | ||
|
|
b6dca3296c |
@@ -8,17 +8,19 @@ on:
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
# if this is a forked repository. Skipping the workflow.
|
||||
if: github.event.repository.fork == false
|
||||
if: ${{ github.repository_owner == 'asaddi' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
# Yet Another LLM Node (for ComfyUI)
|
||||
|
||||
Yet another set of LLM nodes for [ComfyUI](https://github.com/comfyanonymous/ComfyUI). This one only supports OpenAI-like APIs, but of course can be used with local LLM providers such as [llama.cpp](https://github.com/ggerganov/llama.cpp) (and ollama, LM Studio, among many others).
|
||||
@@ -69,6 +71,10 @@ The input is optional, meaning you can mute the node leading into it or even dis
|
||||
|
||||
The node will output whatever is currently being displayed in the textbox. The node text will also be saved in exported workflows and generated images.
|
||||
|
||||
## "Prepend/Append" Node
|
||||
|
||||
A simple utility node that takes a text input and optionally adds a prefix and/or suffix to it. This is useful for dynamically injecting instructions or context into prompts without altering the source text, as demonstrated in the `prompt-optimizer` example workflow.
|
||||
|
||||
## My Related Projects
|
||||
|
||||
* https://github.com/asaddi/YALLM-LlamaVision Basic ComfyUI node for Llama 3.2 Vision. Sampler/model nodes are cross-compatible with this project.
|
||||
|
||||
@@ -6,6 +6,16 @@ models:
|
||||
api_key: none
|
||||
model: whatever
|
||||
|
||||
- name: "[LOCAL] LM Studio"
|
||||
base_url: http://localhost:1234/v1
|
||||
api_key: none
|
||||
model: changeme
|
||||
|
||||
- name: "[LOCAL] Ollama"
|
||||
base_url: http://localhost:11434/v1
|
||||
api_key: none
|
||||
model: changeme
|
||||
|
||||
# Example of using a remote endpoint
|
||||
# If prefixed with "os.environ/" (similar to LiteLLM), base_url and/or api_key
|
||||
# will take their value from the named environment variable.
|
||||
|
||||
@@ -6,9 +6,9 @@ from typing import Any, Optional
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web_request import Request
|
||||
import openai
|
||||
from PIL.Image import Image
|
||||
from pydantic import BaseModel
|
||||
import requests
|
||||
import torchvision.transforms.functional as F
|
||||
import yaml
|
||||
|
||||
@@ -347,20 +347,19 @@ SamplerSetting = tuple[str, Any]
|
||||
|
||||
class LLMModel:
|
||||
def __init__(self, prov_def: ProviderDefinition, model: str | None = None):
|
||||
self._llm = openai.OpenAI(
|
||||
base_url=prov_def.resolve_base_url(),
|
||||
api_key=(
|
||||
prov_def.resolve_api_key() or "none"
|
||||
), # openai package requires something for API key
|
||||
)
|
||||
self._base_url = prov_def.resolve_base_url()
|
||||
self._headers = {}
|
||||
if (api_key := prov_def.resolve_api_key()) and api_key != "none":
|
||||
self._headers["authorization"] = f"Bearer {api_key}"
|
||||
if model is None:
|
||||
self._model = getattr(prov_def, "model", None)
|
||||
else:
|
||||
self._model = model
|
||||
|
||||
def get_models(self):
|
||||
models = list(self._llm.models.list())
|
||||
return [m.id for m in models]
|
||||
with requests.get(self._base_url + "/models", headers=self._headers) as r:
|
||||
models_json = r.json()
|
||||
return [m["id"] for m in models_json["data"]]
|
||||
|
||||
def chat_completion(
|
||||
self,
|
||||
@@ -404,34 +403,35 @@ class LLMModel:
|
||||
|
||||
sampler_order = []
|
||||
extra_args = {}
|
||||
extra_body = {}
|
||||
for k, v in samplers:
|
||||
sampler_order.append(k)
|
||||
if k in (
|
||||
"temperature",
|
||||
"top_p",
|
||||
): # The only ones supported directly by openai package
|
||||
extra_args[k] = v
|
||||
else:
|
||||
# The rest have to go into "extra_body"
|
||||
extra_body[k] = v
|
||||
extra_body["samplers"] = (
|
||||
extra_args[k] = v
|
||||
extra_args["samplers"] = (
|
||||
sampler_order # Hopefully won't cause an issue for non-llama.cpp endpoints
|
||||
)
|
||||
|
||||
if seed is not None:
|
||||
extra_args["seed"] = seed
|
||||
|
||||
# print(f'extra_args = {repr(extra_args)}')
|
||||
# print(f'extra_body = {repr(extra_body)}')
|
||||
# print(f"extra_args = {repr(extra_args)}")
|
||||
|
||||
output = self._llm.chat.completions.create(
|
||||
messages=messages, model=self._model, extra_body=extra_body, **extra_args
|
||||
)
|
||||
req_json = {
|
||||
"model": self._model,
|
||||
"messages": messages,
|
||||
"stream": False,
|
||||
}
|
||||
req_json.update(extra_args)
|
||||
|
||||
# TODO Since we're not streaming, we'll need some sort of timeout. How to specify that?
|
||||
# TODO Since we're not streaming, we'll need some sort of timeout. TODO specify that
|
||||
with requests.post(
|
||||
self._base_url + "/chat/completions", json=req_json, headers=self._headers
|
||||
) as r:
|
||||
output = r.json()
|
||||
|
||||
return output.choices[0].message.content
|
||||
if choices := output.get("choices", []):
|
||||
return choices[0]["message"]["content"]
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
class LLMProvider:
|
||||
|
||||
@@ -5,6 +5,14 @@ providers:
|
||||
base_url: http://localhost:8080/v1
|
||||
api_key: none
|
||||
|
||||
- name: "[LOCAL] LM Studio"
|
||||
base_url: http://localhost:1234/v1
|
||||
api_key: none
|
||||
|
||||
- name: "[LOCAL] Ollama"
|
||||
base_url: http://localhost:11434/v1
|
||||
api_key: none
|
||||
|
||||
# Example of using a remote endpoint
|
||||
# If prefixed with "os.environ/" (similar to LiteLLM), base_url and/or api_key
|
||||
# will take their value from the named environment variable.
|
||||
|
||||
+6
-4
@@ -1,12 +1,14 @@
|
||||
[project]
|
||||
name = "comfyui-yallm-node"
|
||||
description = "Yet another set of LLM nodes for ComfyUI (for local/remote OpenAI-like APIs, multi-modal models supported)"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
license = {file = "LICENSE.txt"}
|
||||
dependencies = [
|
||||
"openai",
|
||||
"pillow",
|
||||
"pydantic",
|
||||
"pyyaml"
|
||||
"pyyaml",
|
||||
"requests",
|
||||
"torchvision"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -19,7 +21,7 @@ DisplayName = "ComfyUI-YALLM-node"
|
||||
Icon = ""
|
||||
|
||||
[tool.bumpversion]
|
||||
current_version = "1.4.0"
|
||||
current_version = "1.5.0"
|
||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
||||
serialize = ["{major}.{minor}.{patch}"]
|
||||
search = "{current_version}"
|
||||
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
openai
|
||||
pydantic
|
||||
|
||||
# These should already be installed by ComfyUI
|
||||
pillow
|
||||
pydantic
|
||||
pyyaml
|
||||
requests
|
||||
torchvision
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Copyright (c) 2024 Allan Saddi <allan@saddi.com>
|
||||
class PromptUtils:
|
||||
def __init__(self, prompt):
|
||||
self.prompt = prompt
|
||||
@@ -68,7 +69,7 @@ class WorkflowUtils:
|
||||
and (workflow := self.extra_pnginfo.get("workflow")) is not None
|
||||
and (nodes := workflow.get("nodes")) is not None
|
||||
):
|
||||
node_id = int(node_id)
|
||||
node_id = str(node_id)
|
||||
# TODO For the time being, we aren't caching anything.
|
||||
# Concievably, if there are a large number of nodes, a lot of time
|
||||
# could be wasted searching.
|
||||
@@ -87,3 +88,18 @@ class WorkflowUtils:
|
||||
# TODO When would there be a mismatch?
|
||||
if slot < len(values):
|
||||
values[slot] = value
|
||||
|
||||
|
||||
class AnyType(str):
|
||||
def __ne__(self, other):
|
||||
if self == "*":
|
||||
# If we're the wildcard, match anything
|
||||
return False
|
||||
return super().__ne__(other)
|
||||
|
||||
|
||||
class ComboType(str):
|
||||
def __ne__(self, other):
|
||||
if self == "*" and isinstance(other, list):
|
||||
return False
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user