mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(gemini): support mid-session model change (#379)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
|
||||
import { logger } from '@/ui/logger';
|
||||
import { killProcessByChildProcess } from '@/utils/process';
|
||||
import { GEMINI_MODEL_PRESETS } from '@hapi/protocol';
|
||||
|
||||
interface JsonRpcRequest {
|
||||
jsonrpc: '2.0';
|
||||
@@ -303,7 +304,7 @@ export class AcpStdioTransport {
|
||||
if (lowerText.includes('status 404') || lowerText.includes('model not found') || lowerText.includes('not_found')) {
|
||||
this.stderrErrorHandler({
|
||||
type: 'model_not_found',
|
||||
message: 'Model not found. Available models: gemini-3.1-pro-preview, gemini-3-flash-preview, gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite',
|
||||
message: `Model not found. Available models: ${GEMINI_MODEL_PRESETS.join(', ')}`,
|
||||
raw: text
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -46,17 +46,22 @@ export async function geminiLoop(opts: GeminiLoopOptions): Promise<void> {
|
||||
session.onSessionFound(opts.resumeSessionId);
|
||||
}
|
||||
|
||||
const getCurrentModel = (): string | undefined => {
|
||||
const sessionModel = session.getModel();
|
||||
return sessionModel != null ? sessionModel : opts.model;
|
||||
};
|
||||
|
||||
await runLocalRemoteSession({
|
||||
session,
|
||||
startingMode: opts.startingMode,
|
||||
logTag: 'gemini-loop',
|
||||
runLocal: (instance) => geminiLocalLauncher(instance, {
|
||||
model: opts.model,
|
||||
model: getCurrentModel(),
|
||||
allowedTools: opts.allowedTools,
|
||||
hookSettingsPath: opts.hookSettingsPath
|
||||
}),
|
||||
runRemote: (instance) => geminiRemoteLauncher(instance, {
|
||||
model: opts.model,
|
||||
model: getCurrentModel(),
|
||||
hookSettingsPath: opts.hookSettingsPath
|
||||
}),
|
||||
onSessionReady: opts.onSessionReady
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mockGeminiSession = vi.hoisted(() => ({
|
||||
setModel: vi.fn(),
|
||||
setPermissionMode: vi.fn(),
|
||||
stopKeepAlive: vi.fn()
|
||||
}));
|
||||
|
||||
const harness = vi.hoisted(() => ({
|
||||
bootstrapArgs: [] as Array<Record<string, unknown>>,
|
||||
geminiLoopArgs: [] as Array<Record<string, unknown>>,
|
||||
@@ -24,6 +30,10 @@ vi.mock('@/agent/sessionFactory', () => ({
|
||||
vi.mock('./loop', () => ({
|
||||
geminiLoop: vi.fn(async (options: Record<string, unknown>) => {
|
||||
harness.geminiLoopArgs.push(options);
|
||||
const onSessionReady = options.onSessionReady as ((session: unknown) => void) | undefined;
|
||||
if (onSessionReady) {
|
||||
onSessionReady(mockGeminiSession);
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
@@ -78,6 +88,8 @@ describe('runGemini', () => {
|
||||
beforeEach(() => {
|
||||
harness.bootstrapArgs.length = 0;
|
||||
harness.geminiLoopArgs.length = 0;
|
||||
mockGeminiSession.setModel.mockReset();
|
||||
mockGeminiSession.setPermissionMode.mockReset();
|
||||
harness.session.onUserMessage.mockReset();
|
||||
harness.session.rpcHandlerManager.registerHandler.mockReset();
|
||||
resolveGeminiRuntimeConfigMock.mockReset();
|
||||
@@ -97,13 +109,125 @@ describe('runGemini', () => {
|
||||
|
||||
it('does not persist the hardcoded default fallback model', async () => {
|
||||
resolveGeminiRuntimeConfigMock.mockReturnValue({
|
||||
model: 'gemini-2.5-pro',
|
||||
model: 'gemini-3-flash-preview',
|
||||
modelSource: 'default'
|
||||
});
|
||||
|
||||
await runGemini({});
|
||||
|
||||
expect(harness.bootstrapArgs[0]?.model).toBeUndefined();
|
||||
expect(harness.geminiLoopArgs[0]?.model).toBe('gemini-3-flash-preview');
|
||||
});
|
||||
|
||||
it('applies model change via set-session-config RPC', async () => {
|
||||
resolveGeminiRuntimeConfigMock.mockReturnValue({
|
||||
model: 'gemini-3-flash-preview',
|
||||
modelSource: 'default'
|
||||
});
|
||||
|
||||
await runGemini({});
|
||||
|
||||
const registerCalls = harness.session.rpcHandlerManager.registerHandler.mock.calls;
|
||||
const configHandler = registerCalls.find(
|
||||
(call: unknown[]) => call[0] === 'set-session-config'
|
||||
);
|
||||
expect(configHandler).toBeDefined();
|
||||
|
||||
const handler = configHandler![1] as (payload: unknown) => Promise<unknown>;
|
||||
const result = await handler({ model: 'gemini-2.5-flash' }) as Record<string, unknown>;
|
||||
const applied = result.applied as Record<string, unknown>;
|
||||
expect(applied.model).toBe('gemini-2.5-flash');
|
||||
});
|
||||
|
||||
it('rejects invalid model in set-session-config RPC', async () => {
|
||||
resolveGeminiRuntimeConfigMock.mockReturnValue({
|
||||
model: 'gemini-3-flash-preview',
|
||||
modelSource: 'default'
|
||||
});
|
||||
|
||||
await runGemini({});
|
||||
|
||||
const registerCalls = harness.session.rpcHandlerManager.registerHandler.mock.calls;
|
||||
const configHandler = registerCalls.find(
|
||||
(call: unknown[]) => call[0] === 'set-session-config'
|
||||
);
|
||||
const handler = configHandler![1] as (payload: unknown) => Promise<unknown>;
|
||||
await expect(handler({ model: 123 })).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('accepts null model (Auto) in set-session-config RPC', async () => {
|
||||
resolveGeminiRuntimeConfigMock.mockReturnValue({
|
||||
model: 'gemini-3-flash-preview',
|
||||
modelSource: 'default'
|
||||
});
|
||||
|
||||
await runGemini({});
|
||||
|
||||
const registerCalls = harness.session.rpcHandlerManager.registerHandler.mock.calls;
|
||||
const configHandler = registerCalls.find(
|
||||
(call: unknown[]) => call[0] === 'set-session-config'
|
||||
);
|
||||
const handler = configHandler![1] as (payload: unknown) => Promise<unknown>;
|
||||
const result = await handler({ model: null }) as Record<string, unknown>;
|
||||
const applied = result.applied as Record<string, unknown>;
|
||||
// null (Default) should be passed through to hub for DB clearing
|
||||
expect(applied.model).toBeNull();
|
||||
});
|
||||
|
||||
it('only includes changed fields in applied response', async () => {
|
||||
resolveGeminiRuntimeConfigMock.mockReturnValue({
|
||||
model: 'gemini-3-flash-preview',
|
||||
modelSource: 'default'
|
||||
});
|
||||
|
||||
await runGemini({});
|
||||
|
||||
const registerCalls = harness.session.rpcHandlerManager.registerHandler.mock.calls;
|
||||
const configHandler = registerCalls.find(
|
||||
(call: unknown[]) => call[0] === 'set-session-config'
|
||||
);
|
||||
const handler = configHandler![1] as (payload: unknown) => Promise<unknown>;
|
||||
const result = await handler({ permissionMode: 'default' }) as Record<string, unknown>;
|
||||
const applied = result.applied as Record<string, unknown>;
|
||||
expect(applied.permissionMode).toBe('default');
|
||||
expect(applied).not.toHaveProperty('model');
|
||||
});
|
||||
|
||||
it('stores null model in session on Default selection for keepalive', async () => {
|
||||
resolveGeminiRuntimeConfigMock.mockReturnValue({
|
||||
model: 'gemini-2.5-pro',
|
||||
modelSource: 'default'
|
||||
});
|
||||
|
||||
await runGemini({});
|
||||
|
||||
const registerCalls = harness.session.rpcHandlerManager.registerHandler.mock.calls;
|
||||
const configHandler = registerCalls.find(
|
||||
(call: unknown[]) => call[0] === 'set-session-config'
|
||||
);
|
||||
const handler = configHandler![1] as (payload: unknown) => Promise<unknown>;
|
||||
|
||||
// First set an explicit model
|
||||
await handler({ model: 'gemini-2.5-flash' });
|
||||
expect(mockGeminiSession.setModel).toHaveBeenLastCalledWith('gemini-2.5-flash');
|
||||
|
||||
// Then select Default (null) — session should store null, not concrete model
|
||||
await handler({ model: null });
|
||||
expect(mockGeminiSession.setModel).toHaveBeenLastCalledWith(null);
|
||||
});
|
||||
|
||||
it('passes machine default (not startup model) to geminiLoop for fallback', async () => {
|
||||
// Session started with explicit model, but machine default differs
|
||||
resolveGeminiRuntimeConfigMock.mockImplementation((opts?: { model?: string }) => {
|
||||
if (opts?.model) {
|
||||
return { model: opts.model, modelSource: 'explicit' };
|
||||
}
|
||||
return { model: 'gemini-2.5-pro', modelSource: 'default' };
|
||||
});
|
||||
|
||||
await runGemini({ model: 'gemini-2.5-flash' });
|
||||
|
||||
// geminiLoop should receive machine default as fallback, not the explicit startup model
|
||||
expect(harness.geminiLoopArgs[0]?.model).toBe('gemini-2.5-pro');
|
||||
});
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export async function runGemini(opts: {
|
||||
controlledByUser: false
|
||||
};
|
||||
|
||||
const machineDefault = resolveGeminiRuntimeConfig().model;
|
||||
const runtimeConfig = resolveGeminiRuntimeConfig({ model: opts.model });
|
||||
const persistedModel = runtimeConfig.modelSource === 'default'
|
||||
? undefined
|
||||
@@ -62,7 +63,8 @@ export async function runGemini(opts: {
|
||||
|
||||
const sessionWrapperRef: { current: GeminiSession | null } = { current: null };
|
||||
let currentPermissionMode: PermissionMode = opts.permissionMode ?? 'default';
|
||||
const resolvedModel = runtimeConfig.model;
|
||||
let sessionModel: string | null = persistedModel ?? null;
|
||||
let resolvedModel = sessionModel ?? machineDefault;
|
||||
|
||||
const hookServer = await startHookServer({
|
||||
onSessionHook: (sessionId, data) => {
|
||||
@@ -105,7 +107,8 @@ export async function runGemini(opts: {
|
||||
return;
|
||||
}
|
||||
sessionInstance.setPermissionMode(currentPermissionMode);
|
||||
logger.debug(`[gemini] Synced session permission mode for keepalive: ${currentPermissionMode}`);
|
||||
sessionInstance.setModel(sessionModel);
|
||||
logger.debug(`[gemini] Synced session config for keepalive: permissionMode=${currentPermissionMode}, model=${resolvedModel}`);
|
||||
};
|
||||
|
||||
session.onUserMessage((message) => {
|
||||
@@ -125,18 +128,36 @@ export async function runGemini(opts: {
|
||||
return parsed.data as PermissionMode;
|
||||
};
|
||||
|
||||
const resolveModel = (value: unknown): string | null => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||
throw new Error('Invalid model');
|
||||
}
|
||||
return value.trim();
|
||||
};
|
||||
|
||||
session.rpcHandlerManager.registerHandler('set-session-config', async (payload: unknown) => {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
throw new Error('Invalid session config payload');
|
||||
}
|
||||
const config = payload as { permissionMode?: unknown };
|
||||
const config = payload as { permissionMode?: unknown; model?: unknown };
|
||||
const applied: Record<string, unknown> = {};
|
||||
|
||||
if (config.permissionMode !== undefined) {
|
||||
currentPermissionMode = resolvePermissionMode(config.permissionMode);
|
||||
applied.permissionMode = currentPermissionMode;
|
||||
}
|
||||
|
||||
if (config.model !== undefined) {
|
||||
sessionModel = resolveModel(config.model);
|
||||
resolvedModel = sessionModel ?? machineDefault;
|
||||
applied.model = sessionModel;
|
||||
}
|
||||
|
||||
syncSessionMode();
|
||||
return { applied: { permissionMode: currentPermissionMode } };
|
||||
return { applied };
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -148,7 +169,7 @@ export async function runGemini(opts: {
|
||||
session,
|
||||
api,
|
||||
permissionMode: currentPermissionMode,
|
||||
model: resolvedModel,
|
||||
model: machineDefault,
|
||||
hookSettingsPath,
|
||||
resumeSessionId: opts.resumeSessionId,
|
||||
onModeChange: createModeChangeHandler(session),
|
||||
|
||||
@@ -78,6 +78,10 @@ export class GeminiSession extends AgentSessionBase<GeminiMode> {
|
||||
this.permissionMode = mode;
|
||||
};
|
||||
|
||||
setModel = (model: string | null): void => {
|
||||
this.model = model;
|
||||
};
|
||||
|
||||
recordLocalLaunchFailure = (message: string, exitReason: LocalLaunchExitReason): void => {
|
||||
this.localLaunchFailure = { message, exitReason };
|
||||
};
|
||||
|
||||
@@ -2,11 +2,12 @@ import { existsSync, readFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { logger } from '@/ui/logger';
|
||||
import { DEFAULT_GEMINI_MODEL } from '@hapi/protocol';
|
||||
|
||||
export const GEMINI_API_KEY_ENV = 'GEMINI_API_KEY';
|
||||
export const GOOGLE_API_KEY_ENV = 'GOOGLE_API_KEY';
|
||||
export const GEMINI_MODEL_ENV = 'GEMINI_MODEL';
|
||||
export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';
|
||||
export { DEFAULT_GEMINI_MODEL };
|
||||
|
||||
export type GeminiLocalConfig = {
|
||||
token?: string;
|
||||
@@ -91,7 +92,7 @@ export function resolveGeminiRuntimeConfig(opts: {
|
||||
const local = readGeminiLocalConfig();
|
||||
|
||||
let modelSource: GeminiModelSource = 'default';
|
||||
let model = DEFAULT_GEMINI_MODEL;
|
||||
let model: string = DEFAULT_GEMINI_MODEL;
|
||||
|
||||
if (opts.model) {
|
||||
model = opts.model;
|
||||
|
||||
@@ -316,8 +316,8 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho
|
||||
}
|
||||
|
||||
const flavor = sessionResult.session.metadata?.flavor ?? 'claude'
|
||||
if (flavor !== 'claude') {
|
||||
return c.json({ error: 'Model selection is only supported for Claude sessions' }, 400)
|
||||
if (flavor !== 'claude' && flavor !== 'gemini') {
|
||||
return c.json({ error: 'Model selection is only supported for Claude and Gemini sessions' }, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -40,6 +40,18 @@ export type ClaudeModelPreset = typeof CLAUDE_MODEL_PRESETS[number]
|
||||
|
||||
export type AgentFlavor = 'claude' | 'codex' | 'gemini' | 'opencode' | 'cursor'
|
||||
|
||||
export const GEMINI_MODEL_LABELS = {
|
||||
'gemini-3.1-pro-preview': 'Gemini 3.1 Pro Preview',
|
||||
'gemini-3-flash-preview': 'Gemini 3 Flash Preview',
|
||||
'gemini-2.5-pro': 'Gemini 2.5 Pro',
|
||||
'gemini-2.5-flash': 'Gemini 2.5 Flash',
|
||||
'gemini-2.5-flash-lite': 'Gemini 2.5 Flash Lite',
|
||||
} as const
|
||||
|
||||
export type GeminiModelPreset = keyof typeof GEMINI_MODEL_LABELS
|
||||
export const GEMINI_MODEL_PRESETS = Object.keys(GEMINI_MODEL_LABELS) as GeminiModelPreset[]
|
||||
export const DEFAULT_GEMINI_MODEL: GeminiModelPreset = 'gemini-2.5-pro'
|
||||
|
||||
export const PERMISSION_MODE_LABELS: Record<PermissionMode, string> = {
|
||||
default: 'Default',
|
||||
acceptEdits: 'Accept Edits',
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useActiveSuggestions } from '@/hooks/useActiveSuggestions'
|
||||
import { applySuggestion } from '@/utils/applySuggestion'
|
||||
import { usePlatform } from '@/hooks/usePlatform'
|
||||
import { usePWAInstall } from '@/hooks/usePWAInstall'
|
||||
import { isClaudeFlavor } from '@/lib/agentFlavorUtils'
|
||||
import { isClaudeFlavor, supportsModelChange } from '@/lib/agentFlavorUtils'
|
||||
import { markSkillUsed } from '@/lib/recent-skills'
|
||||
import { FloatingOverlay } from '@/components/ChatInput/FloatingOverlay'
|
||||
import { Autocomplete } from '@/components/ChatInput/Autocomplete'
|
||||
@@ -28,7 +28,7 @@ import { StatusBar } from '@/components/AssistantChat/StatusBar'
|
||||
import { ComposerButtons } from '@/components/AssistantChat/ComposerButtons'
|
||||
import { AttachmentItem } from '@/components/AssistantChat/AttachmentItem'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
import { getClaudeComposerModelOptions, getNextClaudeComposerModel } from './claudeModelOptions'
|
||||
import { getModelOptionsForFlavor, getNextModelForFlavor } from './modelOptions'
|
||||
import { getClaudeComposerEffortOptions } from './claudeEffortOptions'
|
||||
|
||||
export interface TextInputState {
|
||||
@@ -266,8 +266,8 @@ export function HappyComposer(props: {
|
||||
[agentFlavor]
|
||||
)
|
||||
const claudeModelOptions = useMemo(
|
||||
() => getClaudeComposerModelOptions(model),
|
||||
[model]
|
||||
() => getModelOptionsForFlavor(agentFlavor, model),
|
||||
[agentFlavor, model]
|
||||
)
|
||||
const claudeEffortOptions = useMemo(
|
||||
() => getClaudeComposerEffortOptions(effort),
|
||||
@@ -352,9 +352,9 @@ export function HappyComposer(props: {
|
||||
|
||||
useEffect(() => {
|
||||
const handleGlobalKeyDown = (e: globalThis.KeyboardEvent) => {
|
||||
if (e.key === 'm' && (e.metaKey || e.ctrlKey) && onModelChange && isClaudeFlavor(agentFlavor)) {
|
||||
if (e.key === 'm' && (e.metaKey || e.ctrlKey) && onModelChange && supportsModelChange(agentFlavor)) {
|
||||
e.preventDefault()
|
||||
onModelChange(getNextClaudeComposerModel(model))
|
||||
onModelChange(getNextModelForFlavor(agentFlavor, model))
|
||||
haptic('light')
|
||||
}
|
||||
}
|
||||
@@ -439,7 +439,7 @@ export function HappyComposer(props: {
|
||||
|
||||
const showCollaborationSettings = Boolean(onCollaborationModeChange && collaborationModeOptions.length > 0)
|
||||
const showPermissionSettings = Boolean(onPermissionModeChange && permissionModeOptions.length > 0)
|
||||
const showModelSettings = Boolean(onModelChange && isClaudeFlavor(agentFlavor))
|
||||
const showModelSettings = Boolean(onModelChange && supportsModelChange(agentFlavor))
|
||||
const showEffortSettings = Boolean(onEffortChange && isClaudeFlavor(agentFlavor))
|
||||
const showSettingsButton = Boolean(showCollaborationSettings || showPermissionSettings || showModelSettings || showEffortSettings)
|
||||
const showAbortButton = true
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getModelOptionsForFlavor, getNextModelForFlavor } from './modelOptions'
|
||||
|
||||
describe('getModelOptionsForFlavor', () => {
|
||||
it('returns Gemini model options for gemini flavor', () => {
|
||||
const options = getModelOptionsForFlavor('gemini')
|
||||
expect(options[0]).toEqual({ value: null, label: 'Default' })
|
||||
expect(options.some((o) => o.value === 'gemini-3-flash-preview')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'gemini-2.5-flash')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns Claude model options for claude flavor', () => {
|
||||
const options = getModelOptionsForFlavor('claude')
|
||||
expect(options[0]).toEqual({ value: null, label: 'Auto' })
|
||||
expect(options.some((o) => o.value === 'sonnet')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'opus')).toBe(true)
|
||||
})
|
||||
|
||||
it('includes custom Gemini model from env/config in options', () => {
|
||||
const options = getModelOptionsForFlavor('gemini', 'gemini-custom-experiment')
|
||||
expect(options.some((o) => o.value === 'gemini-custom-experiment')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not duplicate a preset Gemini model', () => {
|
||||
const options = getModelOptionsForFlavor('gemini', 'gemini-2.5-flash')
|
||||
const flashCount = options.filter((o) => o.value === 'gemini-2.5-flash').length
|
||||
expect(flashCount).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNextModelForFlavor', () => {
|
||||
it('cycles Gemini models', () => {
|
||||
const next = getNextModelForFlavor('gemini', null)
|
||||
expect(next).not.toBeNull()
|
||||
})
|
||||
|
||||
it('cycles Claude models', () => {
|
||||
const next = getNextModelForFlavor('claude', null)
|
||||
expect(next).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { MODEL_OPTIONS } from '@/components/NewSession/types'
|
||||
import { getClaudeComposerModelOptions, getNextClaudeComposerModel } from './claudeModelOptions'
|
||||
import type { ClaudeComposerModelOption } from './claudeModelOptions'
|
||||
|
||||
export type ModelOption = ClaudeComposerModelOption
|
||||
|
||||
function getGeminiModelOptions(currentModel?: string | null): ModelOption[] {
|
||||
const options = MODEL_OPTIONS.gemini.map((m) => ({
|
||||
value: m.value === 'auto' ? null : m.value,
|
||||
label: m.label
|
||||
}))
|
||||
const normalized = currentModel?.trim() || null
|
||||
if (normalized && !options.some((o) => o.value === normalized)) {
|
||||
options.splice(1, 0, { value: normalized, label: normalized })
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
function getNextGeminiModel(currentModel?: string | null): string | null {
|
||||
const options = getGeminiModelOptions(currentModel)
|
||||
const currentIndex = options.findIndex((o) => o.value === (currentModel ?? null))
|
||||
if (currentIndex === -1) {
|
||||
return options[0]?.value ?? null
|
||||
}
|
||||
return options[(currentIndex + 1) % options.length]?.value ?? null
|
||||
}
|
||||
|
||||
export function getModelOptionsForFlavor(flavor: string | undefined | null, currentModel?: string | null): ModelOption[] {
|
||||
if (flavor === 'gemini') {
|
||||
return getGeminiModelOptions(currentModel)
|
||||
}
|
||||
return getClaudeComposerModelOptions(currentModel)
|
||||
}
|
||||
|
||||
export function getNextModelForFlavor(flavor: string | undefined | null, currentModel?: string | null): string | null {
|
||||
if (flavor === 'gemini') {
|
||||
return getNextGeminiModel(currentModel)
|
||||
}
|
||||
return getNextClaudeComposerModel(currentModel)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { GEMINI_MODEL_PRESETS, GEMINI_MODEL_LABELS } from '@hapi/protocol'
|
||||
|
||||
export type AgentType = 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode'
|
||||
export type SessionType = 'simple' | 'worktree'
|
||||
export type CodexReasoningEffort = 'default' | 'low' | 'medium' | 'high' | 'xhigh'
|
||||
@@ -23,12 +25,8 @@ export const MODEL_OPTIONS: Record<AgentType, { value: string; label: string }[]
|
||||
],
|
||||
cursor: [],
|
||||
gemini: [
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
{ value: 'gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro Preview' },
|
||||
{ value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' },
|
||||
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
|
||||
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
|
||||
{ value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite' },
|
||||
{ value: 'auto', label: 'Default' },
|
||||
...GEMINI_MODEL_PRESETS.map(m => ({ value: m, label: GEMINI_MODEL_LABELS[m] })),
|
||||
],
|
||||
opencode: [],
|
||||
}
|
||||
|
||||
@@ -13,3 +13,7 @@ export function isCursorFlavor(flavor?: string | null): boolean {
|
||||
export function isKnownFlavor(flavor?: string | null): boolean {
|
||||
return isClaudeFlavor(flavor) || isCodexFamilyFlavor(flavor) || isCursorFlavor(flavor)
|
||||
}
|
||||
|
||||
export function supportsModelChange(flavor?: string | null): boolean {
|
||||
return flavor === 'claude' || flavor === 'gemini'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user