feat(providers): add OpenRouter, Kilo Gateway, and Ollama Cloud capabilities - #26
feat(providers): add OpenRouter, Kilo Gateway, and Ollama Cloud capabilities#26kdegeek wants to merge 8 commits into
Conversation
MarlBurroW
left a comment
There was a problem hiding this comment.
Hey @kdegeek, thanks a lot for this contribution! Three new providers plus OpenRouter embeddings and images in one PR is a serious amount of work, and the core of it is genuinely good. The streaming and parsing code follows the house patterns, the tests assert real request/response shapes (we probed the live APIs and your payload shapes match field for field), and the config/shell test hermeticity fixes are welcome on their own.
We did a deep review pass: test-merged the branch onto current main (one trivial README conflict), ran typecheck plus the full suite on the merged tree (4109 tests, all green), and booted the merged app to confirm the three providers show up in the catalog with the right capabilities. So the foundation is solid. There is one functional blocker and a few items from the project's provider checklist that need a round before this can land.
Blocker
- "Test connection" accepts any key on both new LLM providers.
kilo.tsprobesGET /modelsandollama.tsprobesGET /api/tags, and both endpoints are anonymous. We verified live that both return 200 withAuthorization: Bearer garbage-key-123. SincetestProviderConnectiondispatches to the LLM capability first for a multi-capability provider, the Ollama searchauthenticate(which does validate,/web_search401s without a key) is never reached. Net effect: a typoed key passes setup, capabilities are granted, and the user only finds out at first chat/search. Both chat endpoints properly 401 on a bad key (also verified live), so a minimal chat probe, or any authenticated endpoint, fixes this cheaply.
Should fix
- Ollama model enrichment never fires.
resolveFromModelsDev('ollama', ...)looks up the models.dev idollama, but the bundled snapshot's key isollama-cloud. One line inPROVIDER_ID_MAP(src/server/llm/metadata/models-dev.ts),ollama: 'ollama-cloud', fixes it. Without it every Ollama model ships with no context window, no vision flag and nothinking, so thethinkparam is never sent and reasoning models (gpt-oss, deepseek-v3.1) never reason. Kilo is fine (snapshot keykilomatches). - Kilo streaming drops tool calls without an id and regresses tolerant argument parsing.
if (!state.id || !state.name) continuesilently discards a streamed tool call with no id;openrouter.tssynthesizescall_${idx}instead. Please also useparseToolArgumentsfrom@/server/llm/core/parse-tool-argsrather than strictJSON.parsewith a{ _raw }fallback: the_rawobject gets passed to the tool as its args and breaks execution silently. Every other OpenAI-compatible provider uses the tolerant parser. - Provider checklist step 4: add
CONFIGURATOR_MODEL_PREFERENCESentries forkiloandollamainsrc/shared/constants.tsso onboarding seeds on a strong, tool-reliable model. Kilo lists 337 models, so "first listed" is arbitrary and may be a weak or free tier. - Provider checklist step 6: add the chips to
site/src/components/Providers.astro(DeepSeek/MiniMax/Kimi are there from the same flow). - OpenRouter images: the
sizeparam appears unsupported by the live catalog (OpenAI models usequality, Gemini usesresolution/aspect_ratio), so it should be mapped or dropped. ThepricingFrom(endpoints)path is also dead code inlistModels(endpoints are never passed), and the livegpt-image-1endpoint billsunit: "token", not theunit === 'image'the filter requires. Wire it or remove it. - Ollama search warnings are duplicated. The provider pushes five "does not support X" warnings that the host already emits from the (correctly empty)
capabilitiesflags, so agents see each warning twice. Only warn on conditions the host cannot know (see searxng/perplexity for the precedent). - telegram.test.ts:
expect(shouldUsePolling()).toBe(!config.publicUrl?.startsWith('https://'))mirrors the implementation clause, so it cannot fail. Making the config load hermetic is the better fix, and your ownconfig.test.tschanges in this PR show exactly how.
Minor notes (non-blocking)
kilo.ts: unused importChatCompletionToolMessageParam.kilo.tsmodels some payload fields the live API never returns (reasoning.supported_efforts,features,capabilities; live/modelsonly exposessupported_parameters). Harmless fallbacks fire, but it is guess-modeling. Same flavor for the image response union hedging inimage/openrouter.ts.ollama.ts: the tool-use id is the function name, so two parallel calls to the same tool share an id; a counter suffix would fix it. The trailing buffer flush handlescontent/donebut dropsthinkingandtool_callsin a final partial line. Andthinkpassesmax/xhighthrough unmapped (Ollama accepts only boolean/low/medium/high); unreachable today, becomes live once item 2 is fixed.- Kilo
apiKeyUrldiffers between provider metadata (https://kilo.ai) and the docs table (https://kilo.ai/gateway); neither is a keys page, pick one. - The new "Built-in AI providers" section in
configuration.mdduplicatesproviders/supported.mdinside the env-vars page. - The idempotency guard added in
llm/register.tsseems unneeded (bothbeforeAllcallers pass without it) and is inconsistent with the embedding/image/search registries, which still throw on double registration.
Also, the branch is 104 commits behind main; a rebase leaves only the one README conflict and we already confirmed it merges green.
Happy to help with any of these. Thanks again, this is a valuable expansion of the provider surface, and items 1 to 3 are each a small diff away from mergeable.
…ool calls & Ollama enrichment Address the review blockers on PR #26: - Kilo and Ollama "test connection" accepted any key: both probed anonymous endpoints (GET /models, GET /api/tags) that return 200 for a typoed key. Probe the authenticated POST chat endpoint with a non-existent model instead (auth is checked before the model, so 401/403 = bad key, and a valid key hits a model error without spending generation tokens). - Kilo streaming dropped tool calls with no id and used strict JSON.parse with a { _raw } fallback that reaches the tool as broken args. Synthesize call_<idx> ids and use the shared tolerant parseToolArguments, matching openrouter.ts. - Ollama model enrichment never fired: models.dev keys Ollama Cloud as `ollama-cloud`, so map `ollama` -> `ollama-cloud` in PROVIDER_ID_MAP. Restores context windows, vision and `thinking` flags for Ollama models. Updated Kilo auth tests to assert the chat-probe behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #6 OpenRouter images: stop forwarding the generic top-level `size` param (OpenRouter image models use model-specific params — quality / resolution / aspect_ratio — surfaced via describeModel and passed through request.params). Remove the dead per-image pricing path: mapModel was always called without endpoints so pricingFrom() never populated pricing, and its `unit === 'image'` filter never matched token-billed models like gpt-image-1. - #4 Seed the configurator (Queenie) on a strong, tool-reliable model for the new providers: add CONFIGURATOR_MODEL_PREFERENCES for `kilo` and `ollama`. - #5 Add built-in provider chips to the marketing site (Providers.astro): Kilo Gateway + Ollama Cloud (LLM), OpenRouter (image, embeddings), Ollama Cloud (search). - #7 Ollama web search no longer emits capability-mismatch warnings; the host (search-tools.ts) already derives them from the empty `capabilities`, so the provider was surfacing each warning to agents twice. - #8 Make the telegram shouldUsePolling() tests hermetic: load the module in a subprocess with a controlled PUBLIC_URL (the pattern config.test.ts uses) instead of the previous tautological / ambient-dependent assertions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- kilo.ts: drop the unused ChatCompletionToolMessageParam import. - ollama.ts streaming: give parallel calls to the same tool distinct ids (Ollama keys a tool call only by function name) via a running counter, and route the trailing-buffer flush through the same emitter so a final partial line no longer drops `thinking` / `tool_calls` (it handled only content/done). - ollama.ts: clamp the `think` effort to Ollama's accepted set — minimal → false, max/xhigh → high — now that model enrichment makes reasoning efforts reachable. - provider-metadata.ts: align Kilo `apiKeyUrl` with the docs (https://kilo.ai/gateway). - configuration.md: replace the duplicated built-in-providers list with a pointer to the authoritative Supported providers page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ollama.ts (regression from the unique tool-call-id suffix): Ollama correlates tool results by function name, but messagesToOllama was passing the now-suffixed toolUseId as `tool_name`. Map each tool-use id back to its original name via a toolNameById pre-pass (mirrors messagesToGemini). Adds a regression test. - search/ollama.ts: resolve getBaseUrl/getApiKey before the fetch try block so a missing key fails as AuthError instead of a misleading NetworkError. Skipped (with reasons): - embedding/openrouter.ts "add fetch timeouts": no provider in the codebase uses fetch timeouts on setup probes (not even the openrouter LLM provider); adding only here would be inconsistent and is out of scope for this PR. - kilo.ts messagesToOpenAI ordering: byte-identical to the openrouter/xai house pattern this PR was asked to mirror; any concern is codebase-wide, not Kilo-specific. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
f401d3b to
84f3c0b
Compare
|
Thanks for the incredibly thorough review, @MarlBurroW — the live-API verification and the checklist references made this easy to act on. I've rebased onto current main (only the one README conflict you predicted; now mergeable) and addressed everything: Blocker
Should-fix Minor notes — swept the unused Kilo import; Ollama now gives parallel calls to the same tool distinct ids, the trailing-buffer flush no longer drops I also ran a CodeRabbit pass, which caught one bug the id-uniqueness change introduced: Ollama correlates tool results by function name, so Validation on the rebased tree: |
There was a problem hiding this comment.
Pull request overview
Adds first-class support for additional built-in provider capabilities across the server runtime, UI metadata, and documentation—specifically OpenRouter embeddings/images, plus new Kilo Gateway and Ollama Cloud (LLM + search) providers.
Changes:
- Introduces new provider implementations + registration for Kilo Gateway (LLM), Ollama Cloud (LLM + web search), and OpenRouter (embeddings + images).
- Updates provider metadata/constants to advertise new capabilities and provide model-preference hints.
- Refreshes docs/site copy and hardens a couple tests (Telegram polling env isolation, tmp-path realpathing).
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/shared/provider-metadata.ts | Expands OpenRouter capabilities; adds Kilo + Ollama Cloud provider meta. |
| src/shared/constants.ts | Adds model preference seeds for Kilo and Ollama. |
| src/server/tools/shell-tools.test.ts | Makes tmp workspace path robust via realpathSync('/tmp'). |
| src/server/llm/search/register.ts | Registers the new Ollama search provider. |
| src/server/llm/search/ollama.ts | Implements Ollama Cloud result-only web search provider. |
| src/server/llm/search/ollama.test.ts | Tests Ollama search request/response mapping + clamping. |
| src/server/llm/metadata/models-dev.ts | Maps ollama provider type to ollama-cloud models.dev key. |
| src/server/llm/llm/register.ts | Registers new Kilo and Ollama Cloud LLM providers. |
| src/server/llm/llm/ollama.ts | Implements Ollama Cloud LLM provider (tags + streaming chat). |
| src/server/llm/llm/ollama.test.ts | Tests Ollama model mapping, tool-result correlation, tags call. |
| src/server/llm/llm/kilo.ts | Implements Kilo Gateway LLM provider via OpenAI-compatible API. |
| src/server/llm/llm/kilo.test.ts | Tests Kilo auth probing, model listing error mapping, model mapping. |
| src/server/llm/image/register.ts | Registers OpenRouter image provider. |
| src/server/llm/image/openrouter.ts | Implements OpenRouter image model listing/describe/generate. |
| src/server/llm/image/openrouter.test.ts | Tests OpenRouter image mapping, endpoint path, param mapping, generate. |
| src/server/llm/embedding/register.ts | Registers OpenRouter embedding provider. |
| src/server/llm/embedding/openrouter.ts | Implements OpenRouter embeddings with dedicated model discovery endpoint. |
| src/server/llm/embedding/openrouter.test.ts | Tests embedding model mapping and /embeddings/models usage. |
| src/server/channels/telegram.test.ts | Makes polling logic tests hermetic via subprocess env control. |
| site/src/components/Providers.astro | Updates site provider chips to include Kilo + Ollama, and OpenRouter capabilities. |
| README.md | Updates built-in provider capability listing. |
| docs-site/src/content/docs/providers/supported.md | Updates supported-provider tables + narrative notes. |
| docs-site/src/content/docs/getting-started/configuration.md | Adds clarification that provider creds are configured via UI/vault. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Spread request.params before the provider-controlled fields so a caller can supply model-specific options (quality, resolution, …) but cannot override the model id / prompt or force n > 1 (extra cost). - Validate the upstream image URL: only dereference http(s), rejecting file:, data:, and other schemes from a bad/compromised response. Both covered by new tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the automated Copilot review on
Both covered by new tests; full suite (4007) + typecheck + build green. |
|
Testing report from Hivekeep edge + this PR
Two bugs found during live avatar generation through OpenRouter (Nano Banana 2 Lite, img2img mode): 1. The provider maps image inputs to raw const inputReferences = (request.imageInputs ?? [])
.slice(0, Math.max(0, maxRefs))
.map(dataUrlFor)OpenRouter's Fix: const inputReferences = (request.imageInputs ?? [])
.slice(0, Math.max(0, maxRefs))
.map(input => ({ url: dataUrlFor(input) }))2. Empty prompt fallback — When the LLM prompt-writer (used to derive per-agent character prompts from identity) returns empty text — observed with The Fix (add empty-check before the return): const text = avatarResult.text.trim()
if (!text) return fallbackAvatarPrompt(agent, mode, subject, style)
return textTesting: Both patches applied on a live Hivekeep instance. 8 agent avatars generated successfully through OpenRouter (Nano Banana 2 Lite, |
Summary
Validation
Previously passed before this split PR branch was prepared:
Additional context from prior verification: