v1.2.0:7 — add SparkControl AI provider + fix base-URL footgun
CI / proof-of-work (Next.js app) (push) Waiting to run
CI / start9/0.4 (StartOS package code) (push) Waiting to run

SparkControl is a self-hosted local-inference gateway with an OpenAI-compatible API, reached over the internal same-box StartOS address (http://spark-control.startos:9999/v1, plain HTTP). It takes no API key, so generateOpenAIStyle gained a { requireApiKey } option and now omits the Authorization header when no key is set. The Settings form auto-detects the loaded vLLM model via SparkControl's /api/endpoints probe, mirroring the Ollama auto-detect; it's $0 in the cost UI. Custom-URL => admin-only + SSRF-guarded, same as Ollama.

Also fixes a config footgun behind the empty-response report: a custom base URL could ride along to a fixed-URL provider (claude/openai/gemini) whose form field is hidden, get stored, and be silently ignored (the provider always hits its hardcoded endpoint). Both config write paths now null baseUrl for non-custom-URL providers, and the form clears it on provider change.

No schema/data change (AIConfigProfile.provider is free-text). 259 tests pass; built + sideloaded to immense-voyage.local with a clean non-root launch.
This commit is contained in:
Keysat
2026-06-19 14:47:30 -05:00
parent b2587d767b
commit 91b5b04d97
16 changed files with 635 additions and 27 deletions
+27 -7
View File
@@ -18,7 +18,14 @@ import { isCustomUrlProvider } from '@/lib/ai/providers';
* [id]/activate/route.ts so the action is explicit + auditable.
*/
const PROVIDERS = ['claude', 'openai', 'openai-compatible', 'gemini', 'ollama'] as const;
const PROVIDERS = [
'claude',
'openai',
'openai-compatible',
'gemini',
'ollama',
'sparkcontrol',
] as const;
export async function GET() {
const user = await getCurrentUser();
@@ -82,32 +89,44 @@ export async function POST(request: NextRequest) {
const { name, provider, model, baseUrl, apiKey, setActive } = parsed.data;
// Custom-URL providers (Ollama / OpenAI-compatible) are admin-only — a
// non-admin pointing the server at an arbitrary URL is the SSRF actor
// vector. Fixed-URL cloud providers stay per-user.
// Custom-URL providers (Ollama, SparkControl, OpenAI-compatible) are
// admin-only — a non-admin pointing the server at an arbitrary URL is the
// SSRF actor vector. Fixed-URL cloud providers stay per-user.
if (!user.isAdmin && (baseUrl || isCustomUrlProvider(provider))) {
return NextResponse.json(
{
error:
'Only an admin can configure providers with a custom base URL (Ollama / OpenAI-compatible).',
'Only an admin can configure providers with a custom base URL (Ollama, SparkControl, OpenAI-compatible).',
},
{ status: 403 },
);
}
// Only custom-URL providers (Ollama / OpenAI-compatible / SparkControl) carry
// a base URL. Fixed-URL providers (claude/openai/gemini) hit their hardcoded
// endpoint and ignore it — so we drop any stale baseUrl here rather than
// storing an impossible config (the footgun behind the gemini-with-a-baseURL
// mismatch). The UI also clears the field on provider change.
const normalizedBaseUrl = isCustomUrlProvider(provider) ? baseUrl || null : null;
const profile = await prisma.aIConfigProfile.create({
data: {
userId: user.id,
name: name ?? defaultName(provider, model),
provider,
model,
baseUrl: baseUrl || null,
baseUrl: normalizedBaseUrl,
apiKey: apiKey || null,
},
});
if (setActive) {
await activate(user.id, profile.id, { provider, model, baseUrl, apiKey });
await activate(user.id, profile.id, {
provider,
model,
baseUrl: normalizedBaseUrl,
apiKey,
});
}
return NextResponse.json({
@@ -128,6 +147,7 @@ function defaultName(provider: string, model: string): string {
'openai-compatible': 'Custom',
gemini: 'Gemini',
ollama: 'Ollama',
sparkcontrol: 'SparkControl',
};
const label = PRETTY[provider] ?? provider;
return `${label} · ${model}`;