LLM utilities¶
LLM related utils
See devdocs/system/structured-output-fallback.md; ADRs 0014 (runtime
model switching), 0017 (invoke retry) and 0028 (prompt cache).
File: klea_rag/llm.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- class klea_utils.llm.CredentialScope(provider: str, endpoint: str | None = None)[source]¶
Bases:
NamedTupleProvider (and, for custom endpoints, endpoint) a credential belongs to.
API keys are credentials for a provider – and for an explicit or
custom:endpoint, that endpoint – not for a role or a model. Two models sharing a provider (openai:gpt-4oandopenai:gpt-4.1) share a scope;custom:m:<url>and anyhttp(s)endpoint suffix scope to that URL.
- class klea_utils.llm.CustomEndpoint(base_url: str, model_provider: str, use_responses_api: bool | None = None)[source]¶
Bases:
NamedTupleResolved API surface for a
custom:model endpoint URL.base_urlis the URL to hand to the provider SDK (the endpoint resource path is stripped, because the SDK appends it again).model_provideris the LangChain provider to use, anduse_responses_apiselects the OpenAI Responses API whenTrue(Noneleaves the SDK default).
- klea_utils.llm.DEFAULT_MAX_OUTPUT_TOKENS = 32768¶
Fallback max output tokens used when no node/role default provides a value.
Deliberately generous: reasoning models spend most of the completion budget on hidden reasoning tokens, so a small cap truncates even tiny structured payloads and forces a slow grow-ladder retry. The value is a ceiling, not a target, and is clamped per model by
resolve_output_token_limit()(catalog output limit and context headroom), so a large default is safe for small models too. HuggingFace is the exception: its total-budget accounting makes a large reservation trigger usage limits, so it keeps the smallerHUGGINGFACE_MAX_OUTPUT_TOKENSdefault and relies on retries.
- klea_utils.llm.HUGGINGFACE_DEFAULT_INFERENCE_PROVIDER = 'auto'¶
Inference provider used for the hosted backend when none is given (the HuggingFace router’s default selection).
- klea_utils.llm.HUGGINGFACE_LOCAL_SUFFIX = 'local'¶
Suffix selecting the local (pipeline) HuggingFace backend.
- klea_utils.llm.HUGGINGFACE_MAX_OUTPUT_TOKENS = 16384¶
Conservative HuggingFace default. HuggingFace-style providers account the reserved output window against the context and rate-limit large reservations, so keep the first attempt small and let the grow-ladder handle a genuinely truncated completion.
- klea_utils.llm.HUGGINGFACE_PROVIDER = 'huggingface'¶
HuggingFace provider id. Its backend is selected by the third model-string segment:
huggingface:<model>(or an inference provider / routing policy) uses the hosted Inference Providers API, whilehuggingface:<model>:localruns the model locally with the pipeline backend.
- klea_utils.llm.HUGGINGFACE_ROUTING_POLICIES: frozenset[str] = frozenset({'cheapest', 'fastest', 'preferred'})¶
HuggingFace routing policies. Unlike a concrete provider name these must be appended to the model id: the client’s
providerargument accepts onlyautoor a concrete provider name, not a policy.
- class klea_utils.llm.HuggingFaceBackend(backend: str, model_id: str, kwargs: dict[str, Any])[source]¶
Bases:
NamedTupleResolved HuggingFace backend for a model string suffix.
backendis theChatHuggingFacebackend ("pipeline"or"endpoint"),model_idis the repo id to pass (with a routing policy appended for the hosted backend), andkwargsare thefrom_model_idkwargs to inject (backendand, for the hosted backend,provider).
- klea_utils.llm.INPUT_TOKEN_ESTIMATE_SAFETY_FACTOR = 1.05¶
Safety factor applied on top of the char-based token estimate so the reserved output window keeps headroom for tokenizer variance: the
~4 chars/tokenrule can undercount the real token count (e.g. 9736 chars estimate 2434 tokens while the server tokenizes 2435), which pushes input + output one token over the model’s context window and fails the request.
- class klea_utils.llm.LLMModel(*, model_name: str = '', model_type: Literal['text_generation', 'embedding'] = 'text_generation', instance: Any, role_defaults: dict[str, Any] = {}, provider_defaults: dict[str, dict[str, Any]] = {}, modifiable: bool = True, required: bool = True, user_agent: str = '')[source]¶
Bases:
BaseModelContainer for a single LLM model instance and its runtime configuration.
instanceholds the model object (typically a_ConfigurableModelreturned byinit_chat_model).role_defaultsstores role-wide default parameters (e.g.max_tokens,temperature) that apply to every node sharing this role, unless overridden by node or user config.build_config()performs a five-layer merge:Layer 0 —
role_defaults: role-wide parameters (e.g.{"max_tokens": 4096}).Layer 1 —
model_name: the default model identifier from the graph config.Layer 2 —
context_overrides: per-request fields from the API (model,api_key, etc.). Only applied whenmodifiable=True, and skipping any keys frozen by node defaults.Layer 3 —
node_defaults: frozen per-node defaults (always win).Layer 4 —
provider_defaults: per-provider defaults from the graph config (e.g. HuggingFace role budgets), applied after the model string is parsed so the resolved provider is known. Applied withsetdefaultso explicit role/context/node values always win.modifiablecontrols whether the model can be changed at runtime (both the API and web UI reject modifications to locked roles). Set toFalseto lock a role (e.g. guard) against user overrides in managed deployments.requiredmarks roles that need a default model for the app to function (e.g.chat). At startup, required roles with an empty model trigger a warning (not a failure) listing the environment variables to set. Optional roles (e.g.guard) are skipped when their model is empty.- build_config(context_overrides: dict[str, Any] | None = None, node_defaults: dict[str, Any] | None = None, session_id: str | None = None) RunnableConfig[source]¶
Merge up to five layers of model configuration into a
RunnableConfig.Layer order (lowest -> highest priority):
self.role_defaults— role-wide parametersself.model_name— role model from graph configcontext_overrides— per-request user overridesnode_defaults— frozen per-node defaultsself.provider_defaults— per-provider defaults (setdefault)
- Parameters:
context_overrides – Per-request fields from the API (e.g.
model,api_key). Only applied whenself.modifiable is True, and skipping any keys present innode_defaults.node_defaults – Frozen per-node defaults (e.g.
{"temperature": 0.3}). Always win.session_id – Per-conversation session id (LangGraph
thread_id) used for the opencodex-opencode-sessionheader.
- Returns:
A
RunnableConfigwith theconfigurablekey populated.
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- model_type: Literal['text_generation', 'embedding']¶
Inference kind the role’s model must provide.
text_generationcovers chat LLMs (chat/plan/tool_picker/guard), resolved throughcreate_configurable_model;embeddingcovers vector embedders, resolved throughsetup_embedding(). Reserved future values:decision(System One / Jev-style typed-decisions),rerankingandclassification.
- klea_utils.llm.PROVIDERS_WITHOUT_API_KEY: frozenset[str] = frozenset({'ollama'})¶
Providers that authenticate locally/anonymously and therefore need no API key. Used so readiness checks do not flag e.g. a local Ollama deployment as missing a credential.
- klea_utils.llm.PROVIDERS_WITHOUT_CUSTOM_URL: frozenset[str] = frozenset({'huggingface', 'ollama', 'openrouter'})¶
Providers that do not take a custom URL in the model string. For
ollama/openrouterthe third segment is a model tag; forhuggingfaceit is the inference provider (huggingface:org/model:novita). The model UIs hide the URL field for these.
- klea_utils.llm.PROVIDERS_WITH_MODEL_TAG: frozenset[str] = frozenset({'ollama', 'openrouter'})¶
Providers where
model:tagis part of the model id itself (not a Klea suffix). Example:ollama:qwen3:0.6b,openrouter:org/model:free.
- class klea_utils.llm.ParsedModelName(provider: str | None, model_name: str, suffix: str | None, variant: str | None = None)[source]¶
Bases:
NamedTupleParsed components of a model name string.
- klea_utils.llm.add_memory_to_prompt(context_summary: str) str[source]¶
Add the context summary to the system prompt.
Returns a text block framing the previous-context summary. Recent conversation messages are no longer flattened into this block: they are injected as real message objects by the node’s prompt assembly (see
klea_utils.nodes.base).The summary is dynamic text concatenated into the system prompt template, so its braces are escaped (
escape_prompt_braces()); a summary that echoes a JSON/code fragment from the conversation would otherwise be parsed as a template variable.- Parameters:
context_summary – Summary of the past conversation.
- Returns:
Prompt text block, or
""when there is no summary.
- klea_utils.llm.apply_provider_overrides(overrides: dict[str, Any], *, session_id: str | None = None, user_agent: str = '') None[source]¶
Add provider-specific request headers to a configurable dict (in place).
Called once at the end of
LLMModel.build_config(). Only providers with a registered builder are affected; anydefault_headersalready in overrides win over ours.- Parameters:
overrides – The merged
config["configurable"]dict.session_id – Per-conversation session id (LangGraph
thread_id).user_agent – Klea User-Agent (see
resolve_user_agent()).
- klea_utils.llm.check_model_works(model, timeout=30, retries=5)[source]¶
Check if a model works since it is not tested when loaded
- klea_utils.llm.check_ollama_model(logger, model, exit=False)[source]¶
Check if ollama model is available
- klea_utils.llm.classify_llm_invocation_error(exc: BaseException) LLMInvocationErrorCategory[source]¶
Classify an LLM invocation exception into a category.
Providers report failures inconsistently, so this uses tolerant regex matching over the exception message and its cause chain. Categories are checked in order of specificity (context overflow first), so a message matching several heuristics lands in the most actionable bucket.
- Parameters:
exc – The exception raised by an LLM invocation.
- Returns:
The best-effort
klea_utils.errors.LLMInvocationErrorCategory.
- klea_utils.llm.clear_structured_output_cache() None[source]¶
Clear the process-local structured-output capability cache (tests).
- klea_utils.llm.content_to_str(content: str | list[dict | str] | None) str[source]¶
Normalise an
AIMessage.contentvalue to a plain string.AIMessage.content can be a plain string, a list of content blocks (when the LLM returns tool calls or structured output), or
None. This helper always returns a string suitable for downstream text processing (regex,inchecks, prompt interpolation, etc.).- Parameters:
content – The raw
.contentvalue from an AIMessage.- Returns:
A plain string.
- klea_utils.llm.create_configurable_model(logger: Logger)[source]¶
Set up a configurable chat model.
Creates a
_ConfigurableModelwith no default model. Model, provider, and all other parameters (base_url,api_key,temperature, etc.) are specified per-invoke via theconfig["configurable"]dict passed toainvoke().This enables runtime model switching — each
ainvoke()call creates a fresh underlying model instance for the given provider, so there is no stale configuration leakage between calls.The lookup function
check_model_worksis deliberately not called here — we prefer a “leap before you look” approach so that startup is fast and model availability is checked only at query time.
- klea_utils.llm.credential_scope(model_name: str) CredentialScope[source]¶
Return the credential scope for model_name.
- Parameters:
model_name – Provider-prefixed model string, e.g.
openai:gpt-4oorcustom:model:https://host/v1.- Returns:
CredentialScope– a lowercased provider and an optional endpoint for custom / explicit-URL models.
- klea_utils.llm.escape_prompt_braces(text: str) str[source]¶
Escape
{and}for safe embedding in a prompt template.ChatPromptTemplateparses{...}in its template strings as input variables, so dynamic text concatenated into a template (a context summary, a generated JSON schema, configured domain text) must be escaped or a brace in it raisesINVALID_PROMPT_INPUT. Text passed as a template variable value, and message objects placed in the prompt, are not templated and need no escaping.- Parameters:
text – Dynamic text to embed in a prompt template.
- Returns:
The text with braces doubled (
{->{{,}->}}).
- klea_utils.llm.estimate_input_tokens(input_chars: int) int[source]¶
Conservative token estimate for a prompt’s character count.
Used to keep the reserved output window within a model’s total budget (input + output <= context). ~4 characters per token is a reasonable average for mixed English/code text; an exact count would require provider-specific tokenizers. The estimate is deliberately inflated by
INPUT_TOKEN_ESTIMATE_SAFETY_FACTOR(rounded up) so a tokenizer that counts more tokens than the average never pushes the request over the context window.- Parameters:
input_chars – Number of characters in the prompt.
- Returns:
Conservative estimated token count.
- klea_utils.llm.extract_llm_output_content(output: AIMessage | dict) str[source]¶
Extract plain-text content from an LLM output.
Handles both
AIMessage(non-structured output) anddict(structured output withraw/parsedkeys):AIMessage– returnscontent_to_str(message.content).dict– extracts therawAIMessagefrom a structured output response and returns its content; falls back tooutput["parsed"]and finallystr(output).
- Parameters:
output – The raw output from
llm.invoke().- Returns:
A plain-text string.
- klea_utils.llm.format_alert(text: str, level: str = 'warning') str[source]¶
Wrap text as a GitHub-style markdown alert (e.g.
> [!WARNING]).Multi-line text is prefixed per line so the whole thing stays inside the blockquote. Renderers with the markdown2
alertsextra (the NiceGUI speech bubbles) show it as a styled callout; others fall back to a plain blockquote.- Parameters:
text – Alert body text
level – Alert level (note, tip, important, warning, caution)
- Returns:
Markdown alert blockquote
- klea_utils.llm.get_last_n_conversations(all_messages, start: int = 0, stop: int | None = None) tuple[str, list[BaseMessage]][source]¶
Get recent conversations between start and stop indices.
Returns the conversation as a single text block (used as prompt/summary input) along with the ordered
BaseMessageobjects, preserving the interleaved user/assistant order of the original history.- Parameters:
all_messages – all the messages
start – start index
stop – stop index
- Returns:
(conversation, ordered list of human/ai messages)
- klea_utils.llm.get_provider_allowed_fields(provider: str) set[str][source]¶
Return the set of init-param names accepted by a given provider’s model class.
Uses LangChain’s internal provider registry to look up the Pydantic model class and introspect its fields (including aliases so that both
api_keyandopenai_api_keypass through).Falls back to an empty set if the provider is not registered in LangChain’s built-in providers. Raises
ImportErrorif the provider’s integration package is not installed — callers should handle this at configuration time, not silently fall through.The caller should always include
{"model", "model_provider"}on top of the returned set since those are consumed by_ConfigurableModelbefore reaching the model constructor.
- klea_utils.llm.get_recent_messages(messages, max_chars: int, keep_at_least: int = 1) list[BaseMessage][source]¶
Return the most recent human/ai messages bounded by max_chars.
Walks backwards through messages (in their original interleaved order) accumulating
pretty_repr()length until max_chars would be exceeded. The first keep_at_least messages are always included, so the latest exchange is never dropped even when it alone exceeds the budget.- Parameters:
messages – All conversation messages.
max_chars – Maximum total characters of the returned window.
keep_at_least – Minimum number of messages to always include.
- Returns:
Ordered list of recent human/ai messages.
- klea_utils.llm.get_token_limit_param(provider: str) str[source]¶
Return the max-output token parameter name for a provider.
Providers disagree on the parameter name for the maximum number of output tokens: Ollama uses
num_predict, while HuggingFace’sChatHuggingFace(which internally maps it tomax_new_tokens) and other OpenAI-compatible providers all usemax_tokens.Note
Known benign warning
When Klea resolves
max_tokensfor HuggingFace, the innerHuggingFaceEndpointconstructed byChatHuggingFace.from_model_id(which declaresmax_new_tokens, notmax_tokens) logsWARNING! max_tokens is not default parameterand shuffles it intomodel_kwargs. This is a false positive: the limit is still delivered correctly asmax_tokenstoInferenceClient.chat_completionvia the outerChatHuggingFace, which is the parameter the HuggingFace Inference API actually accepts. Do not “fix” it by switching tomax_new_tokenshere.- Parameters:
provider – Klea provider id (
huggingface,ollama, …)- Returns:
The token parameter name to send in the invoke config.
- klea_utils.llm.is_empty_structured_parse_error(exc: BaseException) bool[source]¶
Return True if exc is a structured-output parse error on blank text.
On the structured path,
with_structured_output(...).ainvokeraisesOutputParserExceptionfor a blank model response instead of returning a dict, sois_output_empty()never sees it and the empty-response retry inBaseLLMNode._invoke_with_retriesis skipped. LangChain’s parsers phrase the failure as"Invalid json output: ..."followed by the (here empty) raw text, so match that shape rather than treating every parser error as retryable: a non-empty invalid payload is a real schema mismatch that should not be retried.- Parameters:
exc – The exception raised by the structured-output invoke.
- Returns:
True when the parser failed because the response was blank.
- klea_utils.llm.is_output_empty(output: AIMessage | dict[str, Any]) bool[source]¶
Return True if an LLM output carries no usable text content.
Some providers (notably HuggingFace) intermittently return a successful response with blank content. Handles both plain
AIMessageoutputs and structured-output dicts ({"raw": AIMessage, "parsed": ...}); for the structured form therawmessage is inspected, so a blank raw response is flagged even when the parser produced an all-default instance.- Parameters:
output – The raw output from
llm.invoke().- Returns:
True when the output has no non-whitespace text.
- klea_utils.llm.is_output_truncated(output: AIMessage | dict[str, Any]) bool[source]¶
Return True if an LLM output was truncated by the max-token limit.
Providers signal truncation via
finish_reason == "length"on the message metadata. Handles both plainAIMessageoutputs and structured-output dicts ({"raw": AIMessage, ...}), and the list-formfinish_reasonsome providers return.- Parameters:
output – The raw output from
llm.invoke().- Returns:
True when the model stopped because it hit the output cap.
- klea_utils.llm.is_structured_capability_rejection(exc: BaseException) bool[source]¶
Return True if exc is a stable capability rejection of structured output.
Stricter than the
STRUCTURED_OUTPUT_REJECTEDcategory returned byclassify_llm_invocation_error(): only messages namingresponse_format/json_schema/structured outputqualify. The category’s generic400 invalid_request_errorpattern is excluded so an unrelated bad request cannot poison the capability cache.- Parameters:
exc – The exception raised by the structured invoke.
- Returns:
True when the endpoint will keep refusing the parameter.
- klea_utils.llm.is_structured_output_failure(exc: BaseException) bool[source]¶
Return True if exc means the structured call produced no usable output.
The structured path (
with_structured_output(...).ainvoke) can fail in ways the plain invoke cannot: the provider rejects theresponse_formatparameter, or the model returns content that does not satisfy the schema. Some SDKs surface the latter as a pydanticValidationError(the OpenAI client validates JSON inside its own parser) rather than a LangChain parser error, so both types are treated as a structured failure.Detection is by exception type wherever possible, so a provider’s new error wording does not need a new pattern; the message-classified cases are
STRUCTURED_OUTPUT_REJECTED(parameter refusal), which has no distinct exception type, and any HTTP 400 from the structured call. The latter covers OpenAI-compatible gateways that return a bare 400 (e.g. only{"model": ...}) when an upstream cannot process the parameter, leaving the SDK message as just"Error code: 400". It is a per-call fallback only: because the same status also covers unrelated bad requests, it must not populate the capability cache.Empty responses are deliberately included: the caller decides whether to retry them on the structured path or fall back to plain.
- Parameters:
exc – Exception raised by the structured invoke.
- Returns:
True when the structured call should give way to a plain invoke.
- klea_utils.llm.join_model_string(provider: str | None, model_name: str, suffix: str | None = None) str[source]¶
Join model name components back into a model string.
The inverse of
parse_model_name(), used by the model selection UIs on save. Givesprovider:model_name, orprovider:model_name:suffixwhen a suffix (e.g. a custom endpoint URL) is set.Without a provider the model name is returned as it is, so a full model string typed straight into the model field still works.
- Parameters:
provider – Klea provider id, e.g.
"openai".model_name – Model id within the provider, e.g.
"gpt-4o".suffix – Optional third segment.
- Returns:
The model string, or
""when no model name is set.
- klea_utils.llm.load_prompt(prompt_name: str, prompt_registry_location: str)[source]¶
Load a prompt from file called prompt_name.md
- Parameters:
str – prompt file name
prompt_registry_location – location of prompts folder/registry
- Returns:
loaded prompt text
- klea_utils.llm.mark_structured_output_unsupported(provider: str, model: str, base_url: str | None = None) None[source]¶
Record a stable capability rejection for the process lifetime.
- Parameters:
provider – Resolved
model_provider.model – Resolved model identifier.
base_url – Endpoint base URL, when one applies.
- klea_utils.llm.missing_required_roles(models: Mapping[str, Any]) list[str][source]¶
Return the roles that are required but have no model configured.
Single source of truth for the “is a required model missing?” predicate, shared by the graph’s startup warning (
BaseLangGraph._check_required_models) and the frontends (which need it to decide whether a query may be sent and to point the user at the model selector).Values may be either the resolved API config dict returned by the model endpoints (e.g.
GET /chat/{user_id}/models/active, withmodel/requiredkeys) orLLMModelinstances (withmodel_name/required). A role is missing whenrequiredis true (the default) and its model is empty. Roles are returned in the input’s iteration order.- Parameters:
models – Mapping of role name to its config dict or object.
- Returns:
Required role names with no model set.
- klea_utils.llm.parse_model_name(raw: str) ParsedModelName[source]¶
Split a model name into provider, model identifier, and suffix.
Follows the
provider:model_idconvention. The provider is expected to be explicitly included; no provider inference is done.With three segments the third is usually a
suffix(provider hint, URL, and so on). Forollamaandopenrouterit is kept on the model name instead, because those providers usemodel:tagids.Examples:
ollama:bge-m3:latest-> provider=ollama, model=bge-m3:latest, suffix=Noneopenrouter:recursive/flint-v1.0:free-> provider=openrouter, model=recursive/flint-v1.0:free, suffix=Nonehuggingface:org/model:auto-> provider=huggingface, model=org/model, suffix=autocustom:model:https://example.com/v1-> provider=custom, model=model, suffix=https://example.com/v1openai:gpt-4o-> provider=openai, model=gpt-4o, suffix=Nonebge-m3-> provider=None, model=bge-m3, suffix=None
- Parameters:
raw – Model name with optional provider prefix
- Returns:
Parsed model name components
- klea_utils.llm.parse_output_with_thought(message: AIMessage, schema: type[TSchema]) tuple[TSchema, str][source]¶
Parse AI message with thought to a dict based on given schema
- klea_utils.llm.prompt_value_to_messages(prompt: PromptValue) list[dict][source]¶
Convert a
PromptValueto a clean list of message dicts.Each dict has
roleandcontentkeys, suitable for JSON serialisation in the inspector debug panel.- Parameters:
prompt – The LangChain
PromptValue(filled, variables already substituted).- Returns:
A list of
{"role": "...", "content": "..."}dicts.
- klea_utils.llm.provider_api_key_env(provider: str) str | None[source]¶
Return the env var holding provider’s API key, or
None.Providers that need no key (see
requires_api_key()) returnNone. Otherwise mirrors the SDK convention so the UI can report whether a credential comes from the environment.
- klea_utils.llm.requires_api_key(model_name: str) bool[source]¶
Return whether model_name needs an API key.
Accepts either a bare provider id (
openai,ollama) or a full model string (openai:gpt-4o,huggingface:org/model:local). Local providers (Ollama) and locally-run HuggingFace models (huggingface:<model>:local) authenticate locally and need no credential; every other provider does. A string that cannot be parsed is judged by its provider part; a string with no:is treated as a provider id.
- klea_utils.llm.resolve_catalog_provider_endpoint(provider: str, model_name: str | None = None) CustomEndpoint | None[source]¶
Resolve a catalog provider to a wire surface via its models.dev entry.
Lets a user write
provider:model(e.g.openrouter:...) without an explicit URL: the catalog’sapisupplies the base URL and itsnpmpackage identifies the wire protocol. The wire surface is resolved from the npm:@ai-sdk/anthropic-> Anthropic Messages API (/v1/messages);@ai-sdk/openai-> OpenAI Responses API (/responses);@ai-sdk/openai-compatible(and anything else with anapi) -> OpenAI Chat Completions, the catalog’s overwhelming default.
The npm is resolved per model when a model name is given, so gateway providers that serve different models on different surfaces (OpenCode, OpenRouter, …) pick the right one – the catalog records this as a per-model
provider.npmoverride.The returned
base_urlis what the SDK appends its resource path to. For the Anthropic surface the catalogapitypically already ends in/v1(e.g..../anthropic/v1) whileChatAnthropicre-appends/v1/messages, so a trailing/v1is stripped here; a full.../v1/messagesis handled byresolve_custom_endpoint().Returns
Nonewhen the provider is not in the catalog, has noapi(native SDK providers resolve their own endpoint), uses a surface Klea does not implement (e.g.@ai-sdk/google), or the catalog is unavailable – callers then leave the model string to LangChain.- Parameters:
provider – Klea provider id (e.g.
"openrouter").model_name – Model identifier within the provider, or
None.
- Returns:
The resolved
CustomEndpoint, orNone.
- klea_utils.llm.resolve_custom_endpoint(url: str) CustomEndpoint[source]¶
Detect the wire API surface from a
custom:model endpoint URL.The suffix of url selects the API surface so that one
custom:model string works for any OpenAI-compatible endpoint, the OpenAI Responses API, or the Anthropic Messages API (e.g. opencode Go serves different models on different surfaces). Because each provider SDK appends its own resource path, the matched suffix is stripped from the returnedbase_url:.../chat/completions-> OpenAI Chat Completions (openai).../responses-> OpenAI Responses API (openai, responses).../v1/messages-> Anthropic Messages API (anthropic; the SDK re-appends/v1/messages, so.../zen/gois returned)
Any other URL is treated as a plain base URL and defaults to the standard OpenAI Chat Completions surface (
openai, flag unset), preserving the behaviour for self-hosted OpenAI-compatible endpoints.- Parameters:
url – The
custom:model string suffix (a base URL or a full endpoint URL).- Returns:
The resolved
CustomEndpoint.- Raises:
ValueError – If url is empty.
- klea_utils.llm.resolve_huggingface_backend(model_name: str, suffix: str | None) HuggingFaceBackend[source]¶
Resolve the HuggingFace backend from a model name and its suffix.
:localselects the local pipeline backend; any other suffix is an inference provider (autowhen absent) for the hosted Inference Providers API. The routing policies (HUGGINGFACE_ROUTING_POLICIES) are appended to the model id instead of being passed asprovider, because the HuggingFace client’sproviderargument accepts onlyautoor a concrete provider name.- Parameters:
model_name – HuggingFace repo id, e.g.
"openai/gpt-oss-20b".suffix – Third model-string segment, or
None.
- Returns:
The resolved
HuggingFaceBackend.
- klea_utils.llm.resolve_langchain_endpoint(instance: Any, config: RunnableConfig) str | None[source]¶
Resolve the base URL/endpoint of a configurable chat model.
create_configurable_modelreturns a generic_ConfigurableModelwhose concrete provider instance is only built at invoke time (via its private_model(config)method, which is already called on everyainvoke). Native providers (mistral:,anthropic:,deepseek:, …) do not carry abase_urlin the configurable dict – the provider resolves its own default endpoint internally – so to probe such an endpoint we materialise the concrete instance and read its resolved endpoint attribute.Cheap by construction:
_model(config)only parses the model string and constructs the provider object (module import is cached per provider); no network or API calls happen at construction.- Parameters:
instance – The configurable model (
_ConfigurableModel) whose concrete instance to materialise.config – Per-invoke RunnableConfig carrying the merged
configurabledict.
- Returns:
The resolved base URL, or
Nonewhen the concrete model exposes no known endpoint attribute (e.g. plainopenai:models, whose default URL lives inside the OpenAI SDK client, not on the model object).
- klea_utils.llm.resolve_output_token_limit(overrides: dict[str, Any], provider: str, role: str | None = None, input_chars: int | None = None, *, use_endpoint: bool = False) None[source]¶
Ensure a bounded max-output token param is set in overrides.
HuggingFace-style providers apply a total budget: the reserved output window (
max_new_tokens) is accounted against the model’s context window alongside the input, and an unset value makes them reserve the entire remaining window (causing spurious usage limits and rate limiting). This helper guarantees a finite, clamped value.Resolution precedence:
An explicit provider token param (
max_tokens/max_new_tokens/num_predict) already present inoverrides(user/node/role value).The generic
max_output_tokenskey (provider-agnostic count).The built-in per-role fallback for role and provider: non-HF providers get the generous
DEFAULT_MAX_OUTPUT_TOKENS; HuggingFace gets the conservativeHUGGINGFACE_MAX_OUTPUT_TOKENSbecause large reservations count against its total budget and trigger usage limits.
The resolved value is clamped to
min(value, catalog limit.output)and, when the catalog exposes a context window and input_chars is given, to the remaining budget (context - estimated input tokens) so HuggingFace’s total-budget check is never exceeded.The context source depends on use_endpoint: for endpoints with a known
base_url(custom OpenAI-compatible endpoints, or a native provider endpoint resolved viaresolve_langchain_endpoint()),max_model_lenfrom the live/modelsprobe is authoritative (models.dev’s values are per-deployment and may under-report). The normal (non-retry) path passesuse_endpoint=Falseso it stays on the fast, offline models.dev value – its purpose is only to produce a finite budget (mainly for the HuggingFace whole-window reservation), not an exact cap. The retry path passesuse_endpoint=Trueto clamp against the real server context, falling back to models.dev when the endpoint probe fails. Theoutputclamp always uses models.dev (the endpoint exposes no separate output cap).- Parameters:
overrides – The merged
configurabledict to update in place.provider – Klea provider id (
huggingface,ollama, …).role – Model role (e.g.
"chat"), used for the built-in per-role, per-provider fallback.input_chars – Character count of the prompt, to bound the output within the total budget.
use_endpoint – When True, prefer the live endpoint’s
max_model_lenas the context source, falling back to models.dev. When False (default), use models.dev only and never query the endpoint.
- klea_utils.llm.resolve_user_agent(prefix: str) str[source]¶
Return
<prefix>/<version>for the running application.Mirrors
web_fetch.honest_user_agent: the version is read from the installed distribution named prefix (importlib.metadatanormalises separators, so the graph name doubles as the distribution name –klea-agent->klea_agent) and falls back todevwhen metadata is unavailable (e.g. an uninstalled checkout).- Parameters:
prefix – Application name, e.g.
"klea-agent".- Returns:
The User-Agent string, e.g.
"klea-agent/0.0.1".
- klea_utils.llm.split_output_by_section(text: str, section_start_marker: str, section_end_marker: str | None = None)[source]¶
Split out thoughts and actual responses from AI responses
- klea_utils.llm.structured_output_known_unsupported(provider: str, model: str, base_url: str | None = None) bool[source]¶
Return True if this endpoint was seen refusing structured output.
Consulted before building
with_structured_outputso a rejecting endpoint is not re-probed on every node call. A miss (False) may still reject – the first attempt for a key always probes.- Parameters:
provider – Resolved
model_provider(e.g."openai").model – Resolved model identifier.
base_url – Endpoint base URL, when one applies.
- Returns:
True when the key is known to reject
response_format.