feat: custom OpenAI-compatible LLM endpoints across Telegram, web and agents - #175
Merged
Merged
Conversation
… agents Reworks the custom-endpoint support so an endpoint configured once is usable everywhere, and so the surfaces that build a model can actually resolve it. Storage - Endpoints move out of raw user_data into condor/preferences.py as a named list synced to config.yml, giving Telegram and the web dashboard one shared view. Legacy user_data["custom_llm"] migrates on first read. - Agent keys become "custom@<endpoint>:<model-id>". The "@" segment keeps the model id recoverable with a single partition even when it contains colons and slashes (e.g. meta-llama/Llama-3.3-70B:free). Fixes - /cancel matched no handler at all: the unified text handler is registered with ~filters.COMMAND and no CommandHandler existed, so the documented escape hatch silently did nothing while an invalid URL re-armed the prompt, trapping the user. Adds the command plus inline Cancel buttons. - Armed text-input flags survived navigation, so a stale flow ate the user's next message. They are now cleared in clear_all_input_states. - consult/delegate/engine constructed PydanticAIClient with no base_url or api_key, so a custom agent_key died with "No base URL configured" — session.py was the only caller that resolved endpoints. Adds a shared resolve_custom_endpoint() used by all of them. - New Agents inherited nothing, so the coordinator invented an agent_key from the agent_builder skill's examples and pinned agents to backends the user never configured. agent_key now defaults to the creator's active model, and get_user_context() reports it so the coordinator stops guessing. - healthcheck_local_backend skipped custom endpoints (it gated on the raw prefix, which is "custom@venice"), so they failed deep in a run instead of falling back. It now preflights them with the Bearer key. - _infer_tool_filter_mode forced "full" for every custom: model; "custom:" says nothing about capability, so it falls through to the size heuristics. - The web model picker dropped any key ending in ":", which is not the same as "is a sentinel" — it was silently hiding Ollama and LM Studio. Sentinels are now marked server-side and filtered from /chat/options. UX - Telegram: saved-endpoint list, add/manage/forget, error keyboards with Retry and a context-appropriate secondary action, a "No API key needed" button, model search, current model floated to page 1, and content-hashed pick tokens instead of list indices. - Web: Settings -> LLM Endpoints for add/validate/forget, and a two-level chat model picker that lazy-fetches each endpoint's models. API keys are never serialized to the browser, only a has_key flag. Hardening - Base-url candidates are probed concurrently at 8s instead of sequentially at 15s each. - /models entries that are embeddings, rerankers, TTS or image models are filtered out; picking one used to succeed here and fail opaquely at prompt time. Note object="model" is uninformative (OpenAI stamps it on everything) and falls through to id heuristics. - Hostnames are shape-checked, since urlparse accepts "not a url at all". - Link-local/metadata addresses are always refused; loopback and RFC1918 stay allowed by default (local model servers are the common case) and can be refused via CUSTOM_LLM_BLOCK_PRIVATE_URLS on shared deployments. - Model labels truncate through the middle, so the distinguishing tail of ids like ...-Instruct-Turbo-Free survives. Tests: 14 -> 34 cases in tests/test_custom_provider.py; 231 pass overall. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolves three conflicts, all in favour of main's newer work with this
branch's custom-endpoint support folded in rather than either side winning.
agent_builder/SKILL.md — main added get_available_models and a real model
recommendation heuristic, which is strictly better than this branch's "omit
agent_key and inherit". Took main's version wholesale and added custom
endpoints to the tool's reported sources. Also corrected main's stale
custom-endpoint bullet: "openai:<model> + model_base_url" is superseded by
"custom@<endpoint>:<model-id>", which resolves URL and key from the user's
saved endpoints at run time.
api.ts / ChatPanel.tsx — both sides added a two-level model picker (main for
OpenRouter and local servers, this branch for custom endpoints). Took main's
AgentDropdown, which is the richer implementation and shared by both call
sites, and generalized its single `showModels` boolean into a `submenu`
discriminated union so OpenRouter and each saved endpoint are peer
drill-downs.
Also reworks how sentinels reach the web client. This branch had stripped
them from /chat/options, which would have broken main's OpenRouter submenu
(it needs the "openrouter:" entry present to render the drill-down). Instead
every entry now carries an explicit `picker` flag. That fixes the underlying
problem both versions had: the client was inferring "is a picker" from
`key.endsWith(":")`, which also matches "ollama:" and "lmstudio:" — real,
startable keys — so those were being silently hidden from the dashboard.
get_available_models now reports custom_endpoints, re-validating each one so
a dead endpoint surfaces as reachable:false instead of being recommended and
failing on the first consult.
Full suite: 271 passed (the whole suite runs again — main repaired the
test_risk_drawdown import that previously broke collection).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The merge changed how sentinels reach the web client: they are now sent with an explicit `picker` flag instead of being filtered out server-side, so main's OpenRouter submenu still renders. Nothing guarded that contract, and the failure mode is silent — inferring "is a picker" from the key ending in ":" also matches "ollama:"/"lmstudio:", which are startable keys, and quietly drops them from the dashboard model picker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Prompts like "Enter model manually" arm a flag that makes the next plain
message mean something other than "talk to the agent". The callback router
never cleared those flags, and Telegram keyboards from earlier messages stay
tappable forever — so arming manual entry and then picking a different LLM
(or a model from the picker) changed the LLM but left
_openrouter_typing_slug set. The next real task was parsed as a model slug
("No tool-calling OpenRouter model matches 'create trading agent'") and the
failure path re-armed itself, blocking agent chat until the user knew to
cancel.
Clear every armed mode in agent_callback_handler before dispatch: a tap is a
fresh intent, and the handlers that want an armed mode set their own flag
afterwards. Only the armed flags go — pending values like
_openrouter_typed_slug survive, so confirm/retry steps still work. This also
closes the symmetric cross-flow holes (the custom-endpoint flow only
disarmed its own keys) and covers agent_compact_custom.
Also give the slug prompt and its "no model matches" retry a Cancel /
Back-to-list keyboard, mirroring the custom-endpoint flow, so a mistyped
slug can't trap the user in a loop with /cancel as the only exit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
|
@carlito-hummingbot try opencode:go with this PR |
carlito-hummingbot
pushed a commit
to carlito-hummingbot/condor
that referenced
this pull request
Jul 29, 2026
…m for opencode-go model Switches agent_key from 'opencode-go:deepseek-v4-flash' to 'custom@opencode:deepseek-v4-flash' so the agent runs on opencode-go via PR hummingbot#175's named custom OpenAI-compatible endpoint (no PR hummingbot#170 needed). Documents endpoint setup: Settings -> LLM Endpoints, or CUSTOM_LLM_BASE_URL/CUSTOM_LLM_API_KEY in .env for headless condor-bot.service deploys.
This PR confirmed and tested to work with opencode-go. I'm closing PR #170. |
cardosofede
approved these changes
Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes custom OpenAI-compatible endpoints (Venice AI, Together, Fireworks, a local vLLM/LM Studio) work as a first-class model source: configure once, use from Telegram chat, the web dashboard, and agent consults/loops.
Why
The initial implementation only worked in Telegram chat. Endpoints were stored in raw
user_data, so the web dashboard couldn't see them, andhandlers/agents/session.pywas the only caller in the codebase that resolved an endpoint's URL and key. That meant acustom:agent key died withNo base URL configuredon every other path — which is how a user running Venice ended up watching a trading-agent consult cascade through claude-code → ollama → gemini and fail, never trying the endpoint they had configured.What changed
Storage
Endpoints move into
condor/preferences.pyas a named list synced toconfig.yml, so Telegram and web share one view. Legacyuser_data["custom_llm"]migrates on first read.Agent keys become
custom@<endpoint>:<model-id>. The@segment keeps the model id recoverable with a single partition even when it contains colons and slashes (custom@together:meta-llama/Llama-3.3-70B:free).Bugs fixed
/canceldid nothing~filters.COMMANDand noCommandHandlerexisted, so the documented escape hatch matched nothing — while an invalid URL re-armed the prompt, trapping the user in a loopconsult/delegate/enginebuiltPydanticAIClientwith nobase_url/api_keyagent_keyfrom theagent_builderskill's examplescustom@venice, so failures surfaced deep in a run instead of falling backtool_filter_modeforcedfullcustom:says nothing about capability — a 4B model on a local vLLM got the full toolset:, which is not the same as "is a sentinel"UX
Telegram — saved-endpoint list, add/manage/forget, error keyboards with Retry plus a context-appropriate secondary action, a "No API key needed" button, model search, current model floated to page 1, and content-hashed pick tokens instead of list indices (indices silently resolve to the wrong model if the list is refetched between render and tap).
Web —
Settings → LLM Endpointsfor add/validate/forget, and a two-level chat model picker that lazy-fetches each endpoint's models. API keys are never serialized to the browser, only ahas_keyflag.Hardening
/models— picking one used to succeed and then fail opaquely at prompt timeurlparsehappily accepts"not a url at all")CUSTOM_LLM_BLOCK_PRIVATE_URLSon shared deployments...-Instruct-Turbo-FreesurvivesReview notes
object: "model"is uninformative. OpenAI stamps it on embeddings, Whisper and DALL·E alike, so it's treated as "not declared" and falls through to id heuristics. Worth a look in_is_chat_model.set_active_llm()mirrorsagent_llmintoconfig.ymlbecause the MCP subprocess can't read the PTB pickle. All nine write sites route through it, and_reclaim_default_agentbackfills on read so existing users don't have to re-pick.agent_key: auto) is the better end state and is being discussed separately; it's deliberately not in this PR.consulthas no override chain (agent.agent_keyonly) whileengineresolvesconfig > strategy > agent. Pre-existing asymmetry, left alone here.agents/is committed and the shipped agents hardcodeclaude-acp:sonnet/claude-code:sonnet. Not touched in this PR, but it's the same failure mode for any user without those CLIs.Testing
tests/test_custom_provider.py: 14 → 34 cases (URL normalization and SSRF guards, model filtering, key composition, endpoint storage and migration, picker helpers, endpoint resolution)tsc --noEmitclean; no new lint findingstests/trading_agent/test_risk_drawdown.pyfails to collect onmain(imports a missingcondor.trading_agent) — pre-existing and unrelated, so it was excluded rather than touched.🤖 Generated with Claude Code