Skip to content

Commit 653b352

Browse files
committed
feat: add model-specific context limits via modelLimits
Introduce toolSettings.modelLimits to allow per-model context limits as absolute token values or percentages (`contextLimit` or hardcoded value is fallback). { "toolSettings": { "contextLimit": "60%", "modelLimits": { "opencode/dax-1": "40%", "opencode/zen-3": 120000 } } }
1 parent 9aa2692 commit 653b352

3 files changed

Lines changed: 88 additions & 13 deletions

File tree

dcp.schema.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,21 @@
121121
"pattern": "^\\d+(?:\\.\\d+)?%$"
122122
}
123123
]
124+
},
125+
"modelLimits": {
126+
"description": "Model-specific context limits with optional wildcard patterns (exact match first, then most specific wildcard). Examples: \"openai/gpt-5\", \"*/zen-1\", \"ollama/*\", \"*sonnet*\"",
127+
"type": "object",
128+
"additionalProperties": {
129+
"oneOf": [
130+
{
131+
"type": "number"
132+
},
133+
{
134+
"type": "string",
135+
"pattern": "^\\d+(?:\\.\\d+)?%$"
136+
}
137+
]
138+
}
124139
}
125140
}
126141
},

lib/config.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface ToolSettings {
2828
nudgeFrequency: number
2929
protectedTools: string[]
3030
contextLimit: number | `${number}%`
31+
modelLimits?: Record<string, number | `${number}%`>
3132
}
3233

3334
export interface Tools {
@@ -107,6 +108,7 @@ export const VALID_CONFIG_KEYS = new Set([
107108
"tools.settings.nudgeFrequency",
108109
"tools.settings.protectedTools",
109110
"tools.settings.contextLimit",
111+
"tools.settings.modelLimits",
110112
"tools.distill",
111113
"tools.distill.permission",
112114
"tools.distill.showDistillation",
@@ -136,6 +138,12 @@ function getConfigKeyPaths(obj: Record<string, any>, prefix = ""): string[] {
136138
for (const key of Object.keys(obj)) {
137139
const fullKey = prefix ? `${prefix}.${key}` : key
138140
keys.push(fullKey)
141+
142+
// modelLimits is a dynamic map keyed by model ID; do not recurse into arbitrary IDs.
143+
if (fullKey === "tools.settings.modelLimits") {
144+
continue
145+
}
146+
139147
if (obj[key] && typeof obj[key] === "object" && !Array.isArray(obj[key])) {
140148
keys.push(...getConfigKeyPaths(obj[key], fullKey))
141149
}
@@ -156,7 +164,7 @@ interface ValidationError {
156164
actual: string
157165
}
158166

159-
function validateConfigTypes(config: Record<string, any>): ValidationError[] {
167+
export function validateConfigTypes(config: Record<string, any>): ValidationError[] {
160168
const errors: ValidationError[] = []
161169

162170
// Top-level validators
@@ -303,9 +311,32 @@ function validateConfigTypes(config: Record<string, any>): ValidationError[] {
303311
})
304312
}
305313
}
306-
}
307-
if (tools.distill) {
308-
if (tools.distill.permission !== undefined) {
314+
if (tools.settings.modelLimits !== undefined) {
315+
if (
316+
typeof tools.settings.modelLimits !== "object" ||
317+
Array.isArray(tools.settings.modelLimits)
318+
) {
319+
errors.push({
320+
key: "tools.settings.modelLimits",
321+
expected: "Record<string, number | ${number}%>",
322+
actual: typeof tools.settings.modelLimits,
323+
})
324+
} else {
325+
for (const [modelId, limit] of Object.entries(tools.settings.modelLimits)) {
326+
const isValidNumber = typeof limit === "number"
327+
const isPercentString =
328+
typeof limit === "string" && /^\d+(?:\.\d+)?%$/.test(limit)
329+
if (!isValidNumber && !isPercentString) {
330+
errors.push({
331+
key: `tools.settings.modelLimits.${modelId}`,
332+
expected: 'number | "${number}%"',
333+
actual: JSON.stringify(limit),
334+
})
335+
}
336+
}
337+
}
338+
}
339+
if (tools.distill?.permission !== undefined) {
309340
const validValues = ["ask", "allow", "deny"]
310341
if (!validValues.includes(tools.distill.permission)) {
311342
errors.push({
@@ -316,7 +347,7 @@ function validateConfigTypes(config: Record<string, any>): ValidationError[] {
316347
}
317348
}
318349
if (
319-
tools.distill.showDistillation !== undefined &&
350+
tools.distill?.showDistillation !== undefined &&
320351
typeof tools.distill.showDistillation !== "boolean"
321352
) {
322353
errors.push({
@@ -684,6 +715,7 @@ function mergeTools(
684715
]),
685716
],
686717
contextLimit: override.settings?.contextLimit ?? base.settings.contextLimit,
718+
modelLimits: override.settings?.modelLimits ?? base.settings.modelLimits,
687719
},
688720
distill: {
689721
permission: override.distill?.permission ?? base.distill.permission,
@@ -724,6 +756,7 @@ function deepCloneConfig(config: PluginConfig): PluginConfig {
724756
settings: {
725757
...config.tools.settings,
726758
protectedTools: [...config.tools.settings.protectedTools],
759+
modelLimits: { ...config.tools.settings.modelLimits },
727760
},
728761
distill: { ...config.tools.distill },
729762
compress: { ...config.tools.compress },

lib/messages/inject.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ function parsePercentageString(value: string, total: number): number | undefined
2727
return Math.round((clampedPercent / 100) * total)
2828
}
2929

30+
export const findModelLimit = (
31+
modelId: string,
32+
modelLimits: Record<string, number | `${number}%`>,
33+
): number | `${number}%` | undefined => {
34+
return modelLimits[modelId]
35+
}
36+
3037
// XML wrappers
3138
export const wrapPrunableTools = (content: string): string => {
3239
return `<prunable-tools>
@@ -66,21 +73,41 @@ Context management was just performed. Do NOT use the ${toolName} again. A fresh
6673
</context-info>`
6774
}
6875

69-
const resolveContextLimit = (config: PluginConfig, state: SessionState): number | undefined => {
70-
const configLimit = config.tools.settings.contextLimit
76+
const resolveContextLimit = (
77+
config: PluginConfig,
78+
state: SessionState,
79+
messages: WithParts[],
80+
): number | undefined => {
81+
const { settings } = config.tools
82+
const { modelLimits, contextLimit } = settings
83+
84+
if (modelLimits) {
85+
const userMsg = getLastUserMessage(messages)
86+
const modelId = userMsg ? (userMsg.info as UserMessage).model.modelID : undefined
87+
const limit = modelId !== undefined ? findModelLimit(modelId, modelLimits) : undefined
88+
89+
if (limit !== undefined) {
90+
if (typeof limit === "string" && limit.endsWith("%")) {
91+
if (state.modelContextLimit === undefined) {
92+
return undefined
93+
}
94+
return parsePercentageString(limit, state.modelContextLimit)
95+
}
96+
return typeof limit === "number" ? limit : undefined
97+
}
98+
}
7199

72-
if (typeof configLimit === "string") {
73-
if (configLimit.endsWith("%")) {
100+
if (typeof contextLimit === "string") {
101+
if (contextLimit.endsWith("%")) {
74102
if (state.modelContextLimit === undefined) {
75103
return undefined
76104
}
77-
return parsePercentageString(configLimit, state.modelContextLimit)
105+
return parsePercentageString(contextLimit, state.modelContextLimit)
78106
}
79-
80107
return undefined
81108
}
82109

83-
return configLimit
110+
return contextLimit
84111
}
85112

86113
const shouldInjectCompressNudge = (
@@ -92,7 +119,7 @@ const shouldInjectCompressNudge = (
92119
return false
93120
}
94121

95-
const contextLimit = resolveContextLimit(config, state)
122+
const contextLimit = resolveContextLimit(config, state, messages)
96123
if (contextLimit === undefined) {
97124
return false
98125
}

0 commit comments

Comments
 (0)