Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ Write each change in both `### English` and `### 中文` under `## Unreleased`.
- Make the Overview traffic chart taller so the area/line plot is readable
- Give the Overview traffic chart a full-width row so the series can use the console width
- Close the add-account dialog after a Trae callback succeeds, and give the pasted callback URL a fixed-height field
- Keep the Qoder context integer on Models, add a Trae Max-context switch, and map OpenAI reasoning fields onto each Trae model's allowed levels
- Size the Logs custom date-range field so start/end datetimes fit, and keep the calendar popover at calendar width

### 中文

- 加高概览流量图,面积折线更容易看清
- 概览流量图单独占一行,折线铺满控制台宽度
- Trae 提交回调成功后关闭添加账号弹窗,并把粘贴框改成固定高度的多行输入
- 模型页保留 Qoder 整数上下文;Trae 增加 Max 开关,并把 OpenAI 推理字段映射到该模型允许的档位
- 日志自定义时间范围加宽到能放下起止日期时间,日历弹层保持日历宽度

## 0.2.18 - 2026-08-29

Expand Down
12 changes: 12 additions & 0 deletions frontend/src/api/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ export function updateModelContext(modelKey: string, contextLength: number) {
})
}

export function updateTraeMaxMode(modelKey: string, maxMode: boolean) {
return api<{
model: string
provider: string
max_mode: boolean
context_custom: boolean
}>(`/api/models/trae/${encodeURIComponent(modelKey)}`, {
method: 'PATCH',
body: JSON.stringify({ max_mode: maxMode }),
})
}

export function rewarmWorker(accountId?: string) {
if (!accountId) throw new Error('account id required')
return api(`/api/accounts/${encodeURIComponent(accountId)}/rewarm`, { method: 'POST', body: '{}' })
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ export type ModelInfo = {
context_length?: number
default_context_length?: number
context_custom?: boolean
context_editable?: boolean
catalog_context_length?: number
catalog_context_length_max?: number
supports_max_mode?: boolean
max_mode?: boolean
max_output_tokens?: number
prompt_max_tokens?: number
reasoning_options?: string[]
reasoning_default?: string
reasoning_type?: string
}

export type Overview = {
Expand Down
70 changes: 70 additions & 0 deletions frontend/src/components/ModelDetailsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Chip, Modal } from '@heroui/react'
import { X } from '@phosphor-icons/react'
import type { ModelInfo } from '@/api/types'

type Translate = (key: string, vars?: Record<string, string | number>) => string

type Props = {
model: ModelInfo | null
t: Translate
onClose: () => void
}

function formatTokens(value?: number) {
if (!value) return '—'
if (value >= 1_000_000) return `${(value / 1_000_000).toString().replace(/\.0$/, '')}M`
if (value >= 1000) return `${Math.round(value / 1000)}k`
return String(value)
}

export function ModelDetailsModal({ model, t, onClose }: Props) {
if (!model) return null
const options = model.reasoning_options || []
const windowDev = model.catalog_context_length || model.default_context_length || model.context_length
const windowMax = model.catalog_context_length_max
return (
<Modal.Root isOpen onOpenChange={(next: boolean) => { if (!next) onClose() }}>
<Modal.Backdrop variant="blur">
<Modal.Container size="md" scroll="inside">
<Modal.Dialog>
<Modal.Header className="items-start justify-between gap-4 px-5 pt-5">
<div>
<div className="text-base font-semibold tracking-tight">{model.display_name || model.id}</div>
<div className="mono mt-1 text-[11px] text-[var(--app-faint)]">{model.id}</div>
</div>
<button type="button" className="grid size-8 place-items-center rounded-md text-[var(--app-muted)] hover:bg-[var(--app-surface-muted)]" onClick={onClose} aria-label={t('close')}>
<X size={16} />
</button>
</Modal.Header>
<Modal.Body className="space-y-4 px-5 pb-5">
<section>
<div className="text-xs font-medium text-[var(--app-muted)]">{t('contextWindowCol')}</div>
<div className="mt-1 text-sm">{formatTokens(windowDev)}{windowMax && windowMax !== windowDev ? ` → ${formatTokens(windowMax)}` : ''}</div>
{model.prompt_max_tokens ? <div className="mt-1 text-[11px] text-[var(--app-faint)]">{t('promptMaxTokens')}: {formatTokens(model.prompt_max_tokens)}</div> : null}
{model.max_output_tokens ? <div className="text-[11px] text-[var(--app-faint)]">{t('maxOutputTokens')}: {formatTokens(model.max_output_tokens)}</div> : null}
</section>
<section>
<div className="text-xs font-medium text-[var(--app-muted)]">{t('reasoningLevels')}</div>
{options.length ? (
<div className="mt-2 flex flex-wrap gap-1.5">
{options.map((level) => (
<Chip key={level} size="sm" variant="soft" color={level === model.reasoning_default ? 'success' : 'default'}>
{level}{level === model.reasoning_default ? ` · ${t('defaultValue')}` : ''}
</Chip>
))}
</div>
) : (
<div className="mt-1 text-sm text-[var(--app-muted)]">
{model.reasoning_type ? t('reasoningFixed', { type: model.reasoning_type }) : t('noReasoningLevels')}
</div>
)}
</section>
</Modal.Body>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal.Root>
)
}

export { formatTokens }
22 changes: 22 additions & 0 deletions frontend/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,17 @@ export const messages: Record<Lang, Dict> = {
contextInvalid: 'Context window must be an integer between 1,024 and 4,000,000 tokens.',
contextSaved: 'Saved context window for {model}.',
contextReset: 'Restored the default context window for {model}.',
contextMaxOn: 'Enabled Max context for {model}.',
contextMaxOff: 'Disabled Max context for {model}.',
contextMaxUnavailable: 'This model has no Max context switch.',
maxMode: 'Max',
catalogWindow: 'Catalog window',
reasoningLevels: 'Reasoning levels',
reasoningFixed: 'Fixed thinking: {type}',
noReasoningLevels: 'This model has no selectable reasoning levels.',
modelDetails: 'Details',
promptMaxTokens: 'Prompt max',
maxOutputTokens: 'Output max',
actions: 'Actions',
save: 'Save',
custom: 'Custom',
Expand Down Expand Up @@ -746,6 +757,17 @@ export const messages: Record<Lang, Dict> = {
contextInvalid: '上下文窗口必须是 1,024 到 4,000,000 之间的整数。',
contextSaved: '已保存 {model} 的上下文窗口。',
contextReset: '已恢复 {model} 的默认上下文窗口。',
contextMaxOn: '已开启 {model} 的 Max 上下文。',
contextMaxOff: '已关闭 {model} 的 Max 上下文。',
contextMaxUnavailable: '这个模型没有 Max 上下文开关。',
maxMode: 'Max',
catalogWindow: '目录窗口',
reasoningLevels: '推理强度',
reasoningFixed: '固定思考:{type}',
noReasoningLevels: '这个模型没有可选的推理档位。',
modelDetails: '详情',
promptMaxTokens: '输入上限',
maxOutputTokens: '输出上限',
actions: '操作',
save: '保存',
custom: '自定义',
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/pages/LogsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ export function LogsPage() {
</ButtonGroup>
{timeRange === 'custom' ? (
<DateRangePicker
className="w-[min(100%,22rem)]"
className="w-[min(100%,42rem)]"
granularity="minute"
hourCycle={24}
shouldForceLeadingZeros
Expand All @@ -494,12 +494,12 @@ export function LogsPage() {
>
{({ state }) => (
<>
<DateField.Group className="h-8 min-h-8 text-xs" variant="secondary">
<DateField.Input slot="start" aria-label={t('logsTimeFrom')}>
<DateField.Group className="h-8 min-h-8 w-full min-w-0 text-xs" variant="secondary">
<DateField.Input className="min-w-0" slot="start" aria-label={t('logsTimeFrom')}>
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
<DateRangePicker.RangeSeparator />
<DateField.Input slot="end" aria-label={t('logsTimeTo')}>
<DateField.Input className="min-w-0" slot="end" aria-label={t('logsTimeTo')}>
{(segment) => <DateField.Segment segment={segment} />}
</DateField.Input>
<DateField.Suffix>
Expand All @@ -508,7 +508,7 @@ export function LogsPage() {
</DateRangePicker.Trigger>
</DateField.Suffix>
</DateField.Group>
<DateRangePicker.Popover className="flex w-full flex-col gap-3">
<DateRangePicker.Popover className="flex w-[20.5rem] max-w-[calc(100vw-2rem)] flex-col gap-3 p-3">
<RangeCalendar aria-label={t('logsTimeRange')}>
<RangeCalendar.Header>
<RangeCalendar.YearPickerTrigger>
Expand Down Expand Up @@ -549,7 +549,7 @@ export function LogsPage() {
})
}
>
<TimeField.Group variant="secondary">
<TimeField.Group className="h-8 min-h-8 w-[7.5rem] text-xs" variant="secondary">
<TimeField.Input>
{(segment) => <TimeField.Segment segment={segment} />}
</TimeField.Input>
Expand All @@ -570,7 +570,7 @@ export function LogsPage() {
})
}
>
<TimeField.Group variant="secondary">
<TimeField.Group className="h-8 min-h-8 w-[7.5rem] text-xs" variant="secondary">
<TimeField.Input>
{(segment) => <TimeField.Segment segment={segment} />}
</TimeField.Input>
Expand Down
113 changes: 92 additions & 21 deletions frontend/src/pages/ProvidersPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useMemo, useState } from 'react'
import { Button, Card, Chip, Input, Table } from '@heroui/react'
import { Cube, ArrowClockwise, ArrowCounterClockwise, FloppyDisk, MagnifyingGlass } from '@phosphor-icons/react'
import { Cube, ArrowClockwise, ArrowCounterClockwise, FloppyDisk, MagnifyingGlass, Info } from '@phosphor-icons/react'
import { useI18n } from '@/hooks/useI18n'
import { useOverview } from '@/hooks/useOverview'
import { refreshModels, updateModelContext } from '@/api/overview'
import { refreshModels, updateModelContext, updateTraeMaxMode } from '@/api/overview'
import type { Overview } from '@/api/types'
import { ProviderMark } from '@/components/ProviderMark'
import { ModelDetailsModal, formatTokens } from '@/components/ModelDetailsModal'
import { CompactSwitch } from '@/components/ui/CompactSwitch'
import { ListPager, type PageSize } from '@/components/ui/ListPager'
import { ProvidersPageSkeleton } from '@/components/ui/PageSkeletons'

Expand Down Expand Up @@ -43,6 +45,7 @@ export function ProvidersPage() {
const [message, setMessage] = useState('')
const [messageError, setMessageError] = useState(false)
const [drafts, setDrafts] = useState<Record<string, string>>({})
const [detailModel, setDetailModel] = useState<ModelInfo | null>(null)
const models = useMemo(() => overview?.models || [], [overview?.models])
const providers = useMemo(() => {
const ids = new Set<string>()
Expand Down Expand Up @@ -98,6 +101,22 @@ export function ProvidersPage() {
setDrafts((current) => ({ ...current, [key]: String(result.context_length) }))
}

function updateTraeInOverview(model: ModelInfo, maxMode: boolean) {
const key = modelSettingsKey(model)
const nextModels = models.map((item) => {
if (modelSettingsKey(item) !== key || modelProvider(item) !== 'trae') return item
const dev = item.catalog_context_length || item.default_context_length || item.context_length
const max = item.catalog_context_length_max
return {
...item,
max_mode: maxMode,
context_custom: maxMode,
context_length: maxMode && max ? max : dev,
}
})
setOverview({ ...(overview || {}), models: nextModels })
}

async function onRefresh() {
setBusy(true)
setMessage('')
Expand Down Expand Up @@ -136,6 +155,28 @@ export function ProvidersPage() {
}
}

async function onToggleTraeMax(model: ModelInfo, maxMode: boolean) {
if (!model.supports_max_mode) {
setMessageError(true)
setMessage(t('contextMaxUnavailable'))
return
}
const key = modelSettingsKey(model)
setSavingKey(key)
setMessage('')
setMessageError(false)
try {
await updateTraeMaxMode(key, maxMode)
updateTraeInOverview(model, maxMode)
setMessage(maxMode ? t('contextMaxOn', { model: model.id }) : t('contextMaxOff', { model: model.id }))
} catch (error) {
setMessageError(true)
setMessage(error instanceof Error ? error.message : String(error))
} finally {
setSavingKey('')
}
}

async function onReset(model: ModelInfo) {
const key = modelSettingsKey(model)
setSavingKey(key)
Expand Down Expand Up @@ -244,19 +285,40 @@ export function ProvidersPage() {
</Table.Cell>
<Table.Cell><span className="mono text-xs text-[var(--app-muted)]">{model.mapped_key || model.native_model || model.id}</span></Table.Cell>
<Table.Cell>
<div className="flex min-w-52 items-center gap-2">
<Input
className="w-40"
type="number"
min={1024}
max={4000000}
step={1024}
value={drafts[key] ?? String(model.context_length || model.default_context_length || '')}
onChange={(event) => setDrafts((current) => ({ ...current, [key]: event.target.value }))}
aria-label={`${model.id} ${t('contextWindowCol')}`}
/>
<span className="mono text-[10px] text-[var(--app-faint)]">tokens</span>
</div>
{provider === 'qoder' ? (
<div className="flex min-w-52 items-center gap-2">
<Input
className="w-40"
type="number"
min={1024}
max={4000000}
step={1024}
value={drafts[key] ?? String(model.context_length || model.default_context_length || '')}
onChange={(event) => setDrafts((current) => ({ ...current, [key]: event.target.value }))}
aria-label={`${model.id} ${t('contextWindowCol')}`}
/>
<span className="mono text-[10px] text-[var(--app-faint)]">tokens</span>
</div>
) : provider === 'trae' ? (
<div className="flex min-w-52 items-center gap-3">
<span className="mono text-xs text-[var(--app-muted)]">{formatTokens(model.catalog_context_length || model.context_length)}</span>
{model.supports_max_mode ? (
<div className="flex items-center gap-2">
<CompactSwitch
isSelected={Boolean(model.max_mode)}
isDisabled={saving}
ariaLabel={`${model.id} ${t('maxMode')}`}
onChange={(selected) => void onToggleTraeMax(model, selected)}
/>
<span className="text-[11px] text-[var(--app-muted)]">{t('maxMode')}{model.catalog_context_length_max ? ` ${formatTokens(model.catalog_context_length_max)}` : ''}</span>
</div>
) : (
<span className="text-[11px] text-[var(--app-faint)]">{t('catalogWindow')}</span>
)}
</div>
) : (
<span className="mono text-xs text-[var(--app-muted)]">{formatTokens(model.catalog_context_length || model.context_length)}</span>
)}
</Table.Cell>
<Table.Cell>
<Chip size="sm" variant="soft" color={model.context_custom ? 'warning' : model.stale ? 'warning' : 'success'}>
Expand All @@ -265,12 +327,20 @@ export function ProvidersPage() {
</Table.Cell>
<Table.Cell>
<div className="flex items-center gap-2">
<Button size="sm" variant="secondary" isPending={saving} onPress={() => void onSave(model)}>
<FloppyDisk size={14} />{t('save')}
</Button>
<Button size="sm" variant="ghost" isDisabled={saving || !model.context_custom} onPress={() => void onReset(model)} aria-label={t('resetDefault')}>
<ArrowCounterClockwise size={14} />
</Button>
{provider === 'qoder' ? (
<>
<Button size="sm" variant="secondary" isPending={saving} onPress={() => void onSave(model)}>
<FloppyDisk size={14} />{t('save')}
</Button>
<Button size="sm" variant="ghost" isDisabled={saving || !model.context_custom} onPress={() => void onReset(model)} aria-label={t('resetDefault')}>
<ArrowCounterClockwise size={14} />
</Button>
</>
) : (
<Button size="sm" variant="ghost" onPress={() => setDetailModel(model)}>
<Info size={14} />{t('modelDetails')}
</Button>
)}
</div>
</Table.Cell>
</Table.Row>
Expand All @@ -283,6 +353,7 @@ export function ProvidersPage() {
)}
</Card>

<ModelDetailsModal model={detailModel} t={t} onClose={() => setDetailModel(null)} />
<ListPager
total={filtered.length}
page={currentPage}
Expand Down
12 changes: 10 additions & 2 deletions internal/accounts/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,16 @@ CREATE INDEX IF NOT EXISTS request_attempts_request_id ON request_attempts(reque
{filename: "005_account_drop_system_prompt.sql", sql: `
ALTER TABLE accounts ADD COLUMN drop_system_prompt INTEGER NOT NULL DEFAULT 1;`},
{filename: "006_request_log_provider.sql", sql: `
ALTER TABLE request_logs ADD COLUMN provider TEXT NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS request_logs_provider ON request_logs(provider);`},
ALTER TABLE request_logs ADD COLUMN provider TEXT NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS request_logs_provider ON request_logs(provider);`},
{filename: "007_provider_model_settings.sql", sql: `
CREATE TABLE IF NOT EXISTS provider_model_settings (
provider TEXT NOT NULL,
model_id TEXT NOT NULL,
max_mode INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL,
PRIMARY KEY (provider, model_id)
);`},
}

const schemaMigrationsDDL = `
Expand Down
Loading
Loading