feat(config): support model-specific max_tokens and fix config key corruption

This commit is contained in:
dtapps 2026-03-28 23:37:21 +08:00
parent e011284d8f
commit 60c6bcfe5b
12 changed files with 61 additions and 2 deletions

View file

@ -131,6 +131,9 @@ func NewAgentInstance(
}
maxTokens := defaults.MaxTokens
if mc, err := cfg.GetModelConfig(defaults.OriginalModelName); err == nil {
maxTokens = mc.MaxTokens
}
if maxTokens == 0 {
maxTokens = 8192
}

View file

@ -327,6 +327,9 @@ type AgentDefaults struct {
SubTurn SubTurnConfig `json:"subturn" envPrefix:"PICOCLAW_AGENTS_DEFAULTS_SUBTURN_"`
ToolFeedback ToolFeedbackConfig `json:"tool_feedback,omitempty"`
SplitOnMarker bool `json:"split_on_marker" env:"PICOCLAW_AGENTS_DEFAULTS_SPLIT_ON_MARKER"` // split messages on <|[SPLIT]|> marker
// OriginalModelName holds the original model name as specified in the config.
// This is used to resolve the model name in the model_list configuration.
OriginalModelName string `json:"-"`
}
const DefaultMaxMediaSize = 20 * 1024 * 1024 // 20 MB
@ -668,6 +671,7 @@ type ModelConfig struct {
// Optional optimizations
RPM int `json:"rpm,omitempty"` // Requests per minute limit
MaxTokens int `json:"max_tokens,omitempty"` // Maximum number of tokens per request
MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens")
RequestTimeout int `json:"request_timeout,omitempty"`
ThinkingLevel string `json:"thinking_level,omitempty"` // Extended thinking: off|low|medium|high|xhigh|adaptive
@ -1287,6 +1291,7 @@ func expandMultiKeyModels(models []*ModelConfig) []*ModelConfig {
ConnectMode: m.ConnectMode,
Workspace: m.Workspace,
RPM: m.RPM,
MaxTokens: m.MaxTokens,
MaxTokensField: m.MaxTokensField,
RequestTimeout: m.RequestTimeout,
ThinkingLevel: m.ThinkingLevel,
@ -1307,6 +1312,7 @@ func expandMultiKeyModels(models []*ModelConfig) []*ModelConfig {
ConnectMode: m.ConnectMode,
Workspace: m.Workspace,
RPM: m.RPM,
MaxTokens: m.MaxTokens,
MaxTokensField: m.MaxTokensField,
RequestTimeout: m.RequestTimeout,
ThinkingLevel: m.ThinkingLevel,

View file

@ -660,6 +660,7 @@ type modelConfigV0 struct {
// Optional optimizations
RPM int `json:"rpm,omitempty"` // Requests per minute limit
MaxTokens int `json:"max_tokens,omitempty"` // Maximum number of tokens per request
MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens")
RequestTimeout int `json:"request_timeout,omitempty"`
ThinkingLevel string `json:"thinking_level,omitempty"` // Extended thinking: off|low|medium|high|xhigh|adaptive
@ -744,6 +745,7 @@ func (c *configV0) Migrate() (*Config, error) {
ConnectMode: m.ConnectMode,
Workspace: m.Workspace,
RPM: m.RPM,
MaxTokens: m.MaxTokens,
MaxTokensField: m.MaxTokensField,
RequestTimeout: m.RequestTimeout,
ThinkingLevel: m.ThinkingLevel,

View file

@ -499,6 +499,7 @@ func loadConfigV0(data []byte) (migratable, error) {
ConnectMode: m.ConnectMode,
Workspace: m.Workspace,
RPM: m.RPM,
MaxTokens: m.MaxTokens,
MaxTokensField: m.MaxTokensField,
RequestTimeout: m.RequestTimeout,
ThinkingLevel: m.ThinkingLevel,

View file

@ -192,6 +192,7 @@ func TestExpandMultiKeyModels_PreservesOtherFields(t *testing.T) {
APIBase: "https://api.example.com",
Proxy: "http://proxy:8080",
RPM: 60,
MaxTokens: 1024,
MaxTokensField: "max_completion_tokens",
RequestTimeout: 30,
ThinkingLevel: "high",
@ -212,6 +213,9 @@ func TestExpandMultiKeyModels_PreservesOtherFields(t *testing.T) {
if primary.RPM != 60 {
t.Errorf("expected rpm preserved, got %d", primary.RPM)
}
if primary.MaxTokens != 1024 {
t.Errorf("expected max_tokens preserved, got %d", primary.MaxTokens)
}
if primary.MaxTokensField != "max_completion_tokens" {
t.Errorf("expected max_tokens_field preserved, got %q", primary.MaxTokensField)
}

View file

@ -113,6 +113,9 @@ func Run(debug bool, homePath, configPath string, allowEmptyStartup bool) error
}
if modelID != "" {
if cfg.Agents.Defaults.OriginalModelName == "" {
cfg.Agents.Defaults.OriginalModelName = cfg.Agents.Defaults.ModelName
}
cfg.Agents.Defaults.ModelName = modelID
}

View file

@ -35,6 +35,7 @@ type modelResponse struct {
ConnectMode string `json:"connect_mode,omitempty"`
Workspace string `json:"workspace,omitempty"`
RPM int `json:"rpm,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
MaxTokensField string `json:"max_tokens_field,omitempty"`
RequestTimeout int `json:"request_timeout,omitempty"`
ThinkingLevel string `json:"thinking_level,omitempty"`
@ -81,6 +82,7 @@ func (h *Handler) handleListModels(w http.ResponseWriter, r *http.Request) {
ConnectMode: m.ConnectMode,
Workspace: m.Workspace,
RPM: m.RPM,
MaxTokens: m.MaxTokens,
MaxTokensField: m.MaxTokensField,
RequestTimeout: m.RequestTimeout,
ThinkingLevel: m.ThinkingLevel,

View file

@ -14,6 +14,7 @@ export interface ModelInfo {
connect_mode?: string
workspace?: string
rpm?: number
max_tokens?: number
max_tokens_field?: string
request_timeout?: number
thinking_level?: string

View file

@ -32,6 +32,7 @@ interface AddForm {
connectMode: string
workspace: string
rpm: string
maxTokens: string
maxTokensField: string
requestTimeout: string
thinkingLevel: string
@ -48,6 +49,7 @@ const EMPTY_ADD_FORM: AddForm = {
connectMode: "",
workspace: "",
rpm: "",
maxTokens: "",
maxTokensField: "",
requestTimeout: "",
thinkingLevel: "",
@ -128,6 +130,7 @@ export function AddModelSheet({
connect_mode: form.connectMode.trim() || undefined,
workspace: form.workspace.trim() || undefined,
rpm: form.rpm ? Number(form.rpm) : undefined,
max_tokens: form.maxTokens ? Number(form.maxTokens) : undefined,
max_tokens_field: form.maxTokensField.trim() || undefined,
request_timeout: form.requestTimeout
? Number(form.requestTimeout)
@ -302,6 +305,19 @@ export function AddModelSheet({
/>
</Field>
<Field
label={t("models.field.maxTokens")}
hint={t("models.field.maxTokensHint")}
>
<Input
value={form.maxTokens}
onChange={setField("maxTokens")}
placeholder="0"
type="number"
min={0}
/>
</Field>
<Field
label={t("models.field.maxTokensField")}
hint={t("models.field.maxTokensFieldHint")}

View file

@ -30,6 +30,7 @@ interface EditForm {
connectMode: string
workspace: string
rpm: string
maxTokens: string
maxTokensField: string
requestTimeout: string
thinkingLevel: string
@ -58,6 +59,7 @@ export function EditModelSheet({
connectMode: "",
workspace: "",
rpm: "",
maxTokens: "",
maxTokensField: "",
requestTimeout: "",
thinkingLevel: "",
@ -77,6 +79,7 @@ export function EditModelSheet({
connectMode: model.connect_mode ?? "",
workspace: model.workspace ?? "",
rpm: model.rpm ? String(model.rpm) : "",
maxTokens: model.max_tokens ? String(model.max_tokens) : "",
maxTokensField: model.max_tokens_field ?? "",
requestTimeout: model.request_timeout
? String(model.request_timeout)
@ -111,6 +114,7 @@ export function EditModelSheet({
connect_mode: form.connectMode || undefined,
workspace: form.workspace || undefined,
rpm: form.rpm ? Number(form.rpm) : undefined,
max_tokens: form.maxTokens ? Number(form.maxTokens) : undefined,
max_tokens_field: form.maxTokensField || undefined,
request_timeout: form.requestTimeout
? Number(form.requestTimeout)
@ -273,6 +277,19 @@ export function EditModelSheet({
/>
</Field>
<Field
label={t("models.field.maxTokens")}
hint={t("models.field.maxTokensHint")}
>
<Input
value={form.maxTokens}
onChange={setField("maxTokens")}
placeholder="0"
type="number"
min={0}
/>
</Field>
<Field
label={t("models.field.maxTokensField")}
hint={t("models.field.maxTokensFieldHint")}

View file

@ -208,6 +208,8 @@
"rpmHint": "Maximum requests per minute. 0 = no limit.",
"thinkingLevel": "Thinking Level",
"thinkingLevelHint": "Extended thinking budget: off, low, medium, high, xhigh, adaptive.",
"maxTokens": "Max Tokens",
"maxTokensHint": "Maximum tokens allowed for a single request. 0 = use default configuration.",
"maxTokensField": "Max Tokens Field",
"maxTokensFieldHint": "Override the request field name for max tokens, e.g. max_completion_tokens.",
"extraBody": "Extra Body",

View file

@ -208,6 +208,8 @@
"rpmHint": "每分钟最大请求数0 表示不限制。",
"thinkingLevel": "思考级别",
"thinkingLevelHint": "扩展思考预算off、low、medium、high、xhigh、adaptive。",
"maxTokens": "最大 Token 数",
"maxTokensHint": "单次请求允许生成的最大 Token 数。若设置为 0则使用默认配置。",
"maxTokensField": "Max Tokens 字段名",
"maxTokensFieldHint": "覆盖请求中 max_tokens 的字段名,例如 max_completion_tokens。",
"extraBody": "Extra Body",