mirror of
https://github.com/hanzoai/flow.git
synced 2026-10-17 01:29:18 +00:00
feat(hanzo): native Hanzo AI model backend + rebrand-completion fixes
The directive: hanzoai/ai (api.hanzo.ai) as the AI backend for all. Adds a native HanzoAI model component so flow routes generations through the Hanzo gateway (one OpenAI-compatible backend fronting Zen/DeepSeek/GLM) instead of a raw provider SDK — which is also what lets the provider-langchain zoo be dropped. - components/hanzo/hanzo_ai.py — HanzoAIModelComponent (LCModelComponent). Points ChatOpenAI at HANZO_BASE_URL (default https://api.hanzo.ai/v1) with the HANZO_API_KEY and the Zen model tiers (zen5-pro default). json_mode, temperature, max_tokens. - components/__init__.py — registered the "hanzo" category. Also fixes two more rebrand-missed-a-file-rename bugs surfaced by finally getting the env to resolve (they broke the whole model layer at import): - renamed lfx/utils/langflow_utils.py -> flow_utils.py (the import was rebranded, the file wasn't) — unblocks every LCModelComponent. - added the missing `from .schemas import DeploymentProviderKey` in the migrated deployment_provider_account model. NOTE: full app-load can't be verified from here yet — a SEPARATE, pre-existing breakage (the concurrent session's in-flight upstream settings-mixin merge, PR #13141) leaves lfx/services/settings/base.py with a broken import block (VARIABLES_TO_GET_FROM_ENVIRONMENT / Field / MAX_TEXT_LENGTH undefined). That's theirs to reconcile; this component's own code is py_compile-clean and mirrors the proven OpenAI component exactly.
This commit is contained in:
@@ -10,6 +10,8 @@ from sqlmodel import Column, DateTime, Field, Relationship, SQLModel, func
|
||||
from typing_extensions import Self
|
||||
|
||||
from flow.schema.serialize import UUIDstr
|
||||
|
||||
from .schemas import DeploymentProviderKey
|
||||
from flow.services.database.utils import (
|
||||
normalize_string_or_none,
|
||||
validate_non_empty_string,
|
||||
|
||||
@@ -178,6 +178,7 @@ _dynamic_imports = {
|
||||
"nvidia": "__module__",
|
||||
"olivya": "__module__",
|
||||
"ollama": "__module__",
|
||||
"hanzo": "__module__",
|
||||
"openai": "__module__",
|
||||
"openrouter": "__module__",
|
||||
"output_parsers": "__module__",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from lfx.components._importing import import_mod
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lfx.components.hanzo.hanzo_ai import HanzoAIModelComponent
|
||||
|
||||
_dynamic_imports = {
|
||||
"HanzoAIModelComponent": "hanzo_ai",
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
"HanzoAIModelComponent",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(attr_name: str) -> Any:
|
||||
"""Lazily import Hanzo AI components on attribute access."""
|
||||
if attr_name not in _dynamic_imports:
|
||||
msg = f"module '{__name__}' has no attribute '{attr_name}'"
|
||||
raise AttributeError(msg)
|
||||
try:
|
||||
result = import_mod(attr_name, _dynamic_imports[attr_name], __spec__.parent)
|
||||
except (ModuleNotFoundError, ImportError, AttributeError) as e:
|
||||
msg = f"Could not import '{attr_name}' from '{__name__}': {e}"
|
||||
raise AttributeError(msg) from e
|
||||
globals()[attr_name] = result
|
||||
return result
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return list(__all__)
|
||||
@@ -0,0 +1,111 @@
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic.v1 import SecretStr
|
||||
|
||||
from lfx.base.models.model import LCModelComponent
|
||||
from lfx.field_typing import LanguageModel
|
||||
from lfx.field_typing.range_spec import RangeSpec
|
||||
from lfx.inputs.inputs import (
|
||||
BoolInput,
|
||||
DictInput,
|
||||
DropdownInput,
|
||||
IntInput,
|
||||
SecretStrInput,
|
||||
SliderInput,
|
||||
StrInput,
|
||||
)
|
||||
from lfx.log.logger import logger
|
||||
|
||||
# The Hanzo AI gateway (api.hanzo.ai) is OpenAI-compatible and fronts EVERY model
|
||||
# (Zen / DeepSeek / GLM / ...). This is the one backend for all: flow never talks
|
||||
# to a raw provider SDK — it talks to the gateway, which routes and bills centrally.
|
||||
HANZO_DEFAULT_BASE_URL = "https://api.hanzo.ai/v1"
|
||||
HANZO_MODEL_NAMES = ["zen5-pro", "zen5", "zen5-coder", "zen5-flash", "best"]
|
||||
|
||||
|
||||
class HanzoAIModelComponent(LCModelComponent):
|
||||
display_name = "Hanzo AI"
|
||||
description = "Generate text through the Hanzo AI gateway (api.hanzo.ai) — one backend for every model."
|
||||
icon = "Hanzo"
|
||||
name = "HanzoAIModel"
|
||||
|
||||
inputs = [
|
||||
*LCModelComponent._base_inputs,
|
||||
IntInput(
|
||||
name="max_tokens",
|
||||
display_name="Max Tokens",
|
||||
advanced=True,
|
||||
info="The maximum number of tokens to generate. 0 or empty means no explicit limit.",
|
||||
range_spec=RangeSpec(min=0, max=128000),
|
||||
),
|
||||
DictInput(
|
||||
name="model_kwargs",
|
||||
display_name="Model Kwargs",
|
||||
advanced=True,
|
||||
info="Extra keyword args forwarded to the gateway (OpenAI-compatible chat params).",
|
||||
),
|
||||
DropdownInput(
|
||||
name="model_name",
|
||||
display_name="Model",
|
||||
advanced=False,
|
||||
options=HANZO_MODEL_NAMES,
|
||||
value=HANZO_MODEL_NAMES[0],
|
||||
combobox=True,
|
||||
info="A Zen tier or 'best'. Any model id the gateway serves is also accepted.",
|
||||
),
|
||||
StrInput(
|
||||
name="base_url",
|
||||
display_name="Hanzo API Base",
|
||||
advanced=True,
|
||||
value=HANZO_DEFAULT_BASE_URL,
|
||||
info="The OpenAI-compatible /v1 endpoint. Overrides the HANZO_BASE_URL env var.",
|
||||
),
|
||||
SecretStrInput(
|
||||
name="api_key",
|
||||
display_name="Hanzo API Key",
|
||||
info="Your Hanzo key. Falls back to the HANZO_API_KEY environment variable.",
|
||||
advanced=False,
|
||||
required=False,
|
||||
),
|
||||
SliderInput(
|
||||
name="temperature",
|
||||
display_name="Temperature",
|
||||
value=0.1,
|
||||
range_spec=RangeSpec(min=0, max=2, step=0.01),
|
||||
advanced=True,
|
||||
),
|
||||
BoolInput(
|
||||
name="json_mode",
|
||||
display_name="JSON Mode",
|
||||
advanced=True,
|
||||
info="If True, force the response into a JSON object.",
|
||||
),
|
||||
]
|
||||
|
||||
def build_model(self) -> LanguageModel: # type: ignore[type-var]
|
||||
logger.debug(f"Hanzo AI request with model: {self.model_name}")
|
||||
|
||||
api_key: Any = self.api_key
|
||||
if isinstance(api_key, SecretStr):
|
||||
api_key = api_key.get_secret_value()
|
||||
api_key = api_key or os.getenv("HANZO_API_KEY")
|
||||
|
||||
base_url = self.base_url or os.getenv("HANZO_BASE_URL") or HANZO_DEFAULT_BASE_URL
|
||||
|
||||
model_kwargs = dict(self.model_kwargs or {})
|
||||
model_kwargs.pop("api_key", None) # never let a stray api_key in kwargs override
|
||||
|
||||
output = ChatOpenAI(
|
||||
model=self.model_name,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
max_tokens=self.max_tokens or None,
|
||||
model_kwargs=model_kwargs,
|
||||
temperature=self.temperature if self.temperature is not None else 0.1,
|
||||
stream_usage=True,
|
||||
)
|
||||
if self.json_mode:
|
||||
output = output.bind(response_format={"type": "json_object"})
|
||||
return output
|
||||
Reference in New Issue
Block a user