Skip to content

feat(providers): add OpenRouter, Kilo Gateway, and Ollama Cloud capabilities - #26

Open
kdegeek wants to merge 8 commits into
MarlBurroW:mainfrom
kdegeek:pr/providers-openrouter-kilo-ollama
Open

feat(providers): add OpenRouter, Kilo Gateway, and Ollama Cloud capabilities#26
kdegeek wants to merge 8 commits into
MarlBurroW:mainfrom
kdegeek:pr/providers-openrouter-kilo-ollama

Conversation

@kdegeek

@kdegeek kdegeek commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add OpenRouter embedding and image provider support alongside the existing OpenRouter LLM provider.
  • Fix OpenRouter embedding model discovery to use GET /api/v1/embeddings/models instead of filtering the general model catalogue.
  • Add Kilo Gateway LLM provider support.
  • Add Ollama Cloud LLM provider and Ollama web-search provider support.
  • Update provider metadata and docs for OpenRouter, Kilo Gateway, and Ollama Cloud capabilities.
  • Preserve the previously prepared Codex/OAuth macOS HOME-path and model-cache fixes that were part of the broad provider branch.

Validation

Previously passed before this split PR branch was prepared:

  • bun test src/server/llm/embedding/openrouter.test.ts src/server/llm/image/openrouter.test.ts src/server/llm/llm/ollama.test.ts src/server/llm/search/ollama.test.ts src/server/llm/llm/kilo.test.ts — PASS (20 tests).
  • bun test src/server/llm/llm/openrouter.test.ts — PASS (22 tests).
  • bun run typecheck — PASS.
  • bun run build — PASS (existing Vite chunk-size warnings only).
  • git diff --check — PASS.
  • Final provider PR prep also ran targeted provider tests, typecheck, build, git diff --check, and full bun run test successfully before committing the provider work.

Additional context from prior verification:

  • Public OpenRouter probes confirmed /api/v1/embeddings/models and /api/v1/images/models return model data.
  • Ollama docs/probes confirmed POST https://ollama.com/api/web_search, max_results default 5/max 10, and result shape {title,url,content}.
  • Kilo auth was hardened to avoid a brittle hardcoded chat-model probe and instead validate with a non-empty key plus /models probing behavior.

@kdegeek
kdegeek marked this pull request as ready for review June 30, 2026 13:13
@kdegeek
kdegeek requested a review from MarlBurroW as a code owner June 30, 2026 13:13

@MarlBurroW MarlBurroW left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. "Test connection" accepts any key on both new LLM providers. kilo.ts probes GET /models and ollama.ts probes GET /api/tags, and both endpoints are anonymous. We verified live that both return 200 with Authorization: Bearer garbage-key-123. Since testProviderConnection dispatches to the LLM capability first for a multi-capability provider, the Ollama search authenticate (which does validate, /web_search 401s 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

  1. Ollama model enrichment never fires. resolveFromModelsDev('ollama', ...) looks up the models.dev id ollama, but the bundled snapshot's key is ollama-cloud. One line in PROVIDER_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 no thinking, so the think param is never sent and reasoning models (gpt-oss, deepseek-v3.1) never reason. Kilo is fine (snapshot key kilo matches).
  2. Kilo streaming drops tool calls without an id and regresses tolerant argument parsing. if (!state.id || !state.name) continue silently discards a streamed tool call with no id; openrouter.ts synthesizes call_${idx} instead. Please also use parseToolArguments from @/server/llm/core/parse-tool-args rather than strict JSON.parse with a { _raw } fallback: the _raw object gets passed to the tool as its args and breaks execution silently. Every other OpenAI-compatible provider uses the tolerant parser.
  3. Provider checklist step 4: add CONFIGURATOR_MODEL_PREFERENCES entries for kilo and ollama in src/shared/constants.ts so 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.
  4. Provider checklist step 6: add the chips to site/src/components/Providers.astro (DeepSeek/MiniMax/Kimi are there from the same flow).
  5. OpenRouter images: the size param appears unsupported by the live catalog (OpenAI models use quality, Gemini uses resolution/aspect_ratio), so it should be mapped or dropped. The pricingFrom(endpoints) path is also dead code in listModels (endpoints are never passed), and the live gpt-image-1 endpoint bills unit: "token", not the unit === 'image' the filter requires. Wire it or remove it.
  6. Ollama search warnings are duplicated. The provider pushes five "does not support X" warnings that the host already emits from the (correctly empty) capabilities flags, so agents see each warning twice. Only warn on conditions the host cannot know (see searxng/perplexity for the precedent).
  7. 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 own config.test.ts changes in this PR show exactly how.
Minor notes (non-blocking)
  • kilo.ts: unused import ChatCompletionToolMessageParam.
  • kilo.ts models some payload fields the live API never returns (reasoning.supported_efforts, features, capabilities; live /models only exposes supported_parameters). Harmless fallbacks fire, but it is guess-modeling. Same flavor for the image response union hedging in image/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 handles content/done but drops thinking and tool_calls in a final partial line. And think passes max/xhigh through unmapped (Ollama accepts only boolean/low/medium/high); unreachable today, becomes live once item 2 is fixed.
  • Kilo apiKeyUrl differs 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.md duplicates providers/supported.md inside the env-vars page.
  • The idempotency guard added in llm/register.ts seems unneeded (both beforeAll callers 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.

kdegeek and others added 7 commits July 6, 2026 13:05
…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>
@kdegeek
kdegeek force-pushed the pr/providers-openrouter-kilo-ollama branch from f401d3b to 84f3c0b Compare July 6, 2026 19:03
Copilot AI review requested due to automatic review settings July 6, 2026 19:03
@kdegeek

kdegeek commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

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

  1. Test-connection now validates the key. Both providers previously probed anonymous endpoints (GET /models, GET /api/tags) that 200 on any key. They now probe the authenticated chat endpoint with a deliberately non-existent model. I re-verified live: a bad key returns 401 even on real, existing paid models (claude-sonnet-5, gpt-5.5 on Kilo; plain 401 Unauthorized on Ollama regardless of model), so the 401 is auth-driven — a valid key passes the gate and the bogus model yields a non-auth model error, spending zero generation tokens. (Kilo's gateway only accepts /chat/completions — no key-info endpoint like OpenRouter's /key — so a chat probe is the only option.)

Should-fix
2. Ollama enrichment: added ollama: 'ollama-cloud' to PROVIDER_ID_MAP.
3. Kilo tool calls: now synthesize call_<idx> ids and use the shared parseToolArguments, matching openrouter.ts.
4. Added CONFIGURATOR_MODEL_PREFERENCES for kilo and ollama (seeded from the live catalogs).
5. Added the provider chips to Providers.astro (LLM, image, embeddings, search groups).
6. OpenRouter images: dropped the top-level size (each model's real param — quality/resolution/aspect_ratio — already flows via describeModelrequest.params) and removed the dead pricingFrom path (it was always called without endpoints and the unit === 'image' filter never matched token-billed gpt-image-1).
7. Ollama search no longer emits capability warnings — the host derives them from the empty capabilities, so agents were seeing each twice.
8. Made the shouldUsePolling telegram tests hermetic via a subprocess with a controlled PUBLIC_URL (the pattern from config.test.ts).

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 thinking/tool_calls, and think is clamped to Ollama's set (max/xhighhigh); aligned Kilo apiKeyUrl to https://kilo.ai/gateway; and replaced the duplicated provider list in configuration.md with a pointer to the supported-providers page. (The register.ts idempotency guard is already on main post-rebase, so no change there.)

I also ran a CodeRabbit pass, which caught one bug the id-uniqueness change introduced: Ollama correlates tool results by function name, so messagesToOllama now maps the suffixed id back to the original name (mirroring messagesToGemini), with a regression test.

Validation on the rebased tree: bun run typecheck, full bun run test (4005 pass), bun run build, and git diff --check all green.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/server/llm/image/openrouter.ts
Comment thread src/server/llm/image/openrouter.ts
- 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>
@kdegeek

kdegeek commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the automated Copilot review on image/openrouter.ts in the latest push:

  • Param override: request.params is now spread before the provider-controlled fields, so callers can supply model-specific options (quality, resolution, …) but can't override the model id / prompt or force n > 1.
  • Image URL fetch: the upstream item.url is now validated to be http(s) before dereferencing, rejecting file: / data: / other schemes.

Both covered by new tests; full suite (4007) + typecheck + build green.

@smitimus

Copy link
Copy Markdown

Testing report from Hivekeep edge + this PR

Disclosure: these issues were found and patched by Queenie (Hivekeep's AI onboarding/config Agent), not by the human submitting this report. I'm posting on her behalf after live-testing the PR changes on my instance. This was tested on the latest main commit (detached at ccea62d2) merged with this PR's branch, not an official release.

Two bugs found during live avatar generation through OpenRouter (Nano Banana 2 Lite, img2img mode):


1. input_references format — src/server/llm/image/openrouter.ts line 291

The provider maps image inputs to raw data: URL strings:

const inputReferences = (request.imageInputs ?? [])
  .slice(0, Math.max(0, maxRefs))
  .map(dataUrlFor)

OpenRouter's /v1/images endpoint expects input_references entries as objects: { url: "data:..." }. Sending bare strings causes:

Invalid input: expected object, received string

Fix:

const inputReferences = (request.imageInputs ?? [])
  .slice(0, Math.max(0, maxRefs))
  .map(input => ({ url: dataUrlFor(input) }))

2. Empty prompt fallback — src/server/services/image-generation.ts line 553

When the LLM prompt-writer (used to derive per-agent character prompts from identity) returns empty text — observed with deepseek-v4-flash and the very long avatar system prompt — the empty string passes straight through to OpenRouter:

Too small: expected string to have >=1 characters

The fallbackAvatarPrompt() function exists in the same file but was only called when pickAnyLLMModel() returned null — not when the LLM responded with empty output.

Fix (add empty-check before the return):

const text = avatarResult.text.trim()
if (!text) return fallbackAvatarPrompt(agent, mode, subject, style)
return text

Testing: Both patches applied on a live Hivekeep instance. 8 agent avatars generated successfully through OpenRouter (Nano Banana 2 Lite, google/gemini-3.1-flash-lite-image) with img2img base reference enabled. No further errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants