Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Connect to PGlite through its socket server (`pglite-server`), with schema browsing, SQL editing, and row editing like PostgreSQL. Defaults to a loopback host with no TLS, and treats the connection as single-use to match PGlite's one-connection limit. (#1911)
- In the CSV editor, right-click a column header to rename, insert, delete, or change the type of that column. (#1913)
- Add llama.cpp and MLX as local AI providers. Each preset points at the server's default local endpoint and needs no API key, alongside the existing Ollama and custom OpenAI-compatible options. (#1777)

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/AI/Registry/AIProviderRegistration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ enum AIProviderRegistration {
}
))

for type in [AIProviderType.openRouter, .openCode, .ollama, .custom] {
for type in [AIProviderType.openRouter, .openCode, .ollama, .llamaCpp, .mlx, .custom] {
var capabilities: AIProviderCapabilities = [
.chat, .models, .endpointConfigurable, .maxOutputTokens, .modelListFetchable
]
Expand Down
10 changes: 10 additions & 0 deletions TablePro/Models/AI/AIModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ enum AIProviderType: String, Codable, CaseIterable, Identifiable, Sendable {
case gemini
case xai
case ollama
case llamaCpp
case mlx
case openCode
case custom

Expand All @@ -33,6 +35,8 @@ enum AIProviderType: String, Codable, CaseIterable, Identifiable, Sendable {
case .gemini: return "Gemini"
case .xai: return "xAI"
case .ollama: return "Ollama"
case .llamaCpp: return "llama.cpp"
case .mlx: return "MLX"
case .openCode: return "OpenCode Zen"
case .custom: return String(localized: "Custom")
}
Expand All @@ -49,6 +53,8 @@ enum AIProviderType: String, Codable, CaseIterable, Identifiable, Sendable {
case .gemini: return "https://generativelanguage.googleapis.com"
case .xai: return "https://api.x.ai"
case .ollama: return "http://localhost:11434"
case .llamaCpp: return "http://localhost:8080"
case .mlx: return "http://localhost:8080"
case .openCode: return "https://opencode.ai/zen"
case .custom: return ""
}
Expand All @@ -67,6 +73,8 @@ enum AIProviderType: String, Codable, CaseIterable, Identifiable, Sendable {
case .cursor: return .optionalApiKey
case .xai: return .optionalApiKey
case .ollama: return .none
case .llamaCpp: return .none
case .mlx: return .none
Comment on lines +76 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Expose connection testing for the unauthenticated presets

When a user adds either new preset, this .none auth style makes AIProviderDetailSheet.authSection render EmptyView; the only existing Test Connection controls are inside the API-key/Cursor/xAI auth sections. Thus llama.cpp and MLX have no way to run the connection test that the new provider instructions explicitly tell users to perform, even after choosing a model. Add an unauthenticated test control (for example in the connection section) or remove that workflow from the provider documentation.

Useful? React with 👍 / 👎.

case .openCode: return .optionalApiKey
default: return .apiKey
}
Expand All @@ -83,6 +91,8 @@ enum AIProviderType: String, Codable, CaseIterable, Identifiable, Sendable {
case .gemini: return "wand.and.stars"
case .xai: return "x.circle"
case .ollama: return "desktopcomputer"
case .llamaCpp: return "memorychip"
case .mlx: return "m.square"
case .openCode: return "sparkles"
case .custom: return "server.rack"
}
Expand Down
14 changes: 5 additions & 9 deletions TablePro/Views/Settings/AISettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -333,16 +333,12 @@ struct AISettingsView: View {
? String(localized: "API key set")
: String(localized: "Not configured")
case .none:
if provider.type == .ollama {
let endpoint = provider.endpoint.isEmpty ? provider.type.defaultEndpoint : provider.endpoint
if let host = URL(string: endpoint)?.host, host == "localhost" || host == "127.0.0.1" {
return String(localized: "Local")
}
return endpoint
let endpoint = provider.endpoint.isEmpty ? provider.type.defaultEndpoint : provider.endpoint
guard !endpoint.isEmpty else { return String(localized: "Not configured") }
if let host = URL(string: endpoint)?.host, host == "localhost" || host == "127.0.0.1" {
return String(localized: "Local")
}
return provider.endpoint.isEmpty
? String(localized: "Not configured")
: provider.endpoint
return endpoint
}
}

Expand Down
68 changes: 68 additions & 0 deletions TableProTests/Core/AI/LocalProviderRegistrationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// LocalProviderRegistrationTests.swift
// TableProTests
//

import Foundation
@testable import TablePro
import Testing

@Suite("Local OpenAI-compatible provider registration")
struct LocalProviderRegistrationTests {
init() {
AIProviderRegistration.registerAll()
}

private func descriptor(for type: AIProviderType) -> AIProviderDescriptor? {
AIProviderRegistry.shared.descriptor(for: type.rawValue)
}

@Test("llama.cpp is a known local provider with no auth and the default llama-server endpoint")
func llamaCppType() {
#expect(AIProviderType.llamaCpp.authStyle == .none)
#expect(AIProviderType.allCases.contains(.llamaCpp))
#expect(AIProviderType(rawValue: "llamaCpp") == .llamaCpp)
#expect(AIProviderType.llamaCpp.displayName == "llama.cpp")
#expect(AIProviderType.llamaCpp.defaultEndpoint == "http://localhost:8080")
}

@Test("MLX is a known local provider with no auth and the default mlx_lm.server endpoint")
func mlxType() {
#expect(AIProviderType.mlx.authStyle == .none)
#expect(AIProviderType.allCases.contains(.mlx))
#expect(AIProviderType(rawValue: "mlx") == .mlx)
#expect(AIProviderType.mlx.displayName == "MLX")
#expect(AIProviderType.mlx.defaultEndpoint == "http://localhost:8080")
}

@Test("Local providers register OpenAI-compatible descriptors that fetch models and take an endpoint")
func descriptorCapabilities() {
for type in [AIProviderType.llamaCpp, .mlx] {
let provider = descriptor(for: type)
#expect(provider != nil)
#expect(provider?.allowsEndpointConfiguration == true)
#expect(provider?.allowsMaxOutputTokens == true)
#expect(provider?.fetchesModelList == true)
#expect(provider?.allowsNameConfiguration == false)
#expect(provider?.supportsReasoning == false)
#expect(provider?.supportsImages == false)
}
}

@Test("Local providers build the OpenAI-compatible transport, not a bespoke one")
func makeProviderUsesOpenAICompatible() {
for type in [AIProviderType.llamaCpp, .mlx] {
let config = AIProviderConfig(type: type, model: "local-model")
#expect(descriptor(for: type)?.makeProvider(config, nil) is OpenAICompatibleProvider)
}
}

@Test("The default local endpoint resolves to the OpenAI chat-completions path")
func defaultEndpointResolvesToOpenAIPath() {
for type in [AIProviderType.llamaCpp, .mlx] {
let config = AIProviderConfig(type: type)
#expect(config.endpoint == "http://localhost:8080")
#expect(config.endpoint.openAIPath("chat/completions") == "http://localhost:8080/v1/chat/completions")
}
}
}
7 changes: 4 additions & 3 deletions docs/features/ai-assistant.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: AI Assistant
description: "Built-in AI for SQL: chat with tool calling, inline suggestions, explain, optimize, and fix-error across 11 providers."
description: "Built-in AI for SQL: chat with tool calling, inline suggestions, explain, optimize, and fix-error across 13 providers."
---

# AI Assistant
Expand All @@ -16,7 +16,7 @@ Open **Settings** (`Cmd+,`) > **AI**. The **Enable AI Features** toggle at the t
<img className="hidden dark:block" src="/images/ai-chat-settings-provider-dark.png" alt="AI settings" />
</Frame>

1. Click **Add Provider...** and pick a type: GitHub Copilot, ChatGPT, Cursor, Claude, OpenAI, OpenRouter, OpenCode Zen, Gemini, xAI, Ollama, or a custom OpenAI-compatible endpoint.
1. Click **Add Provider...** and pick a type: GitHub Copilot, ChatGPT, Cursor, Claude, OpenAI, OpenRouter, OpenCode Zen, Gemini, xAI, Ollama, llama.cpp, MLX, or a custom OpenAI-compatible endpoint.
2. Enter the API key, or sign in for Copilot, ChatGPT, Cursor, and xAI.
3. Enter a model name, or pick one from the fetched list.
4. Click **Test Connection**.
Expand All @@ -29,6 +29,7 @@ Provider notes:
- **ChatGPT**: sign in with your ChatGPT account to use the Codex quota from Plus, Pro, Business, and Enterprise plans, no API key. **Import from Codex CLI** reuses an existing Codex login. Unofficial interface; may change.
- **Cursor**: paste an API key from the Cursor dashboard, or leave it blank and click **Sign in with Cursor** (needs the Cursor CLI installed). Cursor runs as an agent, not a chat-completions endpoint. It cannot call TablePro's database tools, so Edit and Agent modes do not run queries through it.
- **xAI**: paste an API key, or click **Sign in with xAI** to use a SuperGrok or X Premium+ subscription. Sign-in opens a Grok Build consent screen; this path is unofficial and may change. Preset models: Grok 4.5 and Grok 4.3.
- **Ollama, llama.cpp, MLX**: local providers, no API key. Each preset fills in its default endpoint (`http://localhost:11434` for Ollama, `http://localhost:8080` for `llama-server` and `mlx_lm.server`); edit it if you moved the host or port. Start the server first, then **Test Connection** and pick a model from the fetched list. llama.cpp and MLX speak the OpenAI-compatible API; for tool calling, start `llama-server` with `--jinja`. For any other OpenAI-compatible server, use the custom endpoint.

## Chat

Expand Down Expand Up @@ -72,7 +73,7 @@ In Edit and Agent modes, each tool call appears as a card in the reply. Read-onl

If the connection's safe-mode level is **Silent**, write tools auto-approve. If it is **Read Only**, they auto-deny. Destructive operations are the exception: `confirm_destructive_operation` always requires a per-call click. Silent mode does not auto-approve it, and **Always for this connection** is refused for it. The tool also requires the model to pass the verbatim phrase `I understand this is irreversible`.

Provider support for tool calling: every provider except Cursor. Ollama and custom endpoints depend on the model.
Provider support for tool calling: every provider except Cursor. Ollama, llama.cpp, MLX, and custom endpoints depend on the model.

### Attach Context with `@`

Expand Down
Loading