Agent workflows ยท 2026-08-29

Setting up the LLMTR provider in OpenCompany workflows

How the branch that adds LLMTR as OpenCompany's second gateway wires up the credential entry, the chat model node, the catalogue filter and the embedding provider inside the native LLM layer.

LLMTR guide diagram showing how the LLMTR credential, the chat model node and the embedding node connect to model resolution on an OpenCompany workflow canvas.

Where LLMTR sits in OpenCompany's provider layer

OpenCompany is a React Flow based workflow automation platform. Model calls do not pass through LangChain; they go through the native SDK facade under server/services/llm/, where every provider is registered as a protocol implementation. The feat/llmtr-provider branch in the knowhycodata/OpenCompany repository adds a provider named llmtr and takes that surface to 14 providers.

The branch documentation draws a distinction that matters: two of those providers front a gateway rather than a single vendor. Being a gateway has concrete consequences here. Because model ids are spelled vendor/model, llmtr joins the open-world tuple in is_model_valid_for_provider and the router set in model_registry. The first makes every catalogue row valid for the provider; the second lets context and output limits be recovered from the router cache by stripping the vendor prefix.

The provider class inherits OpenAIProvider whole and overrides exactly one method. The wire format is plain OpenAI, so nothing changes except the endpoint, pinned as https://llmtr.com/v1 through the provider spec's client kwargs. A stored proxy credential still overrides that value at call time, so a self-hosted deployment needs no code change.

The credential entry: one field, a different probe

The user-facing half of the provider is the llmtr block in server/config/credential_providers.json. It extends the shared AI base template and declares a single apiKey field whose placeholder shows the key format. The icon reference points at the backend credential-icon endpoint because the shared icon package ships no LLMTR brand, which is the same shape entries like sarvam and stripe already use.

The real difference is validation. Other LLM credentials share a probe that accepts any key whose model list loads. That probe cannot work here, because the model list route is public: it answers with success for no key at all and for an invalid key. So the LLMTR credential overrides it and proves the key with a real one-token completion instead.

The order of that probe is locked by a test as well: chat request first, model listing second. A rejected key must never come back with a populated dropdown beside the error. Error translation is switched off so the raw error object reaches the classifier, which reads its status code.

The core of the llmtr block in credential_providers.json

{
  "llmtr": {
    "extends": "_ai_base",
    "name": "LLMTR",
    "color": "#E11D48",
    "icon_ref": "/api/schemas/credentials/llmtr/icon",
    "fields": [
      {
        "key": "apiKey",
        "placeholder": "llmtr-..."
      }
    ]
  }
}

How the chat model node resolves a model

The node type is llmtrChatModel, its display name is LLMTR and its subtitle is Chat Model. The parameter surface is deliberately generic: the node adds only frequency and presence penalty fields and declares no dedicated thinking control. The reasoning is written into the node itself. The effective parameter set depends on which vendor the selected vendor/model id lands on, and a field the chosen model does not accept is a request-time error rather than a disabled control. The inherited thinking and reasoning effort parameters remain available for rows that take them.

Resolution happens in the shared model policy function, which the branch splits in two. When the gateway's JSON block carries the vendor-prefixed routing flag and the id contains a slash, the prefix is mapped through vendor aliases onto a provider block. Wire shape is then read from that block, while thinking stays with the gateway's own block.

The split is justified with live observations recorded in the test file. A vendor's reasoning model refuses the max tokens plus temperature pair no matter who proxies it, so wire shape is a property of the vendor. Reasoning controls are not forwarded vendor-natively: the gateway normalizes them its own way, drops vendor-proprietary body fields, and rejects the parameter outright on rows where it has not enabled it. Adopting the vendor's native thinking type here would break working calls the moment a user ticked thinking.

Which decision resolves against which block for a gateway request
DecisionResolved fromExample given in the branch
Max completion tokens or max tokensThe routed vendor's blockThe openai/o4-mini row refuses max tokens
Whether temperature may be sentThe routed vendor's blockopenai/gpt-4o sends it, openai/o4-mini does not
Thinking type and reasoning parameterThe gateway's own blockThe anthropic/claude-sonnet-5 row is driven with effort too
Whether the Responses path is usedAlways off for a gatewayThe request always goes to the chat completions path
A vendor prefix with no blockThe gateway's generic blockSelf-hosted rows such as llmtr/trendyol-7b

The catalogue filter and the rows the dropdown hides

The single method the provider overrides is the model fetch. The shared OpenAI path returns every row in the listing, and the LLMTR catalogue is mixed-modality: alongside chat rows it carries embedding, image, video, rerank and audio rows. An unfiltered list puts an embedding model in a chat node's dropdown, where the user learns about it at run time through an opaque failure rather than at selection time. The filter is not a heuristic: the catalogue publishes a supported operations field per row and the code reads it.

When that field is missing or empty the row is treated as chat capable. The permissiveness is deliberate, so an absent declaration never silently hides a working model.

In one respect the filter is narrower than it needs to be. The code excludes rows published only with a Responses binding, assuming they would fail on every call. On LLMTR the bridge is asymmetric: a chat completions request works when the model has either a chat completions or a Responses binding. The direction with no bridge is the other one, since only Responses-bound rows can reach the Responses path. In practice, some rows published today with a Responses binding alone do not appear in the node dropdown even though the request shape the node sends can reach them. Nothing is lost, but the list is conservative; widening it means changing the accepted operation set.

  • The filter reads the published operations field, not the model name.
  • An empty field means the row is treated as chat capable.
  • Rows published only with a Responses binding are not listed in this version.
  • The chat completions path bridges those rows, so the limit is the code's own choice.

Embeddings: the same key, a second wire surface

The branch does not add LLMTR as a chat provider alone. It is also a selectable embedding provider in the vector store, and the document-side embedding generator node exposes llmtr together with its key field. No separate adapter was written: the embeddings endpoint speaks the same OpenAI wire format as chat, so the existing OpenAI embedder is reused.

The price of that reuse is an explicitly pinned endpoint. If the pin regresses, selecting llmtr silently builds a client aimed at OpenAI's own address and sends the user's LLMTR key there. The branch locks this with a test, and it also refuses to build a client with an empty key, since a quiet client would only surface the failure in the middle of a batch.

That the default embedding model carries the llmtr prefix is also under test. The reasoning is data residency: if in-region processing is why the gateway was chosen, the default must not fall back to a foreign vendor. In the public catalogue the only live embedding row published under that prefix is llmtr/embeddinggemma-300m, so verify against the catalogue if you set a different default. Note that the catalogue rows appearing in the branch's unit tests are stubbed payloads, not real listings.

Checking which rows the node dropdown will see, without a key

# Rows the chat node will list
curl -s https://llmtr.com/v1/models \
  | jq -r '.data[] | select((.supported_operations // []) | index("CHAT_COMPLETIONS")) | .id' \
  | head

# Rows the embedding node can use
curl -s https://llmtr.com/v1/models \
  | jq -r '.data[] | select((.supported_operations // []) | index("EMBEDDINGS")) | .id'

Verification and the failures you will meet

After setup, verify three things in order: that the credential saves, that the node dropdown fills, and that the selected model actually answers. These are separate layers and passing one does not imply the next. A populated model list in particular says nothing about the key.

The table below collects the symptoms this branch produces and where each comes from. Every row maps to behaviour present in the code or its tests; none of them is a measurement.

Symptom, likely cause and the check to run
SymptomLikely causeCheck
The model list fills with an invalid keyThe listing route is public and succeeds for any keyLet the one-token completion decide; do not treat the listing as proof
The request returns 400 saying the provider rejected itWire shape resolved from the gateway's generic block instead of the vendor'sConfirm the model prefix maps to a real provider block through vendor aliases
A 400 response says the reasoning toggle is unavailableThe gateway has not enabled reasoning on that rowTurn the thinking field off on the node and retry
An expected model is missing from the dropdownThe row's published operations do not include chat completionsRead the row's binding from the catalogue and widen the accepted set if needed
The embedding call does not reach LLMTRThe client endpoint is not pinnedVerify the client points at llmtr.com when llmtr is selected

Frequently asked questions

Why does the llmtrChatModel node declare no dedicated thinking fields?

The node sits in front of a gateway, and the valid parameter set changes with the vendor the selected vendor/model id resolves to. The code accepts that and keeps the surface generic; a field the model does not take fails at request time. The inherited thinking parameters still work on rows that support them.

Can the same LLMTR key serve both the chat node and the embedding node?

Yes. The embeddings endpoint speaks the same OpenAI wire format as chat, so the branch writes no separate adapter and reuses the existing embedder with a pinned endpoint. The document-side embedding generator node shows the key field when llmtr is selected.

If the model list loads, is my key valid?

No. The LLMTR model listing route is public and succeeds without a key at all. That is why the branch validates the credential with a real one-token completion and runs that request before the listing.

How are parameters resolved for rows carrying the llmtr prefix?

Those rows have no matching vendor block, so the gateway's generic block is used. The code treats this as a deliberate default: rather than guessing which vendor an unknown prefix belongs to, it keeps the conservative wire shape.

Related posts