Align OpenAI key settings with local model profiles
This commit is contained in:
@@ -11,10 +11,23 @@ export const runtime = 'nodejs';
|
|||||||
|
|
||||||
function buildResponse(key: string | null) {
|
function buildResponse(key: string | null) {
|
||||||
const configured = isValidOpenAiKey(key);
|
const configured = isValidOpenAiKey(key);
|
||||||
|
const llmProfile = process.env.LLM_PROFILE || 'openai';
|
||||||
|
const embeddingProfile = process.env.EMBEDDING_PROFILE || 'openai';
|
||||||
|
const openAiKeyWritable = llmProfile === 'openai' || embeddingProfile === 'openai';
|
||||||
return {
|
return {
|
||||||
configured,
|
configured,
|
||||||
maskedKey: configured ? maskOpenAiKey(key) : null,
|
maskedKey: configured ? maskOpenAiKey(key) : null,
|
||||||
envPath: getEnvLocalPath(),
|
envPath: getEnvLocalPath(),
|
||||||
|
openAiKeyWritable,
|
||||||
|
activeProfile: {
|
||||||
|
llmProfile,
|
||||||
|
llmModel: process.env.LLM_MODEL || (llmProfile === 'openai' ? 'gpt-4o-mini' : null),
|
||||||
|
llmBaseUrl: process.env.LLM_BASE_URL || null,
|
||||||
|
embeddingProfile,
|
||||||
|
embeddingModel: process.env.EMBEDDING_MODEL || (embeddingProfile === 'openai' ? 'text-embedding-3-small' : null),
|
||||||
|
embeddingBaseUrl: process.env.EMBEDDING_BASE_URL || process.env.LLM_BASE_URL || null,
|
||||||
|
embeddingDimensions: process.env.EMBEDDING_DIMENSIONS || (embeddingProfile === 'openai' ? '1536' : '1024'),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +50,17 @@ export async function POST(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const key = typeof body?.key === 'string' ? body.key.trim() : '';
|
const key = typeof body?.key === 'string' ? body.key.trim() : '';
|
||||||
|
const currentState = buildResponse(await readStoredOpenAiKey());
|
||||||
|
|
||||||
|
if (!currentState.openAiKeyWritable) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: 'OpenAI key entry is disabled because this install is using a local model profile. Change .env.local and rebuild embeddings to switch providers.',
|
||||||
|
...currentState,
|
||||||
|
},
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isValidOpenAiKey(key)) {
|
if (!isValidOpenAiKey(key)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function FirstRunModal() {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
const dismissed = window.localStorage.getItem(FIRST_RUN_DISMISSED_KEY) === 'true';
|
const dismissed = window.localStorage.getItem(FIRST_RUN_DISMISSED_KEY) === 'true';
|
||||||
setIsOpen(!data.configured && !dismissed);
|
setIsOpen(data.openAiKeyWritable !== false && !data.configured && !dismissed);
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
const dismissed = window.localStorage.getItem(FIRST_RUN_DISMISSED_KEY) === 'true';
|
const dismissed = window.localStorage.getItem(FIRST_RUN_DISMISSED_KEY) === 'true';
|
||||||
|
|||||||
@@ -3,11 +3,23 @@
|
|||||||
import { useState, useEffect, type CSSProperties } from 'react';
|
import { useState, useEffect, type CSSProperties } from 'react';
|
||||||
import { openExternalUrl } from '@/utils/openExternalUrl';
|
import { openExternalUrl } from '@/utils/openExternalUrl';
|
||||||
|
|
||||||
|
type ActiveProfile = {
|
||||||
|
llmProfile: string;
|
||||||
|
llmModel: string | null;
|
||||||
|
llmBaseUrl: string | null;
|
||||||
|
embeddingProfile: string;
|
||||||
|
embeddingModel: string | null;
|
||||||
|
embeddingBaseUrl: string | null;
|
||||||
|
embeddingDimensions: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function ApiKeysViewer() {
|
export default function ApiKeysViewer() {
|
||||||
const [status, setStatus] = useState<'checking' | 'configured' | 'not-set'>('checking');
|
const [status, setStatus] = useState<'checking' | 'configured' | 'not-set'>('checking');
|
||||||
const [keyInput, setKeyInput] = useState('');
|
const [keyInput, setKeyInput] = useState('');
|
||||||
const [maskedKey, setMaskedKey] = useState<string | null>(null);
|
const [maskedKey, setMaskedKey] = useState<string | null>(null);
|
||||||
const [envPath, setEnvPath] = useState<string>('.env.local');
|
const [envPath, setEnvPath] = useState<string>('.env.local');
|
||||||
|
const [openAiKeyWritable, setOpenAiKeyWritable] = useState(true);
|
||||||
|
const [activeProfile, setActiveProfile] = useState<ActiveProfile | null>(null);
|
||||||
const [message, setMessage] = useState<string | null>(null);
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -19,6 +31,8 @@ export default function ApiKeysViewer() {
|
|||||||
setStatus(data.configured ? 'configured' : 'not-set');
|
setStatus(data.configured ? 'configured' : 'not-set');
|
||||||
setMaskedKey(data.maskedKey ?? null);
|
setMaskedKey(data.maskedKey ?? null);
|
||||||
setEnvPath(data.envPath ?? '.env.local');
|
setEnvPath(data.envPath ?? '.env.local');
|
||||||
|
setOpenAiKeyWritable(data.openAiKeyWritable !== false);
|
||||||
|
setActiveProfile(data.activeProfile ?? null);
|
||||||
})
|
})
|
||||||
.catch(() => setStatus('not-set'));
|
.catch(() => setStatus('not-set'));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -44,6 +58,8 @@ export default function ApiKeysViewer() {
|
|||||||
setStatus('configured');
|
setStatus('configured');
|
||||||
setMaskedKey(payload.maskedKey ?? null);
|
setMaskedKey(payload.maskedKey ?? null);
|
||||||
setEnvPath(payload.envPath ?? envPath);
|
setEnvPath(payload.envPath ?? envPath);
|
||||||
|
setOpenAiKeyWritable(payload.openAiKeyWritable !== false);
|
||||||
|
setActiveProfile(payload.activeProfile ?? activeProfile);
|
||||||
setKeyInput('');
|
setKeyInput('');
|
||||||
setMessage('Saved to .env.local and updated the running app.');
|
setMessage('Saved to .env.local and updated the running app.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -71,6 +87,8 @@ export default function ApiKeysViewer() {
|
|||||||
setMaskedKey(null);
|
setMaskedKey(null);
|
||||||
setKeyInput('');
|
setKeyInput('');
|
||||||
setEnvPath(payload.envPath ?? envPath);
|
setEnvPath(payload.envPath ?? envPath);
|
||||||
|
setOpenAiKeyWritable(payload.openAiKeyWritable !== false);
|
||||||
|
setActiveProfile(payload.activeProfile ?? activeProfile);
|
||||||
setMessage('Removed from .env.local and cleared the running app key.');
|
setMessage('Removed from .env.local and cleared the running app key.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to remove API key');
|
setError(err instanceof Error ? err.message : 'Failed to remove API key');
|
||||||
@@ -81,7 +99,37 @@ export default function ApiKeysViewer() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={containerStyle}>
|
<div style={containerStyle}>
|
||||||
|
{activeProfile && (
|
||||||
|
<div style={profileBoxStyle}>
|
||||||
|
<div style={featuresHeaderStyle}>
|
||||||
|
{openAiKeyWritable ? 'OpenAI profile active' : 'Local model profile active'}
|
||||||
|
</div>
|
||||||
|
<div style={profileGridStyle}>
|
||||||
|
<div>
|
||||||
|
<div style={profileLabelStyle}>Utility LLM</div>
|
||||||
|
<code style={codeInlineStyle}>{activeProfile.llmProfile}</code>
|
||||||
|
{activeProfile.llmModel && <span style={profileValueStyle}> {activeProfile.llmModel}</span>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={profileLabelStyle}>Embeddings</div>
|
||||||
|
<code style={codeInlineStyle}>{activeProfile.embeddingProfile}</code>
|
||||||
|
{activeProfile.embeddingModel && <span style={profileValueStyle}> {activeProfile.embeddingModel}</span>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={profileLabelStyle}>Embedding width</div>
|
||||||
|
<span style={profileValueStyle}>{activeProfile.embeddingDimensions}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!openAiKeyWritable && (
|
||||||
|
<p style={profileNoteStyle}>
|
||||||
|
This install is already configured for local models. OpenAI key entry is disabled here so the app does not drift away from the setup profile. To switch providers, edit <code style={codeInlineStyle}>{envPath}</code> and run <code style={codeInlineStyle}>npm run rebuild:embeddings</code>.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Features explanation */}
|
{/* Features explanation */}
|
||||||
|
{openAiKeyWritable && (
|
||||||
<div style={featuresBoxStyle}>
|
<div style={featuresBoxStyle}>
|
||||||
<div style={featuresHeaderStyle}>OpenAI API Key enables:</div>
|
<div style={featuresHeaderStyle}>OpenAI API Key enables:</div>
|
||||||
<ul style={featuresListStyle}>
|
<ul style={featuresListStyle}>
|
||||||
@@ -93,8 +141,10 @@ export default function ApiKeysViewer() {
|
|||||||
Without a key, you can still create and organise nodes manually.
|
Without a key, you can still create and organise nodes manually.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Status */}
|
{/* Status */}
|
||||||
|
{openAiKeyWritable ? (
|
||||||
<div style={cardStyle}>
|
<div style={cardStyle}>
|
||||||
<div style={cardHeaderStyle}>
|
<div style={cardHeaderStyle}>
|
||||||
<span style={cardTitleStyle}>OpenAI API Key</span>
|
<span style={cardTitleStyle}>OpenAI API Key</span>
|
||||||
@@ -178,8 +228,27 @@ export default function ApiKeysViewer() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={cardStyle}>
|
||||||
|
<div style={cardHeaderStyle}>
|
||||||
|
<span style={cardTitleStyle}>OpenAI API Key</span>
|
||||||
|
<span style={{ fontSize: 12, color: 'var(--settings-muted)' }}>Disabled</span>
|
||||||
|
</div>
|
||||||
|
<div style={instructionsStyle}>
|
||||||
|
<p style={{ margin: 0 }}>
|
||||||
|
This workspace is using the local model profile selected during setup. The app will call the configured OpenAI-compatible local endpoints for descriptions and embeddings instead of using an OpenAI API key.
|
||||||
|
</p>
|
||||||
|
{activeProfile?.llmBaseUrl && (
|
||||||
|
<p style={{ margin: '10px 0 0', fontSize: 12, color: 'var(--settings-muted)' }}>
|
||||||
|
Local endpoint: <code style={codeInlineStyle}>{activeProfile.llmBaseUrl}</code>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Get key link */}
|
{/* Get key link */}
|
||||||
|
{openAiKeyWritable && (
|
||||||
<div style={helpStyle}>
|
<div style={helpStyle}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -194,6 +263,7 @@ export default function ApiKeysViewer() {
|
|||||||
Get your API key from OpenAI →
|
Get your API key from OpenAI →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -212,6 +282,38 @@ const featuresBoxStyle: CSSProperties = {
|
|||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const profileBoxStyle: CSSProperties = {
|
||||||
|
background: 'var(--settings-card-bg)',
|
||||||
|
border: '1px solid var(--settings-border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 16,
|
||||||
|
marginBottom: 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
const profileGridStyle: CSSProperties = {
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))',
|
||||||
|
gap: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
const profileLabelStyle: CSSProperties = {
|
||||||
|
fontSize: 11,
|
||||||
|
color: 'var(--settings-muted)',
|
||||||
|
marginBottom: 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
const profileValueStyle: CSSProperties = {
|
||||||
|
fontSize: 12,
|
||||||
|
color: 'var(--settings-subtext)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const profileNoteStyle: CSSProperties = {
|
||||||
|
margin: '12px 0 0',
|
||||||
|
fontSize: 12,
|
||||||
|
color: 'var(--settings-muted)',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
};
|
||||||
|
|
||||||
const featuresHeaderStyle: CSSProperties = {
|
const featuresHeaderStyle: CSSProperties = {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
|
|||||||
Reference in New Issue
Block a user