Skip to content

Add local Ollama API support#20

Open
PeteHaughie wants to merge 8 commits into
mskayyali:mainfrom
PeteHaughie:main
Open

Add local Ollama API support#20
PeteHaughie wants to merge 8 commits into
mskayyali:mainfrom
PeteHaughie:main

Conversation

@PeteHaughie
Copy link
Copy Markdown

This pull request introduces robust handling for missing or undefined content type icons and configuration throughout the UI, and adds dynamic model fetching for Ollama AI provider integration. It also includes two new API endpoints for interacting with Ollama, and improves the display logic for model labels and content type badges. These changes enhance stability, flexibility, and user experience, especially when dealing with custom or unknown content types and dynamic AI model lists.

Dynamic Ollama integration and API endpoints:

  • Added /api/ollama/route.ts and /api/ollama/models/route.ts endpoints to securely forward chat requests and fetch available models from Ollama, with validation and environment-based restrictions. [1] [2]
  • Updated the project sidebar to fetch Ollama models dynamically, update the model list, and auto-select a valid model if the current one is unavailable.

Content type icon/config fallback and UI robustness:

  • Throughout the app (e.g., tile-card, graph-detail-panel, kanban-area, kanban-minimap, tile-index, tiling-minimap, about-panel), added fallback logic to use a default icon (FileText) and general config if a content type is missing or lacks an icon, preventing runtime errors and ensuring consistent UI. [1] [2] [3] [4] [5] [6] [7] [8] [9]

Model label and status display improvements:

  • Improved model label logic in the main page and sidebar to correctly show the Ollama model name or a fallback, and updated the status bar to gracefully handle unknown content types. [1] [2] [3] [4]

Code and import cleanups:

  • Updated imports to include missing icons and utility functions where needed. [1] [2] [3]

These changes collectively improve the application's resilience, especially when dealing with user-customized or future content types and AI providers.

Copilot AI review requested due to automatic review settings April 9, 2026 15:13
Copy link
Copy Markdown

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

This PR adds a local Ollama integration path (including server-side proxy endpoints and dynamic model discovery) while making the UI more resilient to unknown/missing content-type configuration so the app doesn’t crash or render incorrectly with custom/stale data.

Changes:

  • Added /api/ollama and /api/ollama/models endpoints to proxy chat requests and fetch available Ollama models with environment-based restrictions.
  • Introduced AI base URL utilities and updated AI client calls (ghost/enrich) to try multiple OpenAI-compatible endpoints and optionally route through the Ollama proxy.
  • Hardened multiple UI components to fall back to “general” content-type config and default icons/labels when encountering unknown types.

Reviewed changes

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

Show a summary per file
File Description
next.config.mjs CSP connect-src expanded (dev-only) to allow connecting to local Ollama.
lib/ai-utils.ts New helpers for base URL normalization, endpoint selection, and safe URL joining.
lib/ai-settings.ts Adds ollama provider preset; allows Ollama without API key; improves header/base URL behavior.
lib/ai-ghost.ts Routes requests via multiple candidate endpoints and supports the /api/ollama proxy.
lib/ai-enrich.ts Same as ghost: multi-endpoint attempts and optional Ollama proxy routing.
components/tiling-minimap.tsx Falls back to general content type config when unknown types appear.
components/tile-index.tsx Ensures an icon fallback when a content type config is missing an icon.
components/tile-card.tsx Adds robust config fallback for unknown content types and safe accent defaults.
components/status-bar.tsx Gracefully handles unknown content types in type-count display.
components/project-sidebar.tsx Fetches Ollama models dynamically and updates/auto-selects model IDs accordingly.
components/kanban-minimap.tsx Adds a default icon fallback to avoid rendering issues when icon is missing.
components/kanban-area.tsx Adds a default icon fallback for column headers.
components/graph-detail-panel.tsx Uses general config fallback and safer icon/accent handling.
components/about-panel.tsx Uses general config fallback for content type highlights.
app/page.tsx Updates model-label display logic and treats Ollama as “active” without an API key.
app/api/ollama/route.ts New proxy endpoint to forward OpenAI-compatible chat to a local Ollama base URL.
app/api/ollama/models/route.ts New endpoint to fetch available models from a local Ollama base URL.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/page.tsx Outdated
Comment thread lib/ai-settings.ts
}

export type AIProvider = "openrouter" | "openai" | "zai"
export type AIProvider = "openrouter" | "openai" | "zai" | "ollama"
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

AIProvider now includes "ollama", but getModelsForProvider() doesn’t have an Ollama branch and currently falls back to AI_MODELS for unknown providers. That can cause Ollama to incorrectly reuse the OpenRouter model list/defaults in any call site that still relies on getModelsForProvider; add an explicit provider === "ollama" case (e.g. return an empty list or a dedicated Ollama list).

Copilot uses AI. Check for mistakes.
Comment thread components/tile-card.tsx Outdated
Comment thread components/graph-detail-panel.tsx Outdated
Comment thread components/about-panel.tsx Outdated
Comment thread app/api/ollama/models/route.ts Outdated
PeteHaughie and others added 7 commits April 9, 2026 18:04
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…d handled ollama in getAIHeaders so it preserves the configured model id and always sets 'x-or-supports-grounding': 'false'.
Handle Ollama case in model retrieval and headers
Copy link
Copy Markdown

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

Copilot reviewed 17 out of 17 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/ai-utils.ts
}

export function buildTryEndpoints(provider: AIProvider, normalizedBase: string): string[] {
return provider === "ollama" || isLocalBase(normalizedBase)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

buildTryEndpoints routes any local customBaseUrl through /api/ollama (not just the ollama provider). Since ai-ghost/ai-enrich send getProviderHeaders(config), this will forward a non-Ollama provider’s Authorization: Bearer ... header to the local base URL via the Ollama proxy, which can unintentionally leak the user’s remote API key and also applies Ollama-specific model rewriting logic. Consider limiting the /api/ollama proxy to provider === "ollama" (and/or stripping Authorization when proxying to local URLs), or introducing a separate generic local proxy that does not mutate the request model.

Suggested change
return provider === "ollama" || isLocalBase(normalizedBase)
void normalizedBase
return provider === "ollama"

Copilot uses AI. Check for mistakes.
Comment thread components/status-bar.tsx
CONTENT_TYPE_CONFIG[
type as keyof typeof CONTENT_TYPE_CONFIG
]
type in CONTENT_TYPE_CONFIG
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

Using type in CONTENT_TYPE_CONFIG will also return true for keys on the prototype chain (e.g. toString, __proto__), which defeats the fallback path and can yield incorrect labels/colors for unknown/imported content types. Prefer Object.hasOwn(CONTENT_TYPE_CONFIG, type) (or Object.prototype.hasOwnProperty.call) when indexing with untrusted strings.

Suggested change
type in CONTENT_TYPE_CONFIG
Object.hasOwn(CONTENT_TYPE_CONFIG, type)

Copilot uses AI. Check for mistakes.
<div className="flex flex-col gap-1.5">
{page.map(block => {
const config = CONTENT_TYPE_CONFIG[block.contentType]
const config = CONTENT_TYPE_CONFIG[block.contentType] ?? CONTENT_TYPE_CONFIG.general
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

CONTENT_TYPE_CONFIG[block.contentType] ?? CONTENT_TYPE_CONFIG.general is not safe against prototype keys (e.g. an imported contentType of "toString" will return a function, not undefined, so the fallback won’t apply). Use Object.hasOwn(CONTENT_TYPE_CONFIG, block.contentType) (as in tile-card) before indexing.

Suggested change
const config = CONTENT_TYPE_CONFIG[block.contentType] ?? CONTENT_TYPE_CONFIG.general
const config = Object.hasOwn(CONTENT_TYPE_CONFIG, block.contentType)
? CONTENT_TYPE_CONFIG[block.contentType]
: CONTENT_TYPE_CONFIG.general

Copilot uses AI. Check for mistakes.
<div className="grid grid-cols-3 gap-[3px]">
{page.map(block => {
const config = CONTENT_TYPE_CONFIG[block.contentType]
const config = CONTENT_TYPE_CONFIG[block.contentType] ?? CONTENT_TYPE_CONFIG.general
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

Same prototype-key issue as above: CONTENT_TYPE_CONFIG[block.contentType] ?? ... can return inherited properties for unexpected contentType strings, preventing the intended fallback. Use Object.hasOwn/hasOwnProperty check before indexing.

Suggested change
const config = CONTENT_TYPE_CONFIG[block.contentType] ?? CONTENT_TYPE_CONFIG.general
const config = Object.prototype.hasOwnProperty.call(CONTENT_TYPE_CONFIG, block.contentType)
? CONTENT_TYPE_CONFIG[block.contentType]
: CONTENT_TYPE_CONFIG.general

Copilot uses AI. Check for mistakes.
}

const config = CONTENT_TYPE_CONFIG[block.contentType]
const config = CONTENT_TYPE_CONFIG[block.contentType] ?? CONTENT_TYPE_CONFIG.general
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

CONTENT_TYPE_CONFIG[block.contentType] ?? CONTENT_TYPE_CONFIG.general won’t fall back for prototype keys like toString/__proto__ if a project import contains an unexpected contentType string. Use Object.hasOwn(CONTENT_TYPE_CONFIG, block.contentType) before indexing to ensure unknown types resolve to general.

Suggested change
const config = CONTENT_TYPE_CONFIG[block.contentType] ?? CONTENT_TYPE_CONFIG.general
const config = Object.hasOwn(CONTENT_TYPE_CONFIG, block.contentType)
? CONTENT_TYPE_CONFIG[block.contentType as ContentType]
: CONTENT_TYPE_CONFIG.general

Copilot uses AI. Check for mistakes.
{connectedBlocks.map(b => {
const bConfig = CONTENT_TYPE_CONFIG[b.contentType]
const BIcon = bConfig.icon
const bConfig = CONTENT_TYPE_CONFIG[b.contentType] ?? CONTENT_TYPE_CONFIG.general
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

Same prototype-key issue: CONTENT_TYPE_CONFIG[b.contentType] ?? ... can return inherited values for unexpected strings, preventing the intended fallback config. Use an own-property check (e.g. Object.hasOwn) before indexing.

Suggested change
const bConfig = CONTENT_TYPE_CONFIG[b.contentType] ?? CONTENT_TYPE_CONFIG.general
const bConfig = Object.hasOwn(CONTENT_TYPE_CONFIG, b.contentType)
? CONTENT_TYPE_CONFIG[b.contentType as ContentType]
: CONTENT_TYPE_CONFIG.general

Copilot uses AI. Check for mistakes.
Comment thread app/api/ollama/route.ts
Comment on lines +35 to +38
// Disallow non-local remote hosts in production — return 403 rather than throwing.
if (!isLocalHost(baseCandidate) && process.env.NODE_ENV === "production") {
return { ok: false, status: 403, json: { error: "Remote baseUrl not allowed in production" } }
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The proxy only blocks non-local baseUrl values when NODE_ENV === "production". If a dev/staging deployment is accidentally exposed (or NODE_ENV is misconfigured), this endpoint can be used as a general-purpose server-side fetch/proxy (SSRF). Consider enforcing a strict localhost-only allowlist regardless of NODE_ENV, or gating non-local access behind an explicit env flag/allowlist (similar to the SSRF protections in /api/fetch-url).

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +75
// Do not allow arbitrary remote hosts in production — limit to local dev.
if (!isLocalHost(provided) && process.env.NODE_ENV === "production") {
return NextResponse.json({ error: "Remote baseUrl not allowed" }, { status: 403 })
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

This endpoint only blocks non-local baseUrl values in production. In any environment where NODE_ENV isn’t production but the server is reachable, it can be abused as an SSRF proxy to arbitrary hosts. Consider enforcing localhost-only (or an explicit allowlist) regardless of NODE_ENV, or requiring a server-side configuration switch to enable remote bases.

Copilot uses AI. Check for mistakes.
Comment thread components/tile-index.tsx
@@ -45,7 +45,7 @@ export function TileIndex({ blocks, onHighlight, highlightedId, onClose, isOpen,
})
return Array.from(cols).map(type => {
const config = CONTENT_TYPE_CONFIG[type as ContentType] || CONTENT_TYPE_CONFIG.general
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

CONTENT_TYPE_CONFIG[type as ContentType] || CONTENT_TYPE_CONFIG.general can still resolve to inherited properties (e.g. type === "toString") and skip the intended fallback, which can yield undefined labels in the index for imported/custom content types. Consider using an own-property check (Object.hasOwn(CONTENT_TYPE_CONFIG, type)) before indexing.

Suggested change
const config = CONTENT_TYPE_CONFIG[type as ContentType] || CONTENT_TYPE_CONFIG.general
const config = Object.hasOwn(CONTENT_TYPE_CONFIG, type)
? CONTENT_TYPE_CONFIG[type as ContentType]
: CONTENT_TYPE_CONFIG.general

Copilot uses AI. Check for mistakes.
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.

2 participants