Add support for claude 1M context. close #291

This commit is contained in:
weishu
2026-03-16 08:51:40 +08:00
parent fedc8e79eb
commit ce5edc42e1
12 changed files with 103 additions and 11 deletions
+2 -3
View File
@@ -8,6 +8,7 @@ import { claudeRemoteLauncher } from "./claudeRemoteLauncher"
import { ApiClient } from "@/lib"
import type { SessionModelMode } from "@/api/types"
import type { ClaudePermissionMode } from "@hapi/protocol/types"
import { resolveClaudeSessionModelMode } from "./modelMode"
export type PermissionMode = ClaudePermissionMode;
@@ -45,9 +46,7 @@ export async function loop(opts: LoopOptions) {
const logPath = logger.logFilePath;
const startedBy = opts.startedBy ?? 'terminal';
const startingMode = opts.startingMode ?? 'local';
const modelMode: SessionModelMode = opts.model === 'sonnet' || opts.model === 'opus'
? opts.model
: 'default';
const modelMode: SessionModelMode = resolveClaudeSessionModelMode(opts.model)
const session = new Session({
api: opts.api,
client: opts.session,
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'
import { resolveClaudeSessionModelMode } from './modelMode'
describe('resolveClaudeSessionModelMode', () => {
it('returns default when model is missing', () => {
expect(resolveClaudeSessionModelMode()).toBe('default')
})
it('returns default for auto and unsupported models', () => {
expect(resolveClaudeSessionModelMode('auto')).toBe('default')
expect(resolveClaudeSessionModelMode('claude-sonnet-4-5')).toBe('default')
})
it('returns standard Claude session model modes', () => {
expect(resolveClaudeSessionModelMode('sonnet')).toBe('sonnet')
expect(resolveClaudeSessionModelMode('opus')).toBe('opus')
})
it('returns 1m Claude session model modes', () => {
expect(resolveClaudeSessionModelMode('sonnet[1m]')).toBe('sonnet[1m]')
expect(resolveClaudeSessionModelMode('opus[1m]')).toBe('opus[1m]')
})
})
+18
View File
@@ -0,0 +1,18 @@
import type { SessionModelMode } from '@/api/types'
const CLAUDE_SESSION_MODEL_MODES = new Set<SessionModelMode>([
'sonnet',
'sonnet[1m]',
'opus',
'opus[1m]'
])
export function resolveClaudeSessionModelMode(model?: string): SessionModelMode {
if (!model) {
return 'default'
}
return CLAUDE_SESSION_MODEL_MODES.has(model as SessionModelMode)
? model as SessionModelMode
: 'default'
}
+2 -1
View File
@@ -17,6 +17,7 @@ import { createModeChangeHandler, createRunnerLifecycle, setControlledByUser } f
import { isModelModeAllowedForFlavor, isPermissionModeAllowedForFlavor } from '@hapi/protocol';
import { ModelModeSchema, PermissionModeSchema } from '@hapi/protocol/schemas';
import { formatMessageWithAttachments } from '@/utils/attachmentFormatter';
import { resolveClaudeSessionModelMode } from './modelMode';
export interface StartOptions {
model?: string
@@ -143,7 +144,7 @@ export async function runClaude(options: StartOptions = {}): Promise<void> {
// Forward messages to the queue
let currentPermissionMode: PermissionMode = options.permissionMode ?? 'default';
let currentModelMode: SessionModelMode = options.model === 'sonnet' || options.model === 'opus' ? options.model : 'default';
let currentModelMode: SessionModelMode = resolveClaudeSessionModelMode(options.model);
let currentFallbackModel: string | undefined = undefined; // Track current fallback model
let currentCustomSystemPrompt: string | undefined = undefined; // Track current custom system prompt
let currentAppendSystemPrompt: string | undefined = undefined; // Track current append system prompt
+8 -2
View File
@@ -25,7 +25,7 @@ export const PERMISSION_MODES = [
] as const
export type PermissionMode = typeof PERMISSION_MODES[number]
export const MODEL_MODES = ['default', 'sonnet', 'opus'] as const
export const MODEL_MODES = ['default', 'sonnet', 'sonnet[1m]', 'opus', 'opus[1m]'] as const
export type ModelMode = typeof MODEL_MODES[number]
export type AgentFlavor = 'claude' | 'codex' | 'gemini' | 'opencode' | 'cursor'
@@ -63,7 +63,13 @@ export type PermissionModeOption = {
export const MODEL_MODE_LABELS: Record<ModelMode, string> = {
default: 'Default',
sonnet: 'Sonnet',
opus: 'Opus'
'sonnet[1m]': 'Sonnet 1M',
opus: 'Opus',
'opus[1m]': 'Opus 1M'
}
export function getModelModeLabel(mode: ModelMode): string {
return MODEL_MODE_LABELS[mode]
}
export function getPermissionModeLabel(mode: PermissionMode): string {
+1 -1
View File
@@ -48,7 +48,7 @@ See `src/router.tsx` for route definitions.
- Message thread with infinite scroll.
- Composer for sending messages.
- Permission mode toggle (default/acceptEdits/bypassPermissions/plan).
- Model selection (default/sonnet/opus).
- Model selection (default/sonnet/sonnet[1m]/opus/opus[1m]).
- Session abort and mode switch controls.
- Context size display.
+16
View File
@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest'
import { getContextBudgetTokens } from './modelConfig'
describe('getContextBudgetTokens', () => {
it('uses the existing 200k budget for default Claude modes', () => {
expect(getContextBudgetTokens(undefined)).toBe(190_000)
expect(getContextBudgetTokens('default')).toBe(190_000)
expect(getContextBudgetTokens('sonnet')).toBe(190_000)
expect(getContextBudgetTokens('opus')).toBe(190_000)
})
it('uses the 1m budget for Claude 1m modes', () => {
expect(getContextBudgetTokens('sonnet[1m]')).toBe(990_000)
expect(getContextBudgetTokens('opus[1m]')).toBe(990_000)
})
})
+4 -2
View File
@@ -13,10 +13,12 @@ import type { ModelMode } from '@/types/api'
const CONTEXT_HEADROOM_TOKENS = 10_000
const MODEL_CONTEXT_WINDOWS: Record<ModelMode, number> = {
// Claude Code modes used in this app; currently treated as ~200k context.
// Claude Code modes used in this app. 1M variants get the larger budget.
default: 200_000,
sonnet: 200_000,
opus: 200_000
'sonnet[1m]': 1_000_000,
opus: 200_000,
'opus[1m]': 1_000_000
}
export function getContextBudgetTokens(modelMode: ModelMode | undefined): number | null {
@@ -0,0 +1,21 @@
import { getModelModeLabel, MODEL_MODES } from '@hapi/protocol'
import { describe, expect, it } from 'vitest'
import { MODEL_OPTIONS } from './types'
describe('Claude model options', () => {
it('includes 1m model options in the expected order', () => {
expect(MODEL_OPTIONS.claude).toEqual([
{ value: 'auto', label: 'Auto' },
{ value: 'opus', label: 'Opus' },
{ value: 'opus[1m]', label: 'Opus 1M' },
{ value: 'sonnet', label: 'Sonnet' },
{ value: 'sonnet[1m]', label: 'Sonnet 1M' },
])
})
it('exposes friendly labels for session model modes', () => {
expect(MODEL_MODES).toEqual(['default', 'sonnet', 'sonnet[1m]', 'opus', 'opus[1m]'])
expect(getModelModeLabel('sonnet[1m]')).toBe('Sonnet 1M')
expect(getModelModeLabel('opus[1m]')).toBe('Opus 1M')
})
})
+2
View File
@@ -5,7 +5,9 @@ export const MODEL_OPTIONS: Record<AgentType, { value: string; label: string }[]
claude: [
{ value: 'auto', label: 'Auto' },
{ value: 'opus', label: 'Opus' },
{ value: 'opus[1m]', label: 'Opus 1M' },
{ value: 'sonnet', label: 'Sonnet' },
{ value: 'sonnet[1m]', label: 'Sonnet 1M' },
],
codex: [
{ value: 'auto', label: 'Auto' },
+3 -1
View File
@@ -1,3 +1,4 @@
import { getModelModeLabel } from '@hapi/protocol'
import { useId, useMemo, useRef, useState } from 'react'
import type { Session } from '@/types/api'
import type { ApiClient } from '@/api/client'
@@ -70,6 +71,7 @@ export function SessionHeader(props: {
const { session, api, onSessionDeleted } = props
const title = useMemo(() => getSessionTitle(session), [session])
const worktreeBranch = session.metadata?.worktree?.branch
const modelModeLabel = getModelModeLabel(session.modelMode ?? 'default')
const [menuOpen, setMenuOpen] = useState(false)
const [menuAnchorPoint, setMenuAnchorPoint] = useState<{ x: number; y: number }>({ x: 0, y: 0 })
@@ -139,7 +141,7 @@ export function SessionHeader(props: {
{session.metadata?.flavor?.trim() || 'unknown'}
</span>
<span>
{t('session.item.modelMode')}: {session.modelMode || 'default'}
{t('session.item.modelMode')}: {modelModeLabel}
</span>
{worktreeBranch ? (
<span>{t('session.item.worktree')}: {worktreeBranch}</span>
+3 -1
View File
@@ -1,3 +1,4 @@
import { getModelModeLabel } from '@hapi/protocol'
import { useEffect, useMemo, useState } from 'react'
import type { SessionSummary } from '@/types/api'
import type { ApiClient } from '@/api/client'
@@ -198,6 +199,7 @@ function SessionItem(props: {
})
const sessionName = getSessionTitle(s)
const modelModeLabel = getModelModeLabel(s.modelMode ?? 'default')
const statusDotClass = s.active
? (s.thinking ? 'bg-[#007AFF]' : 'bg-[var(--app-badge-success-text)]')
: 'bg-[var(--app-hint)]'
@@ -259,7 +261,7 @@ function SessionItem(props: {
</span>
{getAgentLabel(s)}
</span>
<span>{t('session.item.modelMode')}: {s.modelMode || 'default'}</span>
<span>{t('session.item.modelMode')}: {modelModeLabel}</span>
{s.metadata?.worktree?.branch ? (
<span>{t('session.item.worktree')}: {s.metadata.worktree.branch}</span>
) : null}