feat: add nanogpt provider selection (#5544) * add nanogpt provider selection * update payg text * fix: resync providers from endpoint --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -3648,6 +3648,19 @@ | ||
| 3648 | 3648 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> |
| 3649 | 3649 | </select> |
| 3650 | 3650 | </div> |
| 3651 | + <div> | |
| 3652 | + <h4> | |
| 3653 | + <span data-i18n="Model Providers">Model Providers</span> | |
| 3654 | + <i id="nanogpt_provider_warning" class="fa-solid fa-circle-exclamation displayNone" title="Deselect inapplicable provider(s) or select an applicable provider to avoid a 404 error."></i> | |
| 3655 | + </h4> | |
| 3656 | + <select id="nanogpt_provider"> | |
| 3657 | + <option value="" data-i18n="Auto">Auto</option> | |
| 3658 | + </select> | |
| 3659 | + <label class="checkbox_label marginTopBot5" for="nanogpt_payg_override" data-i18n="[title]Force NanoGPT pay-as-you-go billing for this request." title="Force NanoGPT pay-as-you-go billing for this request."> | |
| 3660 | + <input id="nanogpt_payg_override" type="checkbox" /> | |
| 3661 | + <span data-i18n="Use pay-as-you-go billing">Use pay-as-you-go billing</span> | |
| 3662 | + </label> | |
| 3663 | + </div> | |
| 3651 | 3664 | </div> |
| 3652 | 3665 | <div id="workers_ai_form" data-source="workers_ai"> |
| 3653 | 3666 | <h4><a href="https://dash.cloudflare.com/?to=/:account/ai/workers-ai/api-quick-start" target="_blank" rel="noopener noreferrer" data-i18n="Cloudflare Workers AI API Key">Cloudflare Workers AI API Key</a></h4> |
| @@ -80,7 +80,7 @@ import { t } from './i18n.js'; | ||
| 80 | 80 | import { ToolManager } from './tool-calling.js'; |
| 81 | 81 | import { accountStorage } from './util/AccountStorage.js'; |
| 82 | 82 | import { COMETAPI_IGNORE_PATTERNS, IGNORE_SYMBOL, MEDIA_DISPLAY, MEDIA_TYPE } from './constants.js'; |
| 83 | 83 | import { syncNanoGptProvidersForModel, syncOpenRouterProvidersForModel, updateNanoGptProvidersWarning, updateOpenRouterProvidersWarning } from './textgen-models.js'; |
| 84 | 84 | |
| 85 | 85 | export { |
| 86 | 86 | openai_messages_count, |
| @@ -329,6 +329,8 @@ export const settingsToUpdate = { | ||
| 329 | 329 | minimax_endpoint: ['#minimax_endpoint', 'minimax_endpoint', false, true], |
| 330 | 330 | electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], |
| 331 | 331 | nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true], |
| 332 | + nanogpt_provider: ['#nanogpt_provider', 'nanogpt_provider', false, true], | |
| 333 | + nanogpt_payg_override: ['#nanogpt_payg_override', 'nanogpt_payg_override', true, true], | |
| 332 | 334 | deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true], |
| 333 | 335 | aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], |
| 334 | 336 | xai_model: ['#model_xai_select', 'xai_model', false, true], |
| @@ -443,6 +445,8 @@ const default_settings = { | ||
| 443 | 445 | minimax_endpoint: MINIMAX_ENDPOINT.GLOBAL, |
| 444 | 446 | electronhub_model: 'gpt-4o-mini', |
| 445 | 447 | nanogpt_model: 'gpt-4o-mini', |
| 448 | + nanogpt_provider: '', | |
| 449 | + nanogpt_payg_override: false, | |
| 446 | 450 | deepseek_model: 'deepseek-v4-flash', |
| 447 | 451 | aimlapi_model: 'chatgpt-4o-latest', |
| 448 | 452 | xai_model: 'grok-3-beta', |
| @@ -2828,6 +2832,11 @@ export async function createGenerationParameters(settings, model, type, messages | ||
| 2828 | 2832 | generate_data.middleout = settings.openrouter_middleout; |
| 2829 | 2833 | } |
| 2830 | 2834 | |
| 2835 | + if (settings.chat_completion_source === chat_completion_sources.NANOGPT) { | |
| 2836 | + generate_data.nanogpt_provider = settings.nanogpt_provider; | |
| 2837 | + generate_data.nanogpt_payg_override = settings.nanogpt_payg_override; | |
| 2838 | + } | |
| 2839 | + | |
| 2831 | 2840 | if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(settings.chat_completion_source)) { |
| 2832 | 2841 | const stopStringsLimit = 5; |
| 2833 | 2842 | generate_data.top_k = Number(settings.top_k_openai); |
| @@ -4290,6 +4299,7 @@ function loadOpenAISettings(data, settings) { | ||
| 4290 | 4299 | |
| 4291 | 4300 | $('#openrouter_providers_chat').trigger('change'); |
| 4292 | 4301 | $('#openrouter_quantizations_chat').trigger('change'); |
| 4302 | + $('#nanogpt_provider').trigger('change'); | |
| 4293 | 4303 | $('#chat_completion_source').trigger('change'); |
| 4294 | 4304 | } |
| 4295 | 4305 | |
| @@ -4937,6 +4947,7 @@ function onSettingsPresetChange() { | ||
| 4937 | 4947 | $('#chat_completion_source').trigger('change'); |
| 4938 | 4948 | $('#openrouter_providers_chat').trigger('change'); |
| 4939 | 4949 | $('#openrouter_quantizations_chat').trigger('change'); |
| 4950 | + $('#nanogpt_provider').trigger('change'); | |
| 4940 | 4951 | } |
| 4941 | 4952 | |
| 4942 | 4953 | $('#openai_logit_bias_preset').trigger('change'); |
| @@ -5464,6 +5475,7 @@ async function onModelChange() { | ||
| 5464 | 5475 | |
| 5465 | 5476 | console.log('NanoGPT model changed to', value); |
| 5466 | 5477 | oai_settings.nanogpt_model = value; |
| 5478 | + syncNanoGptProvidersForModel(value, '#nanogpt_provider'); | |
| 5467 | 5479 | } |
| 5468 | 5480 | |
| 5469 | 5481 | if ($(this).is('#model_deepseek_select')) { |
| @@ -7134,6 +7146,17 @@ export function initOpenAI() { | ||
| 7134 | 7146 | saveSettingsDebounced(); |
| 7135 | 7147 | }); |
| 7136 | 7148 | |
| 7149 | + $('#nanogpt_provider').on('change', function () { | |
| 7150 | + oai_settings.nanogpt_provider = String($(this).val() || ''); | |
| 7151 | + updateNanoGptProvidersWarning('#nanogpt_provider'); | |
| 7152 | + saveSettingsDebounced(); | |
| 7153 | + }); | |
| 7154 | + | |
| 7155 | + $('#nanogpt_payg_override').on('input', function () { | |
| 7156 | + oai_settings.nanogpt_payg_override = !!$(this).prop('checked'); | |
| 7157 | + saveSettingsDebounced(); | |
| 7158 | + }); | |
| 7159 | + | |
| 7137 | 7160 | $('#bind_preset_to_connection').on('input', function () { |
| 7138 | 7161 | oai_settings.bind_preset_to_connection = !!$(this).prop('checked'); |
| 7139 | 7162 | saveSettingsDebounced(); |
| @@ -108,6 +108,214 @@ const OPENROUTER_PROVIDERS = [ | ||
| 108 | 108 | 'Z.AI', |
| 109 | 109 | ]; |
| 110 | 110 | |
| 111 | +/** | |
| 112 | + * List of NanoGPT providers. | |
| 113 | + * Providers endpoint: https://nano-gpt.com/api/models/providers | |
| 114 | + * @type {{id: string, label: string}[]} | |
| 115 | + */ | |
| 116 | +const NANOGPT_PROVIDERS = [ | |
| 117 | + { | |
| 118 | + 'id': 'akash', | |
| 119 | + 'label': 'Akash', | |
| 120 | + }, | |
| 121 | + { | |
| 122 | + 'id': 'alibaba', | |
| 123 | + 'label': 'Alibaba', | |
| 124 | + }, | |
| 125 | + { | |
| 126 | + 'id': 'ambient', | |
| 127 | + 'label': 'Ambient', | |
| 128 | + }, | |
| 129 | + { | |
| 130 | + 'id': 'arliai', | |
| 131 | + 'label': 'ArliAI', | |
| 132 | + }, | |
| 133 | + { | |
| 134 | + 'id': 'atlascloud', | |
| 135 | + 'label': 'AtlasCloud', | |
| 136 | + }, | |
| 137 | + { | |
| 138 | + 'id': 'azure', | |
| 139 | + 'label': 'Azure', | |
| 140 | + }, | |
| 141 | + { | |
| 142 | + 'id': 'awsbedrock', | |
| 143 | + 'label': 'Amazon Bedrock', | |
| 144 | + }, | |
| 145 | + { | |
| 146 | + 'id': 'baidu', | |
| 147 | + 'label': 'Baidu', | |
| 148 | + }, | |
| 149 | + { | |
| 150 | + 'id': 'baseten', | |
| 151 | + 'label': 'BaseTen', | |
| 152 | + }, | |
| 153 | + { | |
| 154 | + 'id': 'cerebras', | |
| 155 | + 'label': 'Cerebras', | |
| 156 | + }, | |
| 157 | + { | |
| 158 | + 'id': 'chutes', | |
| 159 | + 'label': 'Chutes', | |
| 160 | + }, | |
| 161 | + { | |
| 162 | + 'id': 'clarifai', | |
| 163 | + 'label': 'Clarifai', | |
| 164 | + }, | |
| 165 | + { | |
| 166 | + 'id': 'cloudflare', | |
| 167 | + 'label': 'Cloudflare', | |
| 168 | + }, | |
| 169 | + { | |
| 170 | + 'id': 'crusoe', | |
| 171 | + 'label': 'Crusoe', | |
| 172 | + }, | |
| 173 | + { | |
| 174 | + 'id': 'dekallm', | |
| 175 | + 'label': 'DekaLLM', | |
| 176 | + }, | |
| 177 | + { | |
| 178 | + 'id': 'deepinfra', | |
| 179 | + 'label': 'DeepInfra', | |
| 180 | + }, | |
| 181 | + { | |
| 182 | + 'id': 'deepseek', | |
| 183 | + 'label': 'DeepSeek', | |
| 184 | + }, | |
| 185 | + { | |
| 186 | + 'id': 'fireworks', | |
| 187 | + 'label': 'Fireworks', | |
| 188 | + }, | |
| 189 | + { | |
| 190 | + 'id': 'friendli', | |
| 191 | + 'label': 'Friendli', | |
| 192 | + }, | |
| 193 | + { | |
| 194 | + 'id': 'gmicloud', | |
| 195 | + 'label': 'GMICloud', | |
| 196 | + }, | |
| 197 | + { | |
| 198 | + 'id': 'lilac', | |
| 199 | + 'label': 'Lilac', | |
| 200 | + }, | |
| 201 | + { | |
| 202 | + 'id': 'google', | |
| 203 | + 'label': 'Google', | |
| 204 | + }, | |
| 205 | + { | |
| 206 | + 'id': 'groq', | |
| 207 | + 'label': 'Groq', | |
| 208 | + }, | |
| 209 | + { | |
| 210 | + 'id': 'hyperbolic', | |
| 211 | + 'label': 'Hyperbolic', | |
| 212 | + }, | |
| 213 | + { | |
| 214 | + 'id': 'ionet', | |
| 215 | + 'label': 'Io Net', | |
| 216 | + }, | |
| 217 | + { | |
| 218 | + 'id': 'inceptron', | |
| 219 | + 'label': 'Inceptron', | |
| 220 | + }, | |
| 221 | + { | |
| 222 | + 'id': 'mancer', | |
| 223 | + 'label': 'Mancer', | |
| 224 | + }, | |
| 225 | + { | |
| 226 | + 'id': 'mara', | |
| 227 | + 'label': 'Mara', | |
| 228 | + }, | |
| 229 | + { | |
| 230 | + 'id': 'meganova', | |
| 231 | + 'label': 'MegaNova', | |
| 232 | + }, | |
| 233 | + { | |
| 234 | + 'id': 'minimax', | |
| 235 | + 'label': 'MiniMax', | |
| 236 | + }, | |
| 237 | + { | |
| 238 | + 'id': 'modelrun', | |
| 239 | + 'label': 'ModelRun', | |
| 240 | + }, | |
| 241 | + { | |
| 242 | + 'id': 'moonshot', | |
| 243 | + 'label': 'Moonshot', | |
| 244 | + }, | |
| 245 | + { | |
| 246 | + 'id': 'morph', | |
| 247 | + 'label': 'Morph', | |
| 248 | + }, | |
| 249 | + { | |
| 250 | + 'id': 'ncompass', | |
| 251 | + 'label': 'NCompass', | |
| 252 | + }, | |
| 253 | + { | |
| 254 | + 'id': 'nebius', | |
| 255 | + 'label': 'Nebius', | |
| 256 | + }, | |
| 257 | + { | |
| 258 | + 'id': 'neuralwatt', | |
| 259 | + 'label': 'Neuralwatt', | |
| 260 | + }, | |
| 261 | + { | |
| 262 | + 'id': 'nextbit', | |
| 263 | + 'label': 'NextBit', | |
| 264 | + }, | |
| 265 | + { | |
| 266 | + 'id': 'novita', | |
| 267 | + 'label': 'Novita', | |
| 268 | + }, | |
| 269 | + { | |
| 270 | + 'id': 'parasail', | |
| 271 | + 'label': 'Parasail', | |
| 272 | + }, | |
| 273 | + { | |
| 274 | + 'id': 'phala', | |
| 275 | + 'label': 'Phala', | |
| 276 | + }, | |
| 277 | + { | |
| 278 | + 'id': 'redpill', | |
| 279 | + 'label': 'Redpill', | |
| 280 | + }, | |
| 281 | + { | |
| 282 | + 'id': 'sambanova', | |
| 283 | + 'label': 'SambaNova', | |
| 284 | + }, | |
| 285 | + { | |
| 286 | + 'id': 'sambanova-high-throughput', | |
| 287 | + 'label': 'SambaNova (High Throughput)', | |
| 288 | + }, | |
| 289 | + { | |
| 290 | + 'id': 'siliconflow', | |
| 291 | + 'label': 'SiliconFlow', | |
| 292 | + }, | |
| 293 | + { | |
| 294 | + 'id': 'streamlake', | |
| 295 | + 'label': 'StreamLake', | |
| 296 | + }, | |
| 297 | + { | |
| 298 | + 'id': 'tinfoil', | |
| 299 | + 'label': 'Tinfoil', | |
| 300 | + }, | |
| 301 | + { | |
| 302 | + 'id': 'together', | |
| 303 | + 'label': 'Together', | |
| 304 | + }, | |
| 305 | + { | |
| 306 | + 'id': 'venice', | |
| 307 | + 'label': 'Venice', | |
| 308 | + }, | |
| 309 | + { | |
| 310 | + 'id': 'wandb', | |
| 311 | + 'label': 'Weights & Biases', | |
| 312 | + }, | |
| 313 | + { | |
| 314 | + 'id': 'zai', | |
| 315 | + 'label': 'Z.AI', | |
| 316 | + }, | |
| 317 | +]; | |
| 318 | + | |
| 111 | 319 | const OPENROUTER_PROVIDER_WARNING_SELECTORS = { |
| 112 | 320 | '#openrouter_providers_text': { |
| 113 | 321 | fallbackSelector: '#openrouter_allow_fallbacks_textgenerationwebui', |
| @@ -187,6 +395,72 @@ export async function syncOpenRouterProvidersForModel(modelId, providersSelector | ||
| 187 | 395 | } |
| 188 | 396 | } |
| 189 | 397 | |
| 398 | +export async function syncNanoGptProvidersForModel(modelId, providersSelector) { | |
| 399 | + const $providers = $(providersSelector); | |
| 400 | + | |
| 401 | + const refreshWarningState = () => { | |
| 402 | + updateNanoGptProvidersWarning(providersSelector); | |
| 403 | + }; | |
| 404 | + | |
| 405 | + if (!modelId) { | |
| 406 | + $providers.find('option').prop('disabled', false); | |
| 407 | + $providers.trigger('change.select2'); | |
| 408 | + refreshWarningState(); | |
| 409 | + return; | |
| 410 | + } | |
| 411 | + | |
| 412 | + try { | |
| 413 | + const response = await fetch('/api/nanogpt/models/providers', { | |
| 414 | + method: 'POST', | |
| 415 | + headers: getRequestHeaders(), | |
| 416 | + body: JSON.stringify({ model: modelId }), | |
| 417 | + }); | |
| 418 | + | |
| 419 | + if (!response.ok) { | |
| 420 | + refreshWarningState(); | |
| 421 | + return; | |
| 422 | + } | |
| 423 | + | |
| 424 | + const data = await response.json(); | |
| 425 | + const providerIds = Array.isArray(data?.providers) ? data.providers : []; | |
| 426 | + | |
| 427 | + if (!data?.supportsProviderSelection || providerIds.length === 0) { | |
| 428 | + $providers.find('option').each(function () { | |
| 429 | + $(this).prop('disabled', Boolean($(this).val())); | |
| 430 | + }); | |
| 431 | + $providers.trigger('change').trigger('change.select2'); | |
| 432 | + refreshWarningState(); | |
| 433 | + return; | |
| 434 | + } | |
| 435 | + | |
| 436 | + $providers.find('option').each(function () { | |
| 437 | + const value = $(this).val(); | |
| 438 | + const isAvailable = !value || providerIds.includes(value); | |
| 439 | + $(this).prop('disabled', !isAvailable); | |
| 440 | + }); | |
| 441 | + | |
| 442 | + $providers.trigger('change.select2'); | |
| 443 | + refreshWarningState(); | |
| 444 | + } catch (error) { | |
| 445 | + console.error('Failed to fetch NanoGPT providers for model', error); | |
| 446 | + refreshWarningState(); | |
| 447 | + } | |
| 448 | +} | |
| 449 | + | |
| 450 | +export function updateNanoGptProvidersWarning(providersSelector) { | |
| 451 | + const $providers = $(providersSelector); | |
| 452 | + | |
| 453 | + if ($providers.length === 0) { | |
| 454 | + return; | |
| 455 | + } | |
| 456 | + | |
| 457 | + const selectedCount = $providers.find('option:selected').length; | |
| 458 | + const applicableSelectedCount = $providers.find('option:selected:not(:disabled)').length; | |
| 459 | + const showWarning = selectedCount > 0 && applicableSelectedCount === 0; | |
| 460 | + | |
| 461 | + $('#nanogpt_provider_warning').toggleClass('displayNone', !showWarning); | |
| 462 | +} | |
| 463 | + | |
| 190 | 464 | export async function loadOllamaModels(data) { |
| 191 | 465 | if (!Array.isArray(data)) { |
| 192 | 466 | console.error('Invalid Ollama models data', data); |
| @@ -1084,6 +1358,14 @@ export function initTextGenModels() { | ||
| 1084 | 1358 | })); |
| 1085 | 1359 | } |
| 1086 | 1360 | |
| 1361 | + const nanoGptProvidersSelect = $('#nanogpt_provider'); | |
| 1362 | + for (const provider of NANOGPT_PROVIDERS) { | |
| 1363 | + nanoGptProvidersSelect.append($('<option>', { | |
| 1364 | + value: provider.id, | |
| 1365 | + text: provider.label, | |
| 1366 | + })); | |
| 1367 | + } | |
| 1368 | + | |
| 1087 | 1369 | if (!isMobile()) { |
| 1088 | 1370 | $('#mancer_model').select2({ |
| 1089 | 1371 | placeholder: t`Select a model`, |
| @@ -1178,5 +1460,13 @@ export function initTextGenModels() { | ||
| 1178 | 1460 | $(this).append($element); |
| 1179 | 1461 | $(this).trigger('change'); |
| 1180 | 1462 | }); |
| 1463 | + nanoGptProvidersSelect.select2({ | |
| 1464 | + sorter: data => data.sort((a, b) => a.text.localeCompare(b.text)), | |
| 1465 | + placeholder: t`Select providers. No selection = all providers.`, | |
| 1466 | + searchInputPlaceholder: t`Search providers...`, | |
| 1467 | + searchInputCssClass: 'text_pole', | |
| 1468 | + width: '100%', | |
| 1469 | + allowClear: true, | |
| 1470 | + }); | |
| 1181 | 1471 | } |
| 1182 | 1472 | } |
| @@ -2382,6 +2382,13 @@ router.post('/generate', async function (request, response) { | ||
| 2382 | 2382 | apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT, request.body.secret_id); |
| 2383 | 2383 | headers = {}; |
| 2384 | 2384 | bodyParams = {}; |
| 2385 | + if (request.body.nanogpt_provider) { | |
| 2386 | + headers['X-Provider'] = request.body.nanogpt_provider; | |
| 2387 | + } | |
| 2388 | + if (request.body.nanogpt_payg_override) { | |
| 2389 | + headers['X-Billing-Mode'] = 'paygo'; | |
| 2390 | + bodyParams['billing_mode'] = 'paygo'; | |
| 2391 | + } | |
| 2385 | 2392 | if (request.body.enable_web_search && !/:online$/.test(request.body.model)) { |
| 2386 | 2393 | request.body.model = `${request.body.model}:online`; |
| 2387 | 2394 | } |
| @@ -100,3 +100,39 @@ router.post('/credits', async (req, res) => { | ||
| 100 | 100 | return res.sendStatus(500); |
| 101 | 101 | } |
| 102 | 102 | }); |
| 103 | + | |
| 104 | +router.post('/models/providers', async (req, res) => { | |
| 105 | + try { | |
| 106 | + const { model } = req.body; | |
| 107 | + | |
| 108 | + if (!model) { | |
| 109 | + return res.status(400).json({ supportsProviderSelection: false, providers: [] }); | |
| 110 | + } | |
| 111 | + | |
| 112 | + const encodedModel = encodeURIComponent(model); | |
| 113 | + const response = await fetch(`${API_NANOGPT}/models/${encodedModel}/providers`, { | |
| 114 | + method: 'GET', | |
| 115 | + headers: { | |
| 116 | + 'Accept': 'application/json', | |
| 117 | + }, | |
| 118 | + }); | |
| 119 | + | |
| 120 | + if (!response.ok) { | |
| 121 | + return res.json({ supportsProviderSelection: false, providers: [] }); | |
| 122 | + } | |
| 123 | + | |
| 124 | + /** @type {any} */ | |
| 125 | + const data = await response.json(); | |
| 126 | + const providers = Array.isArray(data?.providers) | |
| 127 | + ? data.providers.filter(p => p?.available !== false).map(p => p.provider).filter(Boolean) | |
| 128 | + : []; | |
| 129 | + | |
| 130 | + return res.json({ | |
| 131 | + supportsProviderSelection: Boolean(data?.supportsProviderSelection), | |
| 132 | + providers, | |
| 133 | + }); | |
| 134 | + } catch (error) { | |
| 135 | + console.error(error); | |
| 136 | + return res.sendStatus(500); | |
| 137 | + } | |
| 138 | +}); | |