neoagent 2.4.1-beta.39 → 2.4.1-beta.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -111,6 +111,17 @@ GOOGLE_AI_KEY=your-google-ai-key-here
111
111
  # • MiniMax-M2.7 via the Anthropic-compatible MiniMax endpoint
112
112
  MINIMAX_API_KEY=your-minimax-api-key-here
113
113
 
114
+ # NVIDIA NIM API key — used for:
115
+ # • Free-tier and paid models on NVIDIA's hosted inference (Nemotron, Kimi, Llama 4, DeepSeek, etc.)
116
+ # Get your key at: https://build.nvidia.com
117
+ NVIDIA_API_KEY=your-nvidia-nim-api-key-here
118
+
119
+ # OpenRouter API key — used for:
120
+ # • 300+ models from all major providers through a single API
121
+ # • Includes free-tier models (no credit needed for :free variants)
122
+ # Get your key at: https://openrouter.ai/keys
123
+ OPENROUTER_API_KEY=your-openrouter-api-key-here
124
+
114
125
  # GitHub Copilot OAuth token — used for:
115
126
  # • Copilot-backed chat/coding models (gpt-5.3, gpt-4.1)
116
127
  # Set via: neoagent login github-copilot
@@ -40,6 +40,8 @@ At least one key is required unless you only use local Ollama.
40
40
  | `XAI_BASE_URL` | Optional xAI-compatible base URL override |
41
41
  | `GOOGLE_AI_KEY` | Gemini (Google) |
42
42
  | `MINIMAX_API_KEY` | MiniMax (including `MiniMax-M2.7`) |
43
+ | `NVIDIA_API_KEY` | NVIDIA NIM (free-tier + paid: Nemotron, Kimi, Llama 4, DeepSeek, etc.) |
44
+ | `OPENROUTER_API_KEY` | OpenRouter — access 300+ models from all providers through one API; free-tier models included |
43
45
  | `BRAVE_SEARCH_API_KEY` | Brave Search for the `web_search` tool |
44
46
  | `OPENAI_BASE_URL` | Optional OpenAI-compatible base URL override |
45
47
  | `ANTHROPIC_BASE_URL` | Optional Anthropic-compatible base URL override |
@@ -13,6 +13,7 @@ class _ModelPickerOption {
13
13
  this.color,
14
14
  this.icon,
15
15
  this.isAuto = false,
16
+ this.priceTier,
16
17
  });
17
18
 
18
19
  final String value;
@@ -22,6 +23,8 @@ class _ModelPickerOption {
22
23
  final Color? color;
23
24
  final IconData? icon;
24
25
  final bool isAuto;
26
+ /// 'free' | 'cheap' | 'medium' | 'expensive' | null
27
+ final String? priceTier;
25
28
  }
26
29
 
27
30
  // ─── Provider helpers ─────────────────────────────────────────────────────────
@@ -43,6 +46,9 @@ Color _providerPickerColor(String provider) {
43
46
  if (p.contains('copilot') || p.contains('github')) {
44
47
  return const Color(0xFF238636);
45
48
  }
49
+ if (p.contains('openrouter')) return const Color(0xFF6366F1);
50
+ if (p.contains('nvidia')) return const Color(0xFF76B900);
51
+ if (p.contains('minimax')) return const Color(0xFF0EA5E9);
46
52
  if (p.contains('deepgram')) return const Color(0xFF13D4A0);
47
53
  if (p.contains('ollama')) return const Color(0xFF6C8EBF);
48
54
  return const Color(0xFF7C8CFF);
@@ -64,6 +70,9 @@ IconData _providerPickerIcon(String provider) {
64
70
  }
65
71
  if (p.contains('grok') || p.contains('xai')) return Icons.psychology_rounded;
66
72
  if (p.contains('copilot') || p.contains('github')) return Icons.code_rounded;
73
+ if (p.contains('openrouter')) return Icons.hub_rounded;
74
+ if (p.contains('nvidia')) return Icons.speed_rounded;
75
+ if (p.contains('minimax')) return Icons.water_rounded;
67
76
  if (p.contains('deepgram')) return Icons.hearing_rounded;
68
77
  if (p.contains('ollama')) return Icons.device_hub_rounded;
69
78
  return Icons.memory_rounded;
@@ -78,10 +87,15 @@ String _providerPickerLabel(String id) {
78
87
  'meta': 'Meta',
79
88
  'mistral': 'Mistral',
80
89
  'grok': 'xAI',
90
+ 'grok-oauth': 'xAI (OAuth)',
81
91
  'xai': 'xAI',
82
92
  'ollama': 'Ollama',
83
93
  'github-copilot': 'GitHub Copilot',
84
- 'openai-codex': 'OpenAI',
94
+ 'openai-codex': 'OpenAI Codex',
95
+ 'claude-code': 'Claude Code',
96
+ 'openrouter': 'OpenRouter',
97
+ 'nvidia': 'NVIDIA NIM',
98
+ 'minimax': 'MiniMax',
85
99
  'deepgram': 'Deepgram',
86
100
  };
87
101
  return labels[id.toLowerCase()] ?? id;
@@ -113,6 +127,7 @@ List<_ModelPickerOption> _modelPickerOptions(
113
127
  subtitle: parts.isNotEmpty ? parts.join(' · ') : null,
114
128
  color: _providerPickerColor(m.provider),
115
129
  icon: _providerPickerIcon(m.provider),
130
+ priceTier: m.priceTier,
116
131
  );
117
132
  }),
118
133
  ];
@@ -500,6 +515,41 @@ class _ModelPickerDialogState extends State<_ModelPickerDialog> {
500
515
  // Sub-widgets
501
516
  // ─────────────────────────────────────────────────────────────────────────────
502
517
 
518
+ class _PriceTierChip extends StatelessWidget {
519
+ const _PriceTierChip({required this.tier});
520
+
521
+ final String tier;
522
+
523
+ @override
524
+ Widget build(BuildContext context) {
525
+ final (String text, Color color) = switch (tier) {
526
+ 'free' => ('FREE', const Color(0xFF22C55E)),
527
+ 'cheap' => ('\$', const Color(0xFF4ADE80)),
528
+ 'medium' => ('\$\$', const Color(0xFFF59E0B)),
529
+ 'expensive' => ('\$\$\$', const Color(0xFFEF4444)),
530
+ _ => ('', Colors.transparent),
531
+ };
532
+ if (text.isEmpty) return const SizedBox.shrink();
533
+ return Container(
534
+ padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
535
+ decoration: BoxDecoration(
536
+ color: color.withValues(alpha: 0.15),
537
+ borderRadius: BorderRadius.circular(5),
538
+ border: Border.all(color: color.withValues(alpha: 0.35), width: 0.8),
539
+ ),
540
+ child: Text(
541
+ text,
542
+ style: TextStyle(
543
+ fontSize: 10,
544
+ fontWeight: FontWeight.w700,
545
+ color: color,
546
+ letterSpacing: 0.2,
547
+ ),
548
+ ),
549
+ );
550
+ }
551
+ }
552
+
503
553
  class _PickerGroupHeader extends StatelessWidget {
504
554
  const _PickerGroupHeader({required this.label, required this.color});
505
555
 
@@ -612,6 +662,10 @@ class _PickerRow extends StatelessWidget {
612
662
  ),
613
663
  ),
614
664
  const SizedBox(width: 8),
665
+ if (option.priceTier != null) ...<Widget>[
666
+ _PriceTierChip(tier: option.priceTier!),
667
+ const SizedBox(width: 6),
668
+ ],
615
669
  SizedBox(
616
670
  width: 20,
617
671
  child: selected
@@ -2015,6 +2015,7 @@ class ModelMeta {
2015
2015
  this.available = true,
2016
2016
  this.providerStatus = '',
2017
2017
  this.providerStatusLabel = '',
2018
+ this.priceTier,
2018
2019
  });
2019
2020
 
2020
2021
  factory ModelMeta.fromJson(Map<dynamic, dynamic> json) {
@@ -2026,6 +2027,7 @@ class ModelMeta {
2026
2027
  available: json['available'] != false,
2027
2028
  providerStatus: json['providerStatus']?.toString() ?? '',
2028
2029
  providerStatusLabel: json['providerStatusLabel']?.toString() ?? '',
2030
+ priceTier: json['priceTier']?.toString(),
2029
2031
  );
2030
2032
  }
2031
2033
 
@@ -2036,6 +2038,8 @@ class ModelMeta {
2036
2038
  final bool available;
2037
2039
  final String providerStatus;
2038
2040
  final String providerStatusLabel;
2041
+ /// Pricing tier: 'free' | 'cheap' | 'medium' | 'expensive' | null (unknown)
2042
+ final String? priceTier;
2039
2043
  }
2040
2044
 
2041
2045
  class AiProviderConfig {
@@ -187,7 +187,6 @@ class _SettingsPanelState extends State<SettingsPanel> {
187
187
  final Map<String, bool> _providerEnabled = <String, bool>{};
188
188
  final Map<String, TextEditingController> _providerBaseUrlControllers =
189
189
  <String, TextEditingController>{};
190
- final Set<String> _expandedProviderIds = <String>{};
191
190
 
192
191
  // Inline runtime test state — ephemeral, not stored in controller.
193
192
  bool _cliTestRunning = false;
@@ -868,88 +867,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
868
867
  const _SectionTitle('Models'),
869
868
  const SizedBox(height: 10),
870
869
  Text(
871
- 'Enable providers, then choose defaults for chat, agents, fallback behavior, and smart routing.',
870
+ 'Choose defaults for chat, agents, fallback behavior, and smart routing.',
872
871
  style: TextStyle(color: _textSecondary, height: 1.45),
873
872
  ),
874
873
  const SizedBox(height: 16),
875
- Text(
876
- 'Providers',
877
- style: TextStyle(
878
- fontWeight: FontWeight.w700,
879
- color: _textPrimary,
880
- ),
881
- ),
882
- const SizedBox(height: 14),
883
- if (controller.aiProviders.isEmpty)
884
- Text(
885
- 'Provider metadata is unavailable on this server version.',
886
- style: TextStyle(color: _textSecondary),
887
- )
888
- else
889
- LayoutBuilder(
890
- builder: (context, constraints) {
891
- final compact = constraints.maxWidth < 960;
892
- final cardWidth = compact
893
- ? constraints.maxWidth
894
- : (constraints.maxWidth - 16) / 2;
895
- return Wrap(
896
- spacing: 16,
897
- runSpacing: 16,
898
- children: controller.aiProviders
899
- .where(
900
- (provider) =>
901
- provider.available ||
902
- _providerEnabled[provider.id] == true ||
903
- controller
904
- .aiProviderConfigs[provider.id]
905
- ?.enabled ==
906
- true,
907
- )
908
- .map((provider) {
909
- return SizedBox(
910
- width: cardWidth,
911
- child: _AiProviderCard(
912
- provider: provider,
913
- enabled:
914
- _providerEnabled[provider.id] ??
915
- controller
916
- .aiProviderConfigs[provider.id]
917
- ?.enabled ??
918
- true,
919
- models: controller.supportedModels
920
- .where(
921
- (model) => model.provider == provider.id,
922
- )
923
- .toList(),
924
- baseUrlController:
925
- _providerBaseUrlControllers[provider.id]!,
926
- expanded: _expandedProviderIds.contains(
927
- provider.id,
928
- ),
929
- onEnabledChanged: (value) {
930
- setState(() {
931
- _providerEnabled[provider.id] = value;
932
- });
933
- },
934
- onExpandToggle: () {
935
- setState(() {
936
- if (_expandedProviderIds.contains(
937
- provider.id,
938
- )) {
939
- _expandedProviderIds.remove(provider.id);
940
- } else {
941
- _expandedProviderIds.add(provider.id);
942
- }
943
- });
944
- },
945
- ),
946
- );
947
- })
948
- .toList(),
949
- );
950
- },
951
- ),
952
- const Divider(height: 32),
953
874
  Text(
954
875
  'Default Routing',
955
876
  style: TextStyle(
@@ -2205,244 +2126,6 @@ class _SettingsPanelState extends State<SettingsPanel> {
2205
2126
  }
2206
2127
  }
2207
2128
 
2208
- class _AiProviderCard extends StatelessWidget {
2209
- const _AiProviderCard({
2210
- required this.provider,
2211
- required this.enabled,
2212
- required this.expanded,
2213
- required this.models,
2214
- required this.baseUrlController,
2215
- required this.onEnabledChanged,
2216
- required this.onExpandToggle,
2217
- });
2218
-
2219
- final AiProviderMeta provider;
2220
- final bool enabled;
2221
- final bool expanded;
2222
- final List<ModelMeta> models;
2223
- final TextEditingController baseUrlController;
2224
- final ValueChanged<bool> onEnabledChanged;
2225
- final VoidCallback onExpandToggle;
2226
-
2227
- @override
2228
- Widget build(BuildContext context) {
2229
- final availableCount = models.where((model) => model.available).length;
2230
- final hasAdvancedFields = provider.supportsBaseUrl || models.isNotEmpty;
2231
- return Container(
2232
- padding: const EdgeInsets.all(16),
2233
- decoration: BoxDecoration(
2234
- color: _bgSecondary,
2235
- borderRadius: BorderRadius.circular(18),
2236
- border: Border.all(color: _borderLight),
2237
- ),
2238
- child: Column(
2239
- crossAxisAlignment: CrossAxisAlignment.start,
2240
- children: <Widget>[
2241
- Row(
2242
- crossAxisAlignment: CrossAxisAlignment.start,
2243
- children: <Widget>[
2244
- Container(
2245
- width: 44,
2246
- height: 44,
2247
- decoration: BoxDecoration(
2248
- color: _accentMuted,
2249
- borderRadius: BorderRadius.circular(14),
2250
- ),
2251
- child: Icon(provider.icon, color: _accentHover),
2252
- ),
2253
- const SizedBox(width: 12),
2254
- Expanded(
2255
- child: Column(
2256
- crossAxisAlignment: CrossAxisAlignment.start,
2257
- children: <Widget>[
2258
- Text(
2259
- provider.label,
2260
- style: TextStyle(
2261
- fontSize: 16,
2262
- fontWeight: FontWeight.w700,
2263
- ),
2264
- ),
2265
- const SizedBox(height: 4),
2266
- Text(
2267
- provider.description,
2268
- maxLines: 2,
2269
- overflow: TextOverflow.ellipsis,
2270
- style: TextStyle(color: _textSecondary, height: 1.4),
2271
- ),
2272
- ],
2273
- ),
2274
- ),
2275
- const SizedBox(width: 8),
2276
- Column(
2277
- crossAxisAlignment: CrossAxisAlignment.end,
2278
- children: <Widget>[
2279
- _StatusPill(
2280
- label: enabled ? provider.statusLabel : 'Disabled',
2281
- color: enabled ? provider.statusColor : _textSecondary,
2282
- ),
2283
- const SizedBox(height: 8),
2284
- InkWell(
2285
- onTap: hasAdvancedFields || models.isNotEmpty
2286
- ? onExpandToggle
2287
- : null,
2288
- borderRadius: BorderRadius.circular(999),
2289
- child: Container(
2290
- padding: const EdgeInsets.symmetric(
2291
- horizontal: 10,
2292
- vertical: 6,
2293
- ),
2294
- decoration: BoxDecoration(
2295
- color: _bgCard,
2296
- borderRadius: BorderRadius.circular(999),
2297
- border: Border.all(color: _border),
2298
- ),
2299
- child: Row(
2300
- mainAxisSize: MainAxisSize.min,
2301
- children: <Widget>[
2302
- Text(
2303
- expanded ? 'Hide' : 'Setup',
2304
- style: TextStyle(fontSize: 12),
2305
- ),
2306
- const SizedBox(width: 4),
2307
- Icon(
2308
- expanded
2309
- ? Icons.keyboard_arrow_up
2310
- : Icons.keyboard_arrow_down,
2311
- size: 16,
2312
- color: _textSecondary,
2313
- ),
2314
- ],
2315
- ),
2316
- ),
2317
- ),
2318
- ],
2319
- ),
2320
- ],
2321
- ),
2322
- const SizedBox(height: 12),
2323
- Wrap(
2324
- spacing: 8,
2325
- runSpacing: 8,
2326
- children: <Widget>[
2327
- _MetaPill(
2328
- label: '$availableCount of ${models.length} models ready',
2329
- icon: Icons.memory_outlined,
2330
- ),
2331
- if (provider.supportsApiKey && provider.credentialConfigured)
2332
- const _MetaPill(
2333
- label: 'Credentials ready',
2334
- icon: Icons.lock_outline,
2335
- ),
2336
- if (provider.supportsApiKey && !provider.credentialConfigured)
2337
- const _MetaPill(
2338
- label: 'Credentials needed',
2339
- icon: Icons.admin_panel_settings_outlined,
2340
- ),
2341
- if (provider.supportsBaseUrl &&
2342
- baseUrlController.text.trim().isNotEmpty)
2343
- _MetaPill(
2344
- label: _friendlyBaseUrlLabel(baseUrlController.text.trim()),
2345
- icon: Icons.link_outlined,
2346
- ),
2347
- ],
2348
- ),
2349
- const SizedBox(height: 12),
2350
- Container(
2351
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
2352
- decoration: BoxDecoration(
2353
- color: _bgCard,
2354
- borderRadius: BorderRadius.circular(14),
2355
- border: Border.all(color: _border),
2356
- ),
2357
- child: Row(
2358
- children: <Widget>[
2359
- Expanded(
2360
- child: Text(
2361
- provider.availabilityReason,
2362
- style: TextStyle(color: _textSecondary, height: 1.35),
2363
- ),
2364
- ),
2365
- const SizedBox(width: 12),
2366
- Switch(value: enabled, onChanged: onEnabledChanged),
2367
- ],
2368
- ),
2369
- ),
2370
- if (expanded) ...<Widget>[
2371
- const SizedBox(height: 14),
2372
- if (provider.supportsApiKey)
2373
- Container(
2374
- width: double.infinity,
2375
- padding: const EdgeInsets.all(12),
2376
- margin: const EdgeInsets.only(bottom: 12),
2377
- decoration: BoxDecoration(
2378
- color: _bgCard,
2379
- borderRadius: BorderRadius.circular(14),
2380
- border: Border.all(color: _border),
2381
- ),
2382
- child: Text(
2383
- provider.credentialConfigured
2384
- ? 'Credentials for this provider are already available to the runtime.'
2385
- : 'Credentials for this provider are managed outside this workspace UI. Finish the server or admin setup, then return here to enable routing.',
2386
- style: TextStyle(color: _textSecondary, height: 1.35),
2387
- ),
2388
- ),
2389
- if (provider.supportsBaseUrl) ...<Widget>[
2390
- TextField(
2391
- controller: baseUrlController,
2392
- keyboardType: TextInputType.url,
2393
- autocorrect: false,
2394
- decoration: InputDecoration(
2395
- labelText: provider.id == 'ollama'
2396
- ? 'Server URL'
2397
- : 'Base URL',
2398
- helperText: provider.defaultBaseUrl.trim().isEmpty
2399
- ? 'Optional override.'
2400
- : 'Default: ${provider.defaultBaseUrl}',
2401
- ),
2402
- ),
2403
- const SizedBox(height: 12),
2404
- ],
2405
- if (models.isNotEmpty) ...<Widget>[
2406
- Text('Models', style: TextStyle(fontWeight: FontWeight.w700)),
2407
- const SizedBox(height: 8),
2408
- Wrap(
2409
- spacing: 8,
2410
- runSpacing: 8,
2411
- children: models
2412
- .map(
2413
- (model) => Container(
2414
- padding: const EdgeInsets.symmetric(
2415
- horizontal: 10,
2416
- vertical: 8,
2417
- ),
2418
- decoration: BoxDecoration(
2419
- color: model.available ? _bgCard : _bgPrimary,
2420
- borderRadius: BorderRadius.circular(999),
2421
- border: Border.all(
2422
- color: model.available ? _border : _borderLight,
2423
- ),
2424
- ),
2425
- child: Text(
2426
- model.label,
2427
- style: TextStyle(
2428
- fontSize: 12,
2429
- color: model.available
2430
- ? _textPrimary
2431
- : _textSecondary,
2432
- ),
2433
- ),
2434
- ),
2435
- )
2436
- .toList(),
2437
- ),
2438
- ],
2439
- ],
2440
- ],
2441
- ),
2442
- );
2443
- }
2444
- }
2445
-
2446
2129
  class _RoutingSelectCard extends StatelessWidget {
2447
2130
  const _RoutingSelectCard({
2448
2131
  required this.label,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.4.1-beta.39",
3
+ "version": "2.4.1-beta.40",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -1 +1 @@
1
- 1636c605494dd3c6974f0d4eab186b24
1
+ 7955c5e23a99fe6d57d7bfed7573998e
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"c416acfeb8126e097f758c664aaa3da929e27d
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "2734177113" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "1994209727" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });