Skip to content

✨ Register local LLM providers (Ollama, llama.cpp, LocalAI, vLLM, LM Studio, RHAIIS) in agent selector#8248

Merged
clubanderson merged 2 commits intomainfrom
feat/local-llm-agent-providers
Apr 16, 2026
Merged

✨ Register local LLM providers (Ollama, llama.cpp, LocalAI, vLLM, LM Studio, RHAIIS) in agent selector#8248
clubanderson merged 2 commits intomainfrom
feat/local-llm-agent-providers

Conversation

@clubanderson
Copy link
Copy Markdown
Collaborator

Summary

Adds Ollama, llama.cpp, LocalAI, vLLM, LM Studio and Red Hat AI Inference Server as chat-only providers in the agent registry, surfaces them in the agent selector dropdown with icons + install-mission links, and flips on the existing Groq / OpenRouter / Open WebUI providers that had been sitting unregistered.

Chat-vs-missions is respected: local LLM runners report CapabilityChat only, so promoteExecutingDefault() keeps a tool-capable CLI agent as the default for missions. The dropdown honestly reflects which runners are reachable — a runner only shows as available when its URL env var is set (or a loopback default applies for the workstation-first runners).

Why

User feedback from Manuela (2026-04-15): "the console uses external LLMs. Although it supports self-hosting, more comprehensive documentation on using local LLMs would be helpful for users in isolated or high-security environments."

Pairs with kubestellar/console-kb#2028 which adds the corresponding install missions, and an upcoming kubestellar/docs page on local-LLM strategy.

What changed

Backend

  • New pkg/agent/provider_local_openai_compat.go defines a generic LocalOpenAICompatProvider parameterized by (name, displayName, urlEnvVar, defaultURL, chatPath) with one factory per runner.
  • Each provider reads its own URL env var: OLLAMA_URL, LLAMACPP_URL, LOCALAI_URL, VLLM_URL, LM_STUDIO_URL, RHAIIS_URL. Loopback defaults only for Ollama (127.0.0.1:11434) and LM Studio (127.0.0.1:1234).
  • Sentinel placeholder key seeding for unauthenticated runners so the OpenAI-compat helper's Authorization header is well formed. Real keys from env or ~/.kc/config.yaml always win.
  • Registration in InitializeProviders happens AFTER tool-capable CLI agents so promoteExecutingDefault() still picks a mission-capable default.
  • The architectural comment at registry.go:303 is expanded to explain the new chat-vs-missions split and why vendor API agents remain unregistered.
  • config.go gains env-var mappings for each new provider key.

Frontend

  • web/src/types/agent.ts adds ollama, llamacpp, localai, vllm, lm-studio, rhaiis to the AgentProvider union.
  • AgentIcon.tsx adds icon cases for each.
  • agentSelectorUtils.ts adds LOCAL_LLM_INSTALL_MISSIONS mapping provider name → install mission ID (install-ollama, install-llama-cpp, install-localai, install-vllm, install-lm-studio, install-rhaiis, install-open-webui, install-claude-desktop), and enriches unavailable local-LLM agents with installMissionId so the dropdown surfaces the install mission path when the runner is not yet reachable. Mirrors the kagent/kagenti pattern.

Test plan

  • go build ./... passes
  • go test ./pkg/agent/... passes (4.2s)
  • npm run build passes all post-build safety checks
  • npm run test -- src/components/agent/AgentSelector.test.tsx — 17 tests pass
  • Manual: set OLLAMA_URL=http://127.0.0.1:11434 on a workstation with Ollama running, restart kc-agent, confirm Ollama appears in the dropdown as available
  • Manual: without any URLs set, confirm all six local-LLM runners appear in the dropdown marked unavailable, each linking to its install mission

🤖 Generated with Claude Code

Adds Ollama, llama.cpp, LocalAI, vLLM, LM Studio and Red Hat AI
Inference Server as CapabilityChat-only providers in the agent
registry, and also flips on the existing Groq / OpenRouter / Open WebUI
providers that had been sitting unregistered in the tree.

Backend
-------
- New pkg/agent/provider_local_openai_compat.go defines a generic
  LocalOpenAICompatProvider parameterized by (name, displayName,
  urlEnvVar, defaultURL, chatPath) and instantiates one factory per
  runner. Each provider reads its own URL env var (OLLAMA_URL,
  LLAMACPP_URL, LOCALAI_URL, VLLM_URL, LM_STUDIO_URL, RHAIIS_URL) with
  loopback defaults only for the workstation-first runners (Ollama at
  127.0.0.1:11434, LM Studio at 127.0.0.1:1234). IsAvailable() returns
  true only when a URL is resolvable, so the dropdown honestly reflects
  which runners the operator has configured.
- A sentinel placeholder API key is seeded for unauthenticated local
  runners so the shared OpenAI-compatible helper's Authorization header
  is well formed. Real keys from env or ~/.kc/config.yaml always win.
- Registration lives after the tool-capable CLI agents in
  InitializeProviders so promoteExecutingDefault() still picks a
  mission-capable agent as the default. The architectural comment at
  registry.go:303 is expanded to explain the chat-vs-missions split
  and why vendor API agents remain unregistered.
- config.go gains env-var mappings for each new provider key so the
  ConfigManager honors GetAPIKey / GetModel for them.

Frontend
--------
- web/src/types/agent.ts adds ollama, llamacpp, localai, vllm,
  lm-studio and rhaiis to the AgentProvider union.
- AgentIcon.tsx adds icon cases for each new provider.
- agentSelectorUtils.ts adds LOCAL_LLM_INSTALL_MISSIONS mapping
  provider name -> install mission ID, and enriches unavailable local
  LLM agents with installMissionId so the dropdown surfaces the
  install mission path when the runner is not yet reachable. Matches
  the kagent/kagenti pattern that already ships install links.

Validation
----------
- go build ./... passes.
- go test ./pkg/agent/... passes (4.2s).
- npm run build passes all post-build safety checks.
- Agent selector tests (17 tests) still pass.

Signed-off-by: Andrew Anderson <[email protected]>
Copilot AI review requested due to automatic review settings April 15, 2026 23:52
@kubestellar-prow
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign mikespreitzer for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubestellar-prow kubestellar-prow Bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Apr 15, 2026
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 15, 2026

Deploy Preview for kubestellarconsole ready!

Name Link
🔨 Latest commit 7a18c29
🔍 Latest deploy log https://app.netlify.com/projects/kubestellarconsole/deploys/69e026ebcea09900087dfd28
😎 Deploy Preview https://deploy-preview-8248.console-deploy-preview.kubestellar.io
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown
Contributor

👋 Hey @clubanderson — thanks for opening this PR!

🤖 This project is developed exclusively using AI coding assistants.

Please do not attempt to code anything for this project manually.
All contributions should be authored using an AI coding tool such as:

This ensures consistency in code style, architecture patterns, test coverage,
and commit quality across the entire codebase.


This is an automated message.

@kubestellar-prow kubestellar-prow Bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Apr 15, 2026
clubanderson added a commit that referenced this pull request Apr 15, 2026
Updates SECURITY-MODEL.md §3 to reflect #8248, which
registers Ollama, llama.cpp, LocalAI, vLLM, LM Studio, RHAIIS, Groq,
OpenRouter and Open WebUI as chat-only agent providers in
InitializeProviders.

Changes:

- Provider table flips the Registered column from "no" to "yes (chat
  only)" for the nine HTTP providers that are now wired into the agent
  dropdown, and adds rows for the six new local LLM runners with their
  env vars and default URLs.
- Explains the chat-only capability flag and why missions still route
  through the tool-capable CLI agents (registry.go:303 rationale).
- Adds a "Local LLM strategy" subsection that cross-links the
  docs.kubestellar.io local-llm-strategy page and the eight install
  missions on kubestellar/console-kb.
- Replaces the "Planned follow-up" subsection with active recipes for
  each runner — Ollama loopback default, in-cluster Service URLs for
  llama.cpp/LocalAI/vLLM/RHAIIS, LM Studio workstation default, and
  Groq/OpenRouter/Open WebUI gateway overrides. The "# PLANNED —
  not yet wired at runtime" bash comments are removed.

The threat model claims about kubeconfig and credentials staying out
of the request body are unchanged and still authoritative.

Signed-off-by: Andrew Anderson <[email protected]>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds support for selecting local OpenAI-compatible LLM runners (plus enabling previously-unregistered chat-only providers) in the agent registry and Console UI, while keeping mission execution routed to tool-capable CLI agents.

Changes:

  • Backend: Introduces LocalOpenAICompatProvider and registers Ollama/llama.cpp/LocalAI/vLLM/LM Studio/RHAIIS, plus registers Groq/OpenRouter/Open WebUI.
  • Backend: Adds env-var mappings for API key + model configuration for the new providers.
  • Frontend: Extends agent provider types, adds icons, and enriches unavailable agents with install-mission IDs.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
web/src/types/agent.ts Expands AgentProvider union to include new local-runner provider keys.
web/src/components/agent/agentSelectorUtils.ts Adds install-mission mapping and enriches agent entries to surface install paths in the selector.
web/src/components/agent/AgentIcon.tsx Adds icon renderers for the newly-supported local providers.
pkg/agent/registry.go Registers new chat-only local providers and enables Groq/OpenRouter/Open WebUI in provider initialization.
pkg/agent/provider_local_openai_compat.go New generic provider implementation for local OpenAI-compatible runners, with per-runner factories/defaults.
pkg/agent/config.go Adds env-var key/model mappings for the new providers.

Comment on lines +111 to +116
func ensureLocalLLMPlaceholderKey(providerKey string) {
cm := GetConfigManager()
if cm.GetAPIKey(providerKey) == "" {
_ = cm.SetAPIKey(providerKey, localLLMPlaceholderKey)
}
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

ensureLocalLLMPlaceholderKey() persists a sentinel API key by calling ConfigManager.SetAPIKey(), which writes to ~/.kc/config.yaml. This creates surprising disk side effects (and can fail on read-only filesystems) just to satisfy the OpenAI-compat helper’s non-empty Authorization requirement. Prefer avoiding disk writes here (e.g., allow the OpenAI-compat helper to omit Authorization when apiKey is empty for specific providers, or pass an explicit placeholder header without mutating persisted config); at minimum, handle/log the SetAPIKey error and return a clearer failure instead of silently ignoring it.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +69
// IsAvailable returns true when either the env var is set or the provider has
// a non-empty default URL. Local runners typically have no API key — the OpenAI
// helper accepts a sentinel value when CapabilityChat is the only capability,
// so availability is driven by URL reachability rather than credential state.
func (p *LocalOpenAICompatProvider) IsAvailable() bool {
return p.localOpenAICompatBaseURL() != ""
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

IsAvailable() currently returns true whenever localOpenAICompatBaseURL() is non-empty. Because Ollama and LM Studio have non-empty defaultURL values, they will report Available=true even when the runner is not actually running, which affects the agent-selector “available” signal and also makes Registry.HasAvailableProviders() true on fresh installs. Consider either (1) making availability depend on an explicit env var opt-in for these runners, or (2) adding a lightweight reachability probe with a short timeout (and returning false when unreachable) so “available” reflects reality.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +22
export const LOCAL_LLM_INSTALL_MISSIONS: Readonly<Record<string, string>> = Object.freeze({
ollama: 'install-ollama',
llamacpp: 'install-llama-cpp',
localai: 'install-localai',
vllm: 'install-vllm',
'lm-studio': 'install-lm-studio',
rhaiis: 'install-rhaiis',
'open-webui': 'install-open-webui',
'claude-desktop': 'install-claude-desktop',
})
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

LOCAL_LLM_INSTALL_MISSIONS is typed as Record<string, string>, so indexing with an unknown agent name is typed as always returning a string even though at runtime it can be undefined. This defeats type-safety for the enrichment logic. Use a Partial<Record<string, string>> (or Record<string, string | undefined>) so missionId is correctly typed as possibly undefined.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +12
// leaving the Console. Keep this map in sync with the provider keys in
// pkg/agent/provider_local_openai_compat.go.
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

The comment says to keep this map in sync with provider keys in pkg/agent/provider_local_openai_compat.go, but the map also includes entries for open-webui and claude-desktop which are defined/registered elsewhere. Please update the comment to reflect what actually needs to stay in sync (e.g., include other chat-only providers) to avoid future confusion.

Suggested change
// leaving the Console. Keep this map in sync with the provider keys in
// pkg/agent/provider_local_openai_compat.go.
// leaving the Console. Keep this map in sync with the backend-registered
// local/chat-only provider keys that should expose install missions here,
// including the providers in pkg/agent/provider_local_openai_compat.go and
// other chat-only providers such as open-webui and claude-desktop.

Copilot uses AI. Check for mistakes.
…+ list local LLM providers in API keys settings

Two small follow-ups on the local-LLM registration work in the
preceding commit:

1. Groq and OpenRouter key validation were hard-coded to hit the
   public hostnames (api.groq.com, openrouter.ai) even when
   GROQ_BASE_URL or OPENROUTER_BASE_URL redirected the runtime chat
   path at a local runner. That produced a false negative on the
   settings page for operators pointing Groq at a local Ollama or
   an internal corporate gateway. The validator now resolves the
   validation URL from the same base URL resolution helper the Chat
   method uses, so self-hosted and enterprise gateways validate
   correctly.

2. The APIKeySettings modal's PROVIDER_INFO map and
   providerToIconMap now include entries for the six local LLM
   runners (ollama, llamacpp, localai, vllm, lm-studio, rhaiis).
   The placeholder copy tells the operator which URL env var to
   set, since local runners typically run unauthenticated. A
   dedicated Base URL input field per provider is tracked as a
   follow-up issue.

Signed-off-by: Andrew Anderson <[email protected]>
clubanderson added a commit that referenced this pull request Apr 16, 2026
…utes (#8252)

* 📝 docs(security): mark local LLM providers registered + active

Updates SECURITY-MODEL.md §3 to reflect #8248, which
registers Ollama, llama.cpp, LocalAI, vLLM, LM Studio, RHAIIS, Groq,
OpenRouter and Open WebUI as chat-only agent providers in
InitializeProviders.

Changes:

- Provider table flips the Registered column from "no" to "yes (chat
  only)" for the nine HTTP providers that are now wired into the agent
  dropdown, and adds rows for the six new local LLM runners with their
  env vars and default URLs.
- Explains the chat-only capability flag and why missions still route
  through the tool-capable CLI agents (registry.go:303 rationale).
- Adds a "Local LLM strategy" subsection that cross-links the
  docs.kubestellar.io local-llm-strategy page and the eight install
  missions on kubestellar/console-kb.
- Replaces the "Planned follow-up" subsection with active recipes for
  each runner — Ollama loopback default, in-cluster Service URLs for
  llama.cpp/LocalAI/vLLM/RHAIIS, LM Studio workstation default, and
  Groq/OpenRouter/Open WebUI gateway overrides. The "# PLANNED —
  not yet wired at runtime" bash comments are removed.

The threat model claims about kubeconfig and credentials staying out
of the request body are unchanged and still authoritative.

Signed-off-by: Andrew Anderson <[email protected]>

* 🔥 chore(ci): delete 10 dead Copilot workflows + reclaim wasted CI minutes

Third of four PRs from the fullsend-ai/fullsend automation
evaluation. The plan was to replace copilot-pr-monitor.yml's minutely
cron poll with webhook triggers — but closer inspection found 10
workflows in the same fully-disabled state, and two of them were
burning CI minutes on unconditional cron schedules:

- copilot-pr-monitor.yml: cron * * * * * → 1440 wasted runs/day
- copilot-retry.yml:       cron */5 * * * * → 288 wasted runs/day

Every job in every deleted workflow is guarded by:
  if: false # Copilot disabled — issues handled by Claude Code scanner

Deleting them rather than refactoring is the right call per fullsend's
"repo-as-coordinator via native primitives" principle — dead code
pretending to orchestrate nothing is worse than no code at all.

Deleted:
- copilot-pr-monitor.yml (minutely cron poll, 100% no-op)
- copilot-retry.yml       (5-min cron, 100% no-op)
- copilot-assigned.yml
- copilot-automation.yml
- copilot-build-check.yml
- copilot-build-monitor.yml
- copilot-dco.yml
- copilot-recovery.yml
- copilot-review-apply.yml
- ai-fix.yml

Kept:
- copilot-comment-followup.yml — active, reads Copilot review comments
  on merged PRs. Unrelated to the deleted pipeline.
- copilot-setup-steps.yml — harmless workflow_dispatch-only helper.

Verification:
- No `workflow_run:` trigger in workflow-failure-issue.yml references
  any of the deleted workflow names.
- No `uses: ./.github/workflows/...` references to any of the deleted
  files.
- The `ai-fix-requested` label name is unrelated to `ai-fix.yml` —
  label stays, workflow file goes.

Not a replacement for the Claude Code scanner approach — that's
already running and owns the issue-fix loop now. This PR is pure
cleanup.

Savings: ~1728 dead workflow runs per day, plus a lot less noise in
the Actions tab.

Signed-off-by: Andrew Anderson <[email protected]>

---------

Signed-off-by: Andrew Anderson <[email protected]>
clubanderson added a commit that referenced this pull request Apr 16, 2026
…red output (#8253)

* 📝 docs(security): mark local LLM providers registered + active

Updates SECURITY-MODEL.md §3 to reflect #8248, which
registers Ollama, llama.cpp, LocalAI, vLLM, LM Studio, RHAIIS, Groq,
OpenRouter and Open WebUI as chat-only agent providers in
InitializeProviders.

Changes:

- Provider table flips the Registered column from "no" to "yes (chat
  only)" for the nine HTTP providers that are now wired into the agent
  dropdown, and adds rows for the six new local LLM runners with their
  env vars and default URLs.
- Explains the chat-only capability flag and why missions still route
  through the tool-capable CLI agents (registry.go:303 rationale).
- Adds a "Local LLM strategy" subsection that cross-links the
  docs.kubestellar.io local-llm-strategy page and the eight install
  missions on kubestellar/console-kb.
- Replaces the "Planned follow-up" subsection with active recipes for
  each runner — Ollama loopback default, in-cluster Service URLs for
  llama.cpp/LocalAI/vLLM/RHAIIS, LM Studio workstation default, and
  Groq/OpenRouter/Open WebUI gateway overrides. The "# PLANNED —
  not yet wired at runtime" bash comments are removed.

The threat model claims about kubeconfig and credentials staying out
of the request body are unchanged and still authoritative.

Signed-off-by: Andrew Anderson <[email protected]>

* ✨ feat(ci): decomposed-review prompt for Claude Code review — structured output

Fourth of four PRs from the fullsend-ai/fullsend automation
evaluation. Adopts fullsend's "decomposed code review" pattern
(docs/problems/code-review.md) — split review into specialized
concerns (correctness, security, style) rather than one monolithic
pass.

Done as a single LLM call with structured output, not 3 parallel
jobs, to keep token cost neutral. The prompt asks the existing
/code-review:code-review plugin to organize its findings into three
explicit sections with P0/P1/P2 priority tags, and to write "None."
under any section that has nothing to report so it doesn't
fabricate issues.

The SECURITY section references docs/security/SECURITY-AI.md (added
in PR #8249) so the reviewer explicitly watches for the six threat
categories — external prompt injection, insider credentials, DoS,
agent drift, supply chain, agent-to-agent injection — on any PR
that touches LLM-calling code.

Only change is the `prompt:` field in the existing
`.github/workflows/claude-code-review.yml`. No new actions, no new
secrets, no cost increase.

Expected behavior after merge:
- Every PR's Claude Code review comment now has a CORRECTNESS /
  SECURITY / STYLE structure instead of prose.
- Every issue is tagged P0/P1/P2 so reviewers can triage quickly.
- A pure doc PR should show "None." in all three sections, not
  fabricated nits.
- A PR touching LLM-calling code should produce at least one item
  in SECURITY referencing prompt-injection risk.

Signed-off-by: Andrew Anderson <[email protected]>

---------

Signed-off-by: Andrew Anderson <[email protected]>
clubanderson added a commit that referenced this pull request Apr 16, 2026
Updates SECURITY-MODEL.md §3 to reflect #8248, which
registers Ollama, llama.cpp, LocalAI, vLLM, LM Studio, RHAIIS, Groq,
OpenRouter and Open WebUI as chat-only agent providers in
InitializeProviders.

Changes:

- Provider table flips the Registered column from "no" to "yes (chat
  only)" for the nine HTTP providers that are now wired into the agent
  dropdown, and adds rows for the six new local LLM runners with their
  env vars and default URLs.
- Explains the chat-only capability flag and why missions still route
  through the tool-capable CLI agents (registry.go:303 rationale).
- Adds a "Local LLM strategy" subsection that cross-links the
  docs.kubestellar.io local-llm-strategy page and the eight install
  missions on kubestellar/console-kb.
- Replaces the "Planned follow-up" subsection with active recipes for
  each runner — Ollama loopback default, in-cluster Service URLs for
  llama.cpp/LocalAI/vLLM/RHAIIS, LM Studio workstation default, and
  Groq/OpenRouter/Open WebUI gateway overrides. The "# PLANNED —
  not yet wired at runtime" bash comments are removed.

The threat model claims about kubeconfig and credentials staying out
of the request body are unchanged and still authoritative.

Signed-off-by: Andrew Anderson <[email protected]>
@clubanderson clubanderson merged commit 7d3b874 into main Apr 16, 2026
36 of 37 checks passed
@clubanderson clubanderson deleted the feat/local-llm-agent-providers branch April 16, 2026 00:15
@github-actions
Copy link
Copy Markdown
Contributor

Thank you for your contribution! Your PR has been merged.

Check out what's new:

Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey

@github-actions
Copy link
Copy Markdown
Contributor

Post-merge build verification passed

Both Go and frontend builds compiled successfully against merge commit 7d3b874a1d17f24fdce29f806b226f0db1994c45.

@github-actions
Copy link
Copy Markdown
Contributor

✅ Post-Merge Verification: passed

Commit: 7d3b874a1d17f24fdce29f806b226f0db1994c45
Specs run: smoke.spec.ts
Report: https://github.com/kubestellar/console/actions/runs/24485054578

clubanderson added a commit that referenced this pull request Apr 16, 2026
…RL Advanced section

Part 2 of #8254. The Settings → API Keys modal now renders the nine
chat-only providers (Groq, OpenRouter, Open WebUI + Ollama, llama.cpp,
LocalAI, vLLM, LM Studio, RHAIIS) with an Advanced expandable section
that accepts a per-provider Base URL override.

Backend changes
---------------

- KeyStatus gains BaseURL, BaseURLEnvVar, BaseURLSource fields so the
  frontend can render the current resolved value and indicate whether
  it came from env or config file.
- handleGetKeysStatus now returns the nine chat-only providers (was
  previously an empty list per the "API-key-driven agents are hidden"
  comment, which predated PR #8248 flipping them on). CLI-based
  tool-capable agents (claude-code, bob, codex, ...) remain omitted —
  they manage their own credentials.
- SetKeyRequest.APIKey is now optional: setting just BaseURL is the
  common path for unauthenticated local LLM runners. handleSetKey
  accepts any combination of apiKey, baseURL, model — at least one
  is required. Base URL is syntactically validated (http(s)://, no
  whitespace) via validateBaseURL before being saved. On successful
  save, cached key validity is invalidated so the next validation
  hits the new endpoint.
- Key validation is skipped for local LLM runners (validationRequired
  is false on their providerDef entries). There is no meaningful way
  to validate the sentinel "local-llm-no-auth" placeholder against a
  real endpoint, so we report Configured=true based on URL presence.

Frontend changes
----------------

- KeyStatus TS interface gains baseURL, baseURLEnvVar, baseURLSource.
- APIKeySettings.tsx adds an Advanced expandable to each row. It
  only renders when the backend reports baseURLEnvVar (meaning the
  provider actually supports an override). The form includes a
  text input, an inline "env var wins" hint when baseURLSource is
  "env", syntactic validation feedback, and a post-save "restart
  kc-agent to apply" message. The summary row shows the current
  resolved value next to the Advanced toggle so operators can see
  it at a glance.

Closes #8254.

- go build ./... passes.
- go test ./pkg/agent/... passes (flaky auto-update test unrelated).
- npm run build passes all post-build safety checks.

Signed-off-by: Andrew Anderson <[email protected]>
clubanderson added a commit that referenced this pull request Apr 16, 2026
…closes #8254) (#8256)

* ✨ feat(agent): ConfigManager.GetBaseURL/SetBaseURL + dynamic provider base URL resolution

Part 1 of #8254. Adds per-provider base URL storage in ~/.kc/config.yaml
and updates all nine chat-only HTTP providers to resolve their base URL
dynamically at request time, so changes take effect without restarting
kc-agent.

Backend changes
---------------

- AgentKeyConfig gains an optional `base_url` YAML field alongside
  api_key and model. Empty string means "use the compiled-in default".
- ConfigManager.GetBaseURL(provider) / SetBaseURL / RemoveBaseURL
  mirror the existing GetAPIKey / SetAPIKey / RemoveAPIKey shape. The
  precedence chain is env var → config file → compiled default, same
  as GetAPIKey and GetModel.
- getBaseURLEnvKeyForProvider maps each registered provider key to
  its URL env var (OLLAMA_URL, LLAMACPP_URL, LOCALAI_URL, VLLM_URL,
  LM_STUDIO_URL, RHAIIS_URL, GROQ_BASE_URL, OPENROUTER_BASE_URL,
  OPEN_WEBUI_URL).

Provider refactor
-----------------

- LocalOpenAICompatProvider.localOpenAICompatBaseURL() now walks
  env var → ConfigManager.GetBaseURL → struct default. All six local
  LLM runners use this path.
- GroqProvider drops its cached baseURL field and resolves via a new
  groqResolveBaseURL() helper (env → config → default). The existing
  groqValidationURL() already consults the same helper, so key
  validation continues to honor the override.
- OpenRouterProvider and OpenWebUIProvider get the same treatment —
  dynamic resolution via openRouterResolveBaseURL() and
  openWebUIResolveBaseURL() respectively.

The frontend UI (APIKeySettings Advanced section with a Base URL
input field per provider) is Part 2 of this PR, coming next.

- go build ./... passes.
- go test ./pkg/agent/... passes.

Signed-off-by: Andrew Anderson <[email protected]>

* ✨ feat(ui): Settings → API Keys lists local LLM providers with Base URL Advanced section

Part 2 of #8254. The Settings → API Keys modal now renders the nine
chat-only providers (Groq, OpenRouter, Open WebUI + Ollama, llama.cpp,
LocalAI, vLLM, LM Studio, RHAIIS) with an Advanced expandable section
that accepts a per-provider Base URL override.

Backend changes
---------------

- KeyStatus gains BaseURL, BaseURLEnvVar, BaseURLSource fields so the
  frontend can render the current resolved value and indicate whether
  it came from env or config file.
- handleGetKeysStatus now returns the nine chat-only providers (was
  previously an empty list per the "API-key-driven agents are hidden"
  comment, which predated PR #8248 flipping them on). CLI-based
  tool-capable agents (claude-code, bob, codex, ...) remain omitted —
  they manage their own credentials.
- SetKeyRequest.APIKey is now optional: setting just BaseURL is the
  common path for unauthenticated local LLM runners. handleSetKey
  accepts any combination of apiKey, baseURL, model — at least one
  is required. Base URL is syntactically validated (http(s)://, no
  whitespace) via validateBaseURL before being saved. On successful
  save, cached key validity is invalidated so the next validation
  hits the new endpoint.
- Key validation is skipped for local LLM runners (validationRequired
  is false on their providerDef entries). There is no meaningful way
  to validate the sentinel "local-llm-no-auth" placeholder against a
  real endpoint, so we report Configured=true based on URL presence.

Frontend changes
----------------

- KeyStatus TS interface gains baseURL, baseURLEnvVar, baseURLSource.
- APIKeySettings.tsx adds an Advanced expandable to each row. It
  only renders when the backend reports baseURLEnvVar (meaning the
  provider actually supports an override). The form includes a
  text input, an inline "env var wins" hint when baseURLSource is
  "env", syntactic validation feedback, and a post-save "restart
  kc-agent to apply" message. The summary row shows the current
  resolved value next to the Advanced toggle so operators can see
  it at a glance.

Closes #8254.

- go build ./... passes.
- go test ./pkg/agent/... passes (flaky auto-update test unrelated).
- npm run build passes all post-build safety checks.

Signed-off-by: Andrew Anderson <[email protected]>

---------

Signed-off-by: Andrew Anderson <[email protected]>
clubanderson added a commit that referenced this pull request Apr 16, 2026
…8314)

The nightly Release workflow has been red since 2026-04-13 because four
Go tests fell behind handler changes shipped over the past two weeks.
Each test now aligns with the current handler contract:

- `pkg/agent/server_test.go:TestServer_SettingsHandlers` — expected the
  keys endpoint to return 0 providers, but #8248/#8254/#8256 now
  register nine chat-only HTTP providers (3 OpenAI-compatible gateways
  + 6 local LLM runners) so the Settings modal can show per-provider
  base URL overrides. Assert non-empty list and Provider presence.

- `pkg/api/handlers/cards_test.go:TestRecordFocus_BadBody_Returns400` —
  `RecordFocus` now runs `requireEditorOrAdmin` before BodyParser (#7011),
  which calls `store.GetUser`. The test's custom `recordFocusStore`
  never registered `GetUser`, so the mock panicked. Add the admin-user
  expectation.

- `pkg/api/handlers/dashboard_test.go:setupDashboardTest` —
  `CreateDashboard` now calls `store.CountUserDashboards` to enforce
  the per-user dashboard limit (#7010). Register a default
  `Return(0, nil).Maybe()` so the three CreateDashboard tests stay
  under the limit. Tests exercising the limit can override.

- `pkg/api/handlers/setup_test.go` — `TestClusterGroupsCRUD` exercises
  handlers that persist definitions via `SaveClusterGroup` /
  `DeleteClusterGroup` / `ListClusterGroups` (#7013). Register
  permissive `.Maybe()` mocks on the shared `setupTestEnv` MockStore.

Verification: `go test ./...` — all packages pass locally.

Fixes #8310, Fixes #8313, Fixes #8314

Signed-off-by: Andy Anderson <[email protected]>
clubanderson added a commit that referenced this pull request Apr 16, 2026
…8314) (#8317)

The nightly Release workflow has been red since 2026-04-13 because four
Go tests fell behind handler changes shipped over the past two weeks.
Each test now aligns with the current handler contract:

- `pkg/agent/server_test.go:TestServer_SettingsHandlers` — expected the
  keys endpoint to return 0 providers, but #8248/#8254/#8256 now
  register nine chat-only HTTP providers (3 OpenAI-compatible gateways
  + 6 local LLM runners) so the Settings modal can show per-provider
  base URL overrides. Assert non-empty list and Provider presence.

- `pkg/api/handlers/cards_test.go:TestRecordFocus_BadBody_Returns400` —
  `RecordFocus` now runs `requireEditorOrAdmin` before BodyParser (#7011),
  which calls `store.GetUser`. The test's custom `recordFocusStore`
  never registered `GetUser`, so the mock panicked. Add the admin-user
  expectation.

- `pkg/api/handlers/dashboard_test.go:setupDashboardTest` —
  `CreateDashboard` now calls `store.CountUserDashboards` to enforce
  the per-user dashboard limit (#7010). Register a default
  `Return(0, nil).Maybe()` so the three CreateDashboard tests stay
  under the limit. Tests exercising the limit can override.

- `pkg/api/handlers/setup_test.go` — `TestClusterGroupsCRUD` exercises
  handlers that persist definitions via `SaveClusterGroup` /
  `DeleteClusterGroup` / `ListClusterGroups` (#7013). Register
  permissive `.Maybe()` mocks on the shared `setupTestEnv` MockStore.

Verification: `go test ./...` — all packages pass locally.

Fixes #8310, Fixes #8313, Fixes #8314

Signed-off-by: Andy Anderson <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants