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:
2026-07-16 15:19:40 -07:00
parent 81ce604aee
commit 943040fbf7
5 changed files with 148 additions and 0 deletions
@@ -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,
+1
View File
@@ -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