Skip to content
Prev Previous commit
Next Next commit
feat(ai): expose openai-compatible provider in the admin providers UI
  • Loading branch information
Mario
Mario committed Jun 28, 2026
commit 533c2935da9fcfbaa6a81f7d0b456d49edc2def6
7 changes: 4 additions & 3 deletions src/admin/ai/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const ProviderId = Type.Union([
Type.Literal('openai'),
Type.Literal('ollama'),
Type.Literal('openrouter'),
Type.Literal('openai-compatible'),
])

const AuthMode = Type.Union([
Expand Down Expand Up @@ -170,13 +171,13 @@ export async function listCredentials(signal?: AbortSignal): Promise<CredentialV

export type CreateCredentialBody =
| {
providerId: 'anthropic' | 'openai' | 'ollama' | 'openrouter'
providerId: 'anthropic' | 'openai' | 'ollama' | 'openrouter' | 'openai-compatible'
authMode: 'apiKey'
displayLabel: string
apiKey: string
}
| {
providerId: 'anthropic' | 'openai' | 'ollama' | 'openrouter'
providerId: 'anthropic' | 'openai' | 'ollama' | 'openrouter' | 'openai-compatible'
authMode: 'baseUrl'
displayLabel: string
baseUrl: string
Expand Down Expand Up @@ -224,7 +225,7 @@ export async function testCredential(id: string): Promise<TestResult> {
// ---------------------------------------------------------------------------

export async function listModels(
providerId: 'anthropic' | 'openai' | 'ollama' | 'openrouter',
providerId: 'anthropic' | 'openai' | 'ollama' | 'openrouter' | 'openai-compatible',
credentialId?: string,
): Promise<AiModel[]> {
const body = await apiRequest(`/admin/api/ai/providers/${providerId}/models`, {
Expand Down
15 changes: 11 additions & 4 deletions src/admin/pages/ai/tabs/ProvidersTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { ApiError } from '@core/http'
import styles from '../AiPage.module.css'
import { getErrorMessage } from '@core/utils/errorMessage'

type ProviderId = 'anthropic' | 'openai' | 'ollama' | 'openrouter'
type ProviderId = 'anthropic' | 'openai' | 'ollama' | 'openrouter' | 'openai-compatible'
type AuthMode = 'apiKey' | 'baseUrl'

// Each provider has exactly one credential shape; the UI derives it instead
Expand All @@ -37,6 +37,7 @@ const PROVIDERS: Array<{ id: ProviderId; label: string; authMode: AuthMode }> =
{ id: 'openai', label: 'OpenAI', authMode: 'apiKey' },
{ id: 'openrouter', label: 'OpenRouter', authMode: 'apiKey' },
{ id: 'ollama', label: 'Ollama (local)', authMode: 'baseUrl' },
{ id: 'openai-compatible', label: 'OpenAI-Compatible', authMode: 'baseUrl' },
]

const AUTH_MODE_LABEL: Record<AuthMode, string> = {
Expand All @@ -49,12 +50,14 @@ const PROVIDER_LABEL: Record<ProviderId, string> = {
openai: 'OpenAI',
openrouter: 'OpenRouter',
ollama: 'Ollama',
'openai-compatible': 'OpenAI-Compatible',
}

// Hint text for the API-key field, per provider key prefix.
const API_KEY_PLACEHOLDER: Partial<Record<ProviderId, string>> = {
anthropic: 'sk-ant-...',
openrouter: 'sk-or-...',
'openai-compatible': 'sk-... (optional)',
}

async function deleteCredentialAction(
Expand Down Expand Up @@ -278,12 +281,14 @@ function AddCredentialDialog({
const [providerId, setProviderId] = useState<ProviderId>('anthropic')
const [displayLabel, setDisplayLabel] = useState('')
const [apiKey, setApiKey] = useState('')
const [baseUrl, setBaseUrl] = useState('http://localhost:11434')
const [baseUrl, setBaseUrl] = useState('')
const [busy, setBusy] = useState(false)
const [error, setError] = useState<string | null>(null)

const providerSpec = PROVIDERS.find((p) => p.id === providerId)!
const effectiveAuthMode = providerSpec.authMode
const baseUrlPlaceholder =
providerId === 'ollama' ? 'http://localhost:11434' : 'https://api.your-provider.com/v1'

async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
Expand Down Expand Up @@ -360,12 +365,14 @@ function AddCredentialDialog({
id={baseUrlInputId}
value={baseUrl}
onChange={(e) => setBaseUrl(e.currentTarget.value)}
placeholder="http://localhost:11434"
placeholder={baseUrlPlaceholder}
required
/>
</div>
<div className={styles.dialogField}>
<label htmlFor={apiKeyInputId} className={styles.dialogFieldLabel}>Bearer token (optional)</label>
<label htmlFor={apiKeyInputId} className={styles.dialogFieldLabel}>
{providerId === 'ollama' ? 'Bearer token (optional)' : 'API key (optional)'}
</label>
<Input
id={apiKeyInputId}
type="password"
Expand Down