@lobehub/chat 1.133.5 โ†’ 1.134.0

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/apps/desktop/src/main/appBrowsers.ts +51 -0
  3. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +72 -1
  4. package/apps/desktop/src/main/core/browser/BrowserManager.ts +88 -18
  5. package/changelog/v1.json +21 -0
  6. package/package.json +1 -1
  7. package/packages/database/src/models/__tests__/aiModel.test.ts +3 -0
  8. package/packages/database/src/models/aiModel.ts +18 -2
  9. package/packages/electron-client-ipc/src/events/windows.ts +39 -0
  10. package/packages/model-bank/src/aiModels/google.ts +64 -2
  11. package/packages/model-bank/src/types/aiModel.ts +13 -9
  12. package/packages/model-runtime/src/providers/google/createImage.ts +13 -4
  13. package/src/app/[variants]/(main)/_layout/Desktop/DesktopLayoutContainer.tsx +4 -2
  14. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/index.tsx +3 -1
  15. package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +3 -1
  16. package/src/app/[variants]/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicItem/TopicContent.tsx +25 -1
  17. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx +19 -2
  18. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +27 -1
  19. package/src/app/[variants]/(main)/chat/_layout/Desktop/SessionPanel.tsx +11 -1
  20. package/src/app/[variants]/(main)/chat/features/TogglePanelButton.tsx +6 -0
  21. package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +6 -3
  22. package/src/config/featureFlags/index.ts +2 -2
  23. package/src/config/featureFlags/schema.test.ts +165 -9
  24. package/src/config/featureFlags/schema.ts +68 -46
  25. package/src/features/ElectronTitlebar/Connection/index.tsx +0 -1
  26. package/src/hooks/useIsSingleMode.test.ts +66 -0
  27. package/src/hooks/useIsSingleMode.ts +29 -0
  28. package/src/server/featureFlags/index.ts +56 -0
  29. package/src/server/modules/EdgeConfig/index.ts +43 -4
  30. package/src/server/routers/lambda/aiModel.test.ts +2 -0
  31. package/src/store/global/actions/general.ts +46 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 1.134.0](https://github.com/lobehub/lobe-chat/compare/v1.133.6...v1.134.0)
6
+
7
+ <sup>Released on **2025-10-04**</sup>
8
+
9
+ #### โœจ Features
10
+
11
+ - **misc**: Support double-click to open multi agent window on the desktop.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Support double-click to open multi agent window on the desktop, closes [#9331](https://github.com/lobehub/lobe-chat/issues/9331) ([a060901](https://github.com/lobehub/lobe-chat/commit/a060901))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 1.133.6](https://github.com/lobehub/lobe-chat/compare/v1.133.5...v1.133.6)
31
+
32
+ <sup>Released on **2025-10-04**</sup>
33
+
34
+ #### ๐Ÿ› Bug Fixes
35
+
36
+ - **misc**: `type` not preserved when model is disabled or sorted.
37
+
38
+ #### ๐Ÿ’„ Styles
39
+
40
+ - **misc**: Nano banana support `aspect_ratio`.
41
+
42
+ <br/>
43
+
44
+ <details>
45
+ <summary><kbd>Improvements and Fixes</kbd></summary>
46
+
47
+ #### What's fixed
48
+
49
+ - **misc**: `type` not preserved when model is disabled or sorted, closes [#9530](https://github.com/lobehub/lobe-chat/issues/9530) ([476b897](https://github.com/lobehub/lobe-chat/commit/476b897))
50
+
51
+ #### Styles
52
+
53
+ - **misc**: Nano banana support `aspect_ratio`, closes [#9528](https://github.com/lobehub/lobe-chat/issues/9528) ([ae3ed6e](https://github.com/lobehub/lobe-chat/commit/ae3ed6e))
54
+
55
+ </details>
56
+
57
+ <div align="right">
58
+
59
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
60
+
61
+ </div>
62
+
5
63
  ### [Version 1.133.5](https://github.com/lobehub/lobe-chat/compare/v1.133.4...v1.133.5)
6
64
 
7
65
  <sup>Released on **2025-10-04**</sup>
@@ -46,4 +46,55 @@ export const appBrowsers = {
46
46
  },
47
47
  } satisfies Record<string, BrowserWindowOpts>;
48
48
 
49
+ // Window templates for multi-instance windows
50
+ export interface WindowTemplate {
51
+ allowMultipleInstances: boolean;
52
+ // Include common BrowserWindow options
53
+ autoHideMenuBar?: boolean;
54
+ baseIdentifier: string;
55
+ basePath: string;
56
+ devTools?: boolean;
57
+ height?: number;
58
+ keepAlive?: boolean;
59
+ minWidth?: number;
60
+ parentIdentifier?: string;
61
+ showOnInit?: boolean;
62
+ title?: string;
63
+ titleBarStyle?: 'hidden' | 'default' | 'hiddenInset' | 'customButtonsOnHover';
64
+ vibrancy?:
65
+ | 'appearance-based'
66
+ | 'content'
67
+ | 'fullscreen-ui'
68
+ | 'header'
69
+ | 'hud'
70
+ | 'menu'
71
+ | 'popover'
72
+ | 'selection'
73
+ | 'sheet'
74
+ | 'sidebar'
75
+ | 'titlebar'
76
+ | 'tooltip'
77
+ | 'under-page'
78
+ | 'under-window'
79
+ | 'window';
80
+ width?: number;
81
+ }
82
+
83
+ export const windowTemplates = {
84
+ chatSingle: {
85
+ allowMultipleInstances: true,
86
+ autoHideMenuBar: true,
87
+ baseIdentifier: 'chatSingle',
88
+ basePath: '/chat',
89
+ height: 600,
90
+ keepAlive: false, // Multi-instance windows don't need to stay alive
91
+ minWidth: 400,
92
+ parentIdentifier: 'chat',
93
+ titleBarStyle: 'hidden',
94
+ vibrancy: 'under-window',
95
+ width: 900,
96
+ },
97
+ } satisfies Record<string, WindowTemplate>;
98
+
49
99
  export type AppBrowsersIdentifiers = keyof typeof appBrowsers;
100
+ export type WindowTemplateIdentifiers = keyof typeof windowTemplates;
@@ -1,7 +1,7 @@
1
1
  import { InterceptRouteParams } from '@lobechat/electron-client-ipc';
2
2
  import { extractSubPath, findMatchingRoute } from '~common/routes';
3
3
 
4
- import { AppBrowsersIdentifiers, BrowsersIdentifiers } from '@/appBrowsers';
4
+ import { AppBrowsersIdentifiers, BrowsersIdentifiers, WindowTemplateIdentifiers } from '@/appBrowsers';
5
5
  import { IpcClientEventSender } from '@/types/ipcClientEvent';
6
6
 
7
7
  import { ControllerModule, ipcClientEvent, shortcut } from './index';
@@ -100,6 +100,77 @@ export default class BrowserWindowsCtr extends ControllerModule {
100
100
  }
101
101
  }
102
102
 
103
+ /**
104
+ * Create a new multi-instance window
105
+ */
106
+ @ipcClientEvent('createMultiInstanceWindow')
107
+ async createMultiInstanceWindow(params: {
108
+ templateId: WindowTemplateIdentifiers;
109
+ path: string;
110
+ uniqueId?: string;
111
+ }) {
112
+ try {
113
+ console.log('[BrowserWindowsCtr] Creating multi-instance window:', params);
114
+
115
+ const result = this.app.browserManager.createMultiInstanceWindow(
116
+ params.templateId,
117
+ params.path,
118
+ params.uniqueId,
119
+ );
120
+
121
+ // Show the window
122
+ result.browser.show();
123
+
124
+ return {
125
+ success: true,
126
+ windowId: result.identifier,
127
+ };
128
+ } catch (error) {
129
+ console.error('[BrowserWindowsCtr] Failed to create multi-instance window:', error);
130
+ return {
131
+ error: error.message,
132
+ success: false,
133
+ };
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get all windows by template
139
+ */
140
+ @ipcClientEvent('getWindowsByTemplate')
141
+ async getWindowsByTemplate(templateId: string) {
142
+ try {
143
+ const windowIds = this.app.browserManager.getWindowsByTemplate(templateId);
144
+ return {
145
+ success: true,
146
+ windowIds,
147
+ };
148
+ } catch (error) {
149
+ console.error('[BrowserWindowsCtr] Failed to get windows by template:', error);
150
+ return {
151
+ error: error.message,
152
+ success: false,
153
+ };
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Close all windows by template
159
+ */
160
+ @ipcClientEvent('closeWindowsByTemplate')
161
+ async closeWindowsByTemplate(templateId: string) {
162
+ try {
163
+ this.app.browserManager.closeWindowsByTemplate(templateId);
164
+ return { success: true };
165
+ } catch (error) {
166
+ console.error('[BrowserWindowsCtr] Failed to close windows by template:', error);
167
+ return {
168
+ error: error.message,
169
+ success: false,
170
+ };
171
+ }
172
+ }
173
+
103
174
  /**
104
175
  * Open target window and navigate to specified sub-path
105
176
  */
@@ -3,7 +3,7 @@ import { WebContents } from 'electron';
3
3
 
4
4
  import { createLogger } from '@/utils/logger';
5
5
 
6
- import { AppBrowsersIdentifiers, appBrowsers } from '../../appBrowsers';
6
+ import { AppBrowsersIdentifiers, appBrowsers, WindowTemplate, WindowTemplateIdentifiers, windowTemplates } from '../../appBrowsers';
7
7
  import type { App } from '../App';
8
8
  import type { BrowserWindowOpts } from './Browser';
9
9
  import Browser from './Browser';
@@ -14,9 +14,9 @@ const logger = createLogger('core:BrowserManager');
14
14
  export class BrowserManager {
15
15
  app: App;
16
16
 
17
- browsers: Map<AppBrowsersIdentifiers, Browser> = new Map();
17
+ browsers: Map<string, Browser> = new Map();
18
18
 
19
- private webContentsMap = new Map<WebContents, AppBrowsersIdentifiers>();
19
+ private webContentsMap = new Map<WebContents, string>();
20
20
 
21
21
  constructor(app: App) {
22
22
  logger.debug('Initializing BrowserManager');
@@ -51,12 +51,12 @@ export class BrowserManager {
51
51
  };
52
52
 
53
53
  broadcastToWindow = <T extends MainBroadcastEventKey>(
54
- identifier: AppBrowsersIdentifiers,
54
+ identifier: string,
55
55
  event: T,
56
56
  data: MainBroadcastParams<T>,
57
57
  ) => {
58
58
  logger.debug(`Broadcasting event ${event} to window: ${identifier}`);
59
- this.browsers.get(identifier).broadcast(event, data);
59
+ this.browsers.get(identifier)?.broadcast(event, data);
60
60
  };
61
61
 
62
62
  /**
@@ -87,13 +87,21 @@ export class BrowserManager {
87
87
  * @param identifier Window identifier
88
88
  * @param subPath Sub-path, such as 'agent', 'about', etc.
89
89
  */
90
- async redirectToPage(identifier: AppBrowsersIdentifiers, subPath?: string) {
90
+ async redirectToPage(identifier: string, subPath?: string) {
91
91
  try {
92
92
  // Ensure window is retrieved or created
93
93
  const browser = this.retrieveByIdentifier(identifier);
94
94
  browser.hide();
95
95
 
96
- const baseRoute = appBrowsers[identifier].path;
96
+ // Handle both static and dynamic windows
97
+ let baseRoute: string;
98
+ if (identifier in appBrowsers) {
99
+ baseRoute = appBrowsers[identifier as AppBrowsersIdentifiers].path;
100
+ } else {
101
+ // For dynamic windows, extract base route from the browser options
102
+ const browserOptions = browser.options;
103
+ baseRoute = browserOptions.path;
104
+ }
97
105
 
98
106
  // Build complete URL path
99
107
  const fullPath = subPath ? `${baseRoute}/${subPath}` : baseRoute;
@@ -114,13 +122,75 @@ export class BrowserManager {
114
122
  /**
115
123
  * get Browser by identifier
116
124
  */
117
- retrieveByIdentifier(identifier: AppBrowsersIdentifiers) {
125
+ retrieveByIdentifier(identifier: string) {
118
126
  const browser = this.browsers.get(identifier);
119
127
 
120
128
  if (browser) return browser;
121
129
 
122
- logger.debug(`Browser ${identifier} not found, initializing new instance`);
123
- return this.retrieveOrInitialize(appBrowsers[identifier]);
130
+ // Check if it's a static browser
131
+ if (identifier in appBrowsers) {
132
+ logger.debug(`Browser ${identifier} not found, initializing new instance`);
133
+ return this.retrieveOrInitialize(appBrowsers[identifier as AppBrowsersIdentifiers]);
134
+ }
135
+
136
+ throw new Error(`Browser ${identifier} not found and is not a static browser`);
137
+ }
138
+
139
+ /**
140
+ * Create a multi-instance window from template
141
+ * @param templateId Template identifier
142
+ * @param path Full path with query parameters
143
+ * @param uniqueId Optional unique identifier, will be generated if not provided
144
+ * @returns The window identifier and Browser instance
145
+ */
146
+ createMultiInstanceWindow(templateId: WindowTemplateIdentifiers, path: string, uniqueId?: string) {
147
+ const template = windowTemplates[templateId];
148
+ if (!template) {
149
+ throw new Error(`Window template ${templateId} not found`);
150
+ }
151
+
152
+ // Generate unique identifier
153
+ const windowId = uniqueId || `${template.baseIdentifier}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
154
+
155
+ // Create browser options from template
156
+ const browserOpts: BrowserWindowOpts = {
157
+ ...template,
158
+ identifier: windowId,
159
+ path: path,
160
+ };
161
+
162
+ logger.debug(`Creating multi-instance window: ${windowId} with path: ${path}`);
163
+
164
+ const browser = this.retrieveOrInitialize(browserOpts);
165
+
166
+ return {
167
+ identifier: windowId,
168
+ browser: browser,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Get all windows based on template
174
+ * @param templateId Template identifier
175
+ * @returns Array of window identifiers matching the template
176
+ */
177
+ getWindowsByTemplate(templateId: string): string[] {
178
+ const prefix = `${templateId}_`;
179
+ return Array.from(this.browsers.keys()).filter(id => id.startsWith(prefix));
180
+ }
181
+
182
+ /**
183
+ * Close all windows based on template
184
+ * @param templateId Template identifier
185
+ */
186
+ closeWindowsByTemplate(templateId: string): void {
187
+ const windowIds = this.getWindowsByTemplate(templateId);
188
+ windowIds.forEach(id => {
189
+ const browser = this.browsers.get(id);
190
+ if (browser) {
191
+ browser.close();
192
+ }
193
+ });
124
194
  }
125
195
 
126
196
  /**
@@ -144,7 +214,7 @@ export class BrowserManager {
144
214
  * @param options Browser window options
145
215
  */
146
216
  private retrieveOrInitialize(options: BrowserWindowOpts) {
147
- let browser = this.browsers.get(options.identifier as AppBrowsersIdentifiers);
217
+ let browser = this.browsers.get(options.identifier);
148
218
  if (browser) {
149
219
  logger.debug(`Retrieved existing browser: ${options.identifier}`);
150
220
  return browser;
@@ -153,7 +223,7 @@ export class BrowserManager {
153
223
  logger.debug(`Creating new browser: ${options.identifier}`);
154
224
  browser = new Browser(options, this.app);
155
225
 
156
- const identifier = options.identifier as AppBrowsersIdentifiers;
226
+ const identifier = options.identifier;
157
227
  this.browsers.set(identifier, browser);
158
228
 
159
229
  // ่ฎฐๅฝ• WebContents ๅ’Œ identifier ็š„ๆ˜ ๅฐ„
@@ -166,32 +236,32 @@ export class BrowserManager {
166
236
 
167
237
  browser.browserWindow.on('show', () => {
168
238
  if (browser.webContents)
169
- this.webContentsMap.set(browser.webContents, browser.identifier as AppBrowsersIdentifiers);
239
+ this.webContentsMap.set(browser.webContents, browser.identifier);
170
240
  });
171
241
 
172
242
  return browser;
173
243
  }
174
244
 
175
245
  closeWindow(identifier: string) {
176
- const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
246
+ const browser = this.browsers.get(identifier);
177
247
  browser?.close();
178
248
  }
179
249
 
180
250
  minimizeWindow(identifier: string) {
181
- const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
251
+ const browser = this.browsers.get(identifier);
182
252
  browser?.browserWindow.minimize();
183
253
  }
184
254
 
185
255
  maximizeWindow(identifier: string) {
186
- const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
187
- if (browser.browserWindow.isMaximized()) {
256
+ const browser = this.browsers.get(identifier);
257
+ if (browser?.browserWindow.isMaximized()) {
188
258
  browser?.browserWindow.unmaximize();
189
259
  } else {
190
260
  browser?.browserWindow.maximize();
191
261
  }
192
262
  }
193
263
 
194
- getIdentifierByWebContents(webContents: WebContents): AppBrowsersIdentifiers | null {
264
+ getIdentifierByWebContents(webContents: WebContents): string | null {
195
265
  return this.webContentsMap.get(webContents) || null;
196
266
  }
197
267
 
package/changelog/v1.json CHANGED
@@ -1,4 +1,25 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Support double-click to open multi agent window on the desktop."
6
+ ]
7
+ },
8
+ "date": "2025-10-04",
9
+ "version": "1.134.0"
10
+ },
11
+ {
12
+ "children": {
13
+ "fixes": [
14
+ "type not preserved when model is disabled or sorted."
15
+ ],
16
+ "improvements": [
17
+ "Nano banana support aspect_ratio."
18
+ ]
19
+ },
20
+ "date": "2025-10-04",
21
+ "version": "1.133.6"
22
+ },
2
23
  {
3
24
  "children": {
4
25
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.133.5",
3
+ "version": "1.134.0",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -211,16 +211,19 @@ describe('AiModelModel', () => {
211
211
  id: 'model1',
212
212
  providerId: 'openai',
213
213
  enabled: true,
214
+ type: 'image',
214
215
  });
215
216
 
216
217
  await aiProviderModel.toggleModelEnabled({
217
218
  id: model.id,
218
219
  providerId: 'openai',
219
220
  enabled: false,
221
+ type: 'image',
220
222
  });
221
223
 
222
224
  const updatedModel = await aiProviderModel.findById(model.id);
223
225
  expect(updatedModel?.enabled).toBe(false);
226
+ expect(updatedModel?.type).toBe('image');
224
227
  });
225
228
  });
226
229
 
@@ -122,11 +122,27 @@ export class AiModelModel {
122
122
  };
123
123
 
124
124
  toggleModelEnabled = async (value: ToggleAiModelEnableParams) => {
125
+ const now = new Date();
126
+ const insertValues = {
127
+ ...value,
128
+ updatedAt: now,
129
+ userId: this.userId,
130
+ } as typeof aiModels.$inferInsert;
131
+
132
+ if (value.type) insertValues.type = value.type;
133
+
134
+ const updateValues: Partial<typeof aiModels.$inferInsert> = {
135
+ enabled: value.enabled,
136
+ updatedAt: now,
137
+ };
138
+
139
+ if (value.type) updateValues.type = value.type;
140
+
125
141
  return this.db
126
142
  .insert(aiModels)
127
- .values({ ...value, updatedAt: new Date(), userId: this.userId })
143
+ .values(insertValues)
128
144
  .onConflictDoUpdate({
129
- set: { enabled: value.enabled, updatedAt: new Date() },
145
+ set: updateValues,
130
146
  target: [aiModels.id, aiModels.providerId, aiModels.userId],
131
147
  });
132
148
  };
@@ -1,5 +1,23 @@
1
1
  import { InterceptRouteParams, InterceptRouteResponse } from '../types/route';
2
2
 
3
+ export interface CreateMultiInstanceWindowParams {
4
+ templateId: string;
5
+ path: string;
6
+ uniqueId?: string;
7
+ }
8
+
9
+ export interface CreateMultiInstanceWindowResponse {
10
+ success: boolean;
11
+ windowId?: string;
12
+ error?: string;
13
+ }
14
+
15
+ export interface GetWindowsByTemplateResponse {
16
+ success: boolean;
17
+ windowIds?: string[];
18
+ error?: string;
19
+ }
20
+
3
21
  export interface WindowsDispatchEvents {
4
22
  /**
5
23
  * ๆ‹ฆๆˆชๅฎขๆˆท็ซฏ่ทฏ็”ฑๅฏผ่ˆช่ฏทๆฑ‚
@@ -14,4 +32,25 @@ export interface WindowsDispatchEvents {
14
32
  openDevtools: () => void;
15
33
 
16
34
  openSettingsWindow: (tab?: string) => void;
35
+
36
+ /**
37
+ * Create a new multi-instance window
38
+ * @param params Window creation parameters
39
+ * @returns Creation result
40
+ */
41
+ createMultiInstanceWindow: (params: CreateMultiInstanceWindowParams) => CreateMultiInstanceWindowResponse;
42
+
43
+ /**
44
+ * Get all windows by template
45
+ * @param templateId Template identifier
46
+ * @returns List of window identifiers
47
+ */
48
+ getWindowsByTemplate: (templateId: string) => GetWindowsByTemplateResponse;
49
+
50
+ /**
51
+ * Close all windows by template
52
+ * @param templateId Template identifier
53
+ * @returns Operation result
54
+ */
55
+ closeWindowsByTemplate: (templateId: string) => { success: boolean; error?: string };
17
56
  }
@@ -366,6 +366,28 @@ const googleChatModels: AIChatModelCard[] = [
366
366
  'Nano Banana ๆ˜ฏ Google ๆœ€ๆ–ฐใ€ๆœ€ๅฟซใ€ๆœ€้ซ˜ๆ•ˆ็š„ๅŽŸ็”Ÿๅคšๆจกๆ€ๆจกๅž‹๏ผŒๅฎƒๅ…่ฎธๆ‚จ้€š่ฟ‡ๅฏน่ฏ็”Ÿๆˆๅ’Œ็ผ–่พ‘ๅ›พๅƒใ€‚',
367
367
  displayName: 'Nano Banana',
368
368
  enabled: true,
369
+ id: 'gemini-2.5-flash-image',
370
+ maxOutput: 8192,
371
+ pricing: {
372
+ units: [
373
+ { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
374
+ { name: 'imageInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
375
+ { name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
376
+ { name: 'imageOutput', rate: 30, strategy: 'fixed', unit: 'millionTokens' },
377
+ ],
378
+ },
379
+ releasedAt: '2025-08-26',
380
+ type: 'chat',
381
+ },
382
+ {
383
+ abilities: {
384
+ imageOutput: true,
385
+ vision: true,
386
+ },
387
+ contextWindowTokens: 32_768 + 8192,
388
+ description:
389
+ 'Nano Banana ๆ˜ฏ Google ๆœ€ๆ–ฐใ€ๆœ€ๅฟซใ€ๆœ€้ซ˜ๆ•ˆ็š„ๅŽŸ็”Ÿๅคšๆจกๆ€ๆจกๅž‹๏ผŒๅฎƒๅ…่ฎธๆ‚จ้€š่ฟ‡ๅฏน่ฏ็”Ÿๆˆๅ’Œ็ผ–่พ‘ๅ›พๅƒใ€‚',
390
+ displayName: 'Nano Banana (Preview)',
369
391
  id: 'gemini-2.5-flash-image-preview',
370
392
  maxOutput: 8192,
371
393
  pricing: {
@@ -805,22 +827,62 @@ const imagenBaseParameters: ModelParamsSchema = {
805
827
  prompt: { default: '' },
806
828
  };
807
829
 
830
+ const NANO_BANANA_ASPECT_RATIOS = [
831
+ '1:1', // 1024x1024
832
+ '2:3', // 832x1248
833
+ '3:2', // 1248x832
834
+ '3:4', // 864x1184
835
+ '4:3', // 1184x864
836
+ '4:5', // 896x1152
837
+ '5:4', // 1152x896
838
+ '9:16', // 768x1344
839
+ '16:9', // 1344x768
840
+ '21:9', // 1536x672
841
+ ];
842
+
843
+ const nanoBananaParameters: ModelParamsSchema = {
844
+ aspectRatio: {
845
+ default: '1:1',
846
+ enum: NANO_BANANA_ASPECT_RATIOS,
847
+ },
848
+ imageUrls: {
849
+ default: [],
850
+ },
851
+ prompt: { default: '' },
852
+ };
853
+
808
854
  /* eslint-disable sort-keys-fix/sort-keys-fix */
809
855
  const googleImageModels: AIImageModelCard[] = [
810
856
  {
811
857
  displayName: 'Nano Banana',
812
- id: 'gemini-2.5-flash-image-preview:image',
858
+ id: 'gemini-2.5-flash-image:image',
813
859
  enabled: true,
814
860
  type: 'image',
815
861
  description:
816
862
  'Nano Banana ๆ˜ฏ Google ๆœ€ๆ–ฐใ€ๆœ€ๅฟซใ€ๆœ€้ซ˜ๆ•ˆ็š„ๅŽŸ็”Ÿๅคšๆจกๆ€ๆจกๅž‹๏ผŒๅฎƒๅ…่ฎธๆ‚จ้€š่ฟ‡ๅฏน่ฏ็”Ÿๆˆๅ’Œ็ผ–่พ‘ๅ›พๅƒใ€‚',
817
863
  releasedAt: '2025-08-26',
864
+ parameters: nanoBananaParameters,
865
+ pricing: {
866
+ units: [
867
+ { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
868
+ { name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
869
+ { name: 'imageOutput', rate: 30, strategy: 'fixed', unit: 'millionTokens' },
870
+ ],
871
+ },
872
+ },
873
+ {
874
+ displayName: 'Nano Banana (Preview)',
875
+ id: 'gemini-2.5-flash-image-preview:image',
876
+ type: 'image',
877
+ description:
878
+ 'Nano Banana ๆ˜ฏ Google ๆœ€ๆ–ฐใ€ๆœ€ๅฟซใ€ๆœ€้ซ˜ๆ•ˆ็š„ๅŽŸ็”Ÿๅคšๆจกๆ€ๆจกๅž‹๏ผŒๅฎƒๅ…่ฎธๆ‚จ้€š่ฟ‡ๅฏน่ฏ็”Ÿๆˆๅ’Œ็ผ–่พ‘ๅ›พๅƒใ€‚',
879
+ releasedAt: '2025-08-26',
818
880
  parameters: CHAT_MODEL_IMAGE_GENERATION_PARAMS,
819
881
  pricing: {
820
882
  units: [
821
883
  { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
822
884
  { name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
823
- { name: 'imageOutput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
885
+ { name: 'imageOutput', rate: 30, strategy: 'fixed', unit: 'millionTokens' },
824
886
  ],
825
887
  },
826
888
  },
@@ -12,15 +12,18 @@ export const AiModelSourceEnum = {
12
12
 
13
13
  export type AiModelSourceType = (typeof AiModelSourceEnum)[keyof typeof AiModelSourceEnum];
14
14
 
15
- export type AiModelType =
16
- | 'chat'
17
- | 'embedding'
18
- | 'tts'
19
- | 'stt'
20
- | 'image'
21
- | 'text2video'
22
- | 'text2music'
23
- | 'realtime';
15
+ export const AiModelTypeSchema = z.enum([
16
+ 'chat',
17
+ 'embedding',
18
+ 'tts',
19
+ 'stt',
20
+ 'image',
21
+ 'text2video',
22
+ 'text2music',
23
+ 'realtime',
24
+ ] as const);
25
+
26
+ export type AiModelType = z.infer<typeof AiModelTypeSchema>;
24
27
 
25
28
  export interface ModelAbilities {
26
29
  /**
@@ -373,6 +376,7 @@ export const ToggleAiModelEnableSchema = z.object({
373
376
  id: z.string(),
374
377
  providerId: z.string(),
375
378
  source: z.enum(['builtin', 'custom', 'remote']).optional(),
379
+ type: AiModelTypeSchema.optional(),
376
380
  });
377
381
 
378
382
  export type ToggleAiModelEnableParams = z.infer<typeof ToggleAiModelEnableSchema>;