Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 15 additions & 0 deletions dcp.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,21 @@
"pattern": "^\\d+(?:\\.\\d+)?%$"
}
]
},
"modelLimits": {
"description": "Model-specific context limits with optional wildcard patterns (exact match first, then most specific wildcard). Examples: \"openai/gpt-5\", \"*/zen-1\", \"ollama/*\", \"*sonnet*\"",
Comment thread
Tarquinen marked this conversation as resolved.
Outdated
"type": "object",
"additionalProperties": {
"oneOf": [
{
"type": "number"
},
{
"type": "string",
"pattern": "^\\d+(?:\\.\\d+)?%$"
Comment thread
Tarquinen marked this conversation as resolved.
}
]
}
}
}
},
Expand Down
43 changes: 38 additions & 5 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface ToolSettings {
nudgeFrequency: number
protectedTools: string[]
contextLimit: number | `${number}%`
modelLimits?: Record<string, number | `${number}%`>
}

export interface Tools {
Expand Down Expand Up @@ -107,6 +108,7 @@ export const VALID_CONFIG_KEYS = new Set([
"tools.settings.nudgeFrequency",
"tools.settings.protectedTools",
"tools.settings.contextLimit",
"tools.settings.modelLimits",
"tools.distill",
"tools.distill.permission",
"tools.distill.showDistillation",
Expand Down Expand Up @@ -136,6 +138,12 @@ function getConfigKeyPaths(obj: Record<string, any>, prefix = ""): string[] {
for (const key of Object.keys(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key
keys.push(fullKey)

// modelLimits is a dynamic map keyed by model ID; do not recurse into arbitrary IDs.
if (fullKey === "tools.settings.modelLimits") {
continue
}

if (obj[key] && typeof obj[key] === "object" && !Array.isArray(obj[key])) {
keys.push(...getConfigKeyPaths(obj[key], fullKey))
}
Expand All @@ -156,7 +164,7 @@ interface ValidationError {
actual: string
}

function validateConfigTypes(config: Record<string, any>): ValidationError[] {
export function validateConfigTypes(config: Record<string, any>): ValidationError[] {
const errors: ValidationError[] = []

// Top-level validators
Expand Down Expand Up @@ -303,9 +311,32 @@ function validateConfigTypes(config: Record<string, any>): ValidationError[] {
})
}
}
}
if (tools.distill) {
Comment thread
Tarquinen marked this conversation as resolved.
if (tools.distill.permission !== undefined) {
if (tools.settings.modelLimits !== undefined) {
if (
typeof tools.settings.modelLimits !== "object" ||
Array.isArray(tools.settings.modelLimits)
) {
errors.push({
key: "tools.settings.modelLimits",
expected: "Record<string, number | ${number}%>",
actual: typeof tools.settings.modelLimits,
})
} else {
for (const [modelId, limit] of Object.entries(tools.settings.modelLimits)) {
const isValidNumber = typeof limit === "number"
const isPercentString =
typeof limit === "string" && /^\d+(?:\.\d+)?%$/.test(limit)
if (!isValidNumber && !isPercentString) {
errors.push({
key: `tools.settings.modelLimits.${modelId}`,
expected: 'number | "${number}%"',
actual: JSON.stringify(limit),
})
}
}
}
}
if (tools.distill?.permission !== undefined) {
const validValues = ["ask", "allow", "deny"]
if (!validValues.includes(tools.distill.permission)) {
errors.push({
Expand All @@ -316,7 +347,7 @@ function validateConfigTypes(config: Record<string, any>): ValidationError[] {
}
}
if (
tools.distill.showDistillation !== undefined &&
Comment thread
Tarquinen marked this conversation as resolved.
tools.distill?.showDistillation !== undefined &&
typeof tools.distill.showDistillation !== "boolean"
) {
errors.push({
Expand Down Expand Up @@ -684,6 +715,7 @@ function mergeTools(
]),
],
contextLimit: override.settings?.contextLimit ?? base.settings.contextLimit,
modelLimits: override.settings?.modelLimits ?? base.settings.modelLimits,
},
distill: {
permission: override.distill?.permission ?? base.distill.permission,
Expand Down Expand Up @@ -724,6 +756,7 @@ function deepCloneConfig(config: PluginConfig): PluginConfig {
settings: {
...config.tools.settings,
protectedTools: [...config.tools.settings.protectedTools],
modelLimits: { ...config.tools.settings.modelLimits },
},
distill: { ...config.tools.distill },
compress: { ...config.tools.compress },
Expand Down
43 changes: 35 additions & 8 deletions lib/messages/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ function parsePercentageString(value: string, total: number): number | undefined
return Math.round((clampedPercent / 100) * total)
}

export const findModelLimit = (
Comment thread
Tarquinen marked this conversation as resolved.
Outdated
modelId: string,
modelLimits: Record<string, number | `${number}%`>,
): number | `${number}%` | undefined => {
return modelLimits[modelId]
}

// XML wrappers
export const wrapPrunableTools = (content: string): string => {
return `<prunable-tools>
Expand Down Expand Up @@ -66,21 +73,41 @@ Context management was just performed. Do NOT use the ${toolName} again. A fresh
</context-info>`
}

const resolveContextLimit = (config: PluginConfig, state: SessionState): number | undefined => {
const configLimit = config.tools.settings.contextLimit
const resolveContextLimit = (
config: PluginConfig,
state: SessionState,
messages: WithParts[],
): number | undefined => {
const { settings } = config.tools
const { modelLimits, contextLimit } = settings
Comment thread
Tarquinen marked this conversation as resolved.
Outdated

if (modelLimits) {
const userMsg = getLastUserMessage(messages)
Comment thread
Tarquinen marked this conversation as resolved.
Outdated
const modelId = userMsg ? (userMsg.info as UserMessage).model.modelID : undefined
Comment thread
Tarquinen marked this conversation as resolved.
Outdated
const limit = modelId !== undefined ? findModelLimit(modelId, modelLimits) : undefined

if (limit !== undefined) {
if (typeof limit === "string" && limit.endsWith("%")) {
if (state.modelContextLimit === undefined) {
return undefined
}
return parsePercentageString(limit, state.modelContextLimit)
}
return typeof limit === "number" ? limit : undefined
}
}

if (typeof configLimit === "string") {
if (configLimit.endsWith("%")) {
if (typeof contextLimit === "string") {
if (contextLimit.endsWith("%")) {
if (state.modelContextLimit === undefined) {
return undefined
}
return parsePercentageString(configLimit, state.modelContextLimit)
return parsePercentageString(contextLimit, state.modelContextLimit)
}

return undefined
}

return configLimit
return contextLimit
}

const shouldInjectCompressNudge = (
Expand All @@ -92,7 +119,7 @@ const shouldInjectCompressNudge = (
return false
}

const contextLimit = resolveContextLimit(config, state)
const contextLimit = resolveContextLimit(config, state, messages)
if (contextLimit === undefined) {
return false
}
Expand Down