Add local Ollama API support#20
Conversation
There was a problem hiding this comment.
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/ollamaand/api/ollama/modelsendpoints 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.
| } | ||
|
|
||
| export type AIProvider = "openrouter" | "openai" | "zai" | ||
| export type AIProvider = "openrouter" | "openai" | "zai" | "ollama" |
There was a problem hiding this comment.
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).
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
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| export function buildTryEndpoints(provider: AIProvider, normalizedBase: string): string[] { | ||
| return provider === "ollama" || isLocalBase(normalizedBase) |
There was a problem hiding this comment.
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.
| return provider === "ollama" || isLocalBase(normalizedBase) | |
| void normalizedBase | |
| return provider === "ollama" |
| CONTENT_TYPE_CONFIG[ | ||
| type as keyof typeof CONTENT_TYPE_CONFIG | ||
| ] | ||
| type in CONTENT_TYPE_CONFIG |
There was a problem hiding this comment.
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.
| type in CONTENT_TYPE_CONFIG | |
| Object.hasOwn(CONTENT_TYPE_CONFIG, type) |
| <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 |
There was a problem hiding this comment.
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.
| 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 |
| <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 |
There was a problem hiding this comment.
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.
| 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 |
| } | ||
|
|
||
| const config = CONTENT_TYPE_CONFIG[block.contentType] | ||
| const config = CONTENT_TYPE_CONFIG[block.contentType] ?? CONTENT_TYPE_CONFIG.general |
There was a problem hiding this comment.
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.
| 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 |
| {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 |
There was a problem hiding this comment.
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.
| 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 |
| // 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" } } | ||
| } |
There was a problem hiding this comment.
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).
| // 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 }) | ||
| } |
There was a problem hiding this comment.
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.
| @@ -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 | |||
There was a problem hiding this comment.
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.
| 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 |
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:
/api/ollama/route.tsand/api/ollama/models/route.tsendpoints to securely forward chat requests and fetch available models from Ollama, with validation and environment-based restrictions. [1] [2]Content type icon/config fallback and UI robustness:
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:
Code and import cleanups:
These changes collectively improve the application's resilience, especially when dealing with user-customized or future content types and AI providers.