fix(acp): OMP models other than the running one keep a remembered effort in new chats (#33646)

OMP native chats keep a remembered or explicit effort on models other than OMP's current one: for dialects that clamp unlisted efforts themselves (OMP only, via clampsUnlistedEffort), non-current models' efforts are recorded as unknown instead of "admits none"; catalog merges keep an older explicit empty list. OpenCode keeps main's behaviour.

Fixes #31995
Supersedes #31998

Co-authored-by: Pandu dwi Putra <218730385+pandudpn@users.noreply.github.com>
This commit is contained in:
Neil
2026-10-25 15:31:43 -07:00
committed by GitHub
co-authored by Pandu dwi Putra
parent c6769fc9be
commit 7e8bf3db47
6 changed files with 61 additions and 5 deletions
+2
View File
@@ -100,6 +100,8 @@ export type AcpDialect = {
notification?(method: string, params: unknown, at: number): AcpDialectNotification | undefined
contextWindow?(models: unknown): number | undefined
modelEfforts?(model: ModelInfo): { efforts: AgentSessionOptionChoice[]; defaultEffort?: string }
/** The agent clamps an effort another model lacks, so unlisted models' menus are unknown, not empty. */
clampsUnlistedEffort?: true
/** Claimed permission requests are never auto-approved. */
request?(method: string, params: unknown): AcpRequestPresentation | undefined
settleRequest?(method: string, params: unknown): AcpRequestSettlement | undefined
+2
View File
@@ -113,6 +113,8 @@ function errorDetails(error: AcpAgentError): string | undefined {
}
export const OMP_ACP_DIALECT: AcpDialect = {
// OMP resolves any thinking level against the selected model (model-controls.ts setThinkingLevel).
clampsUnlistedEffort: true,
normalizeToolUpdate,
compactionReply,
authenticationRequired: (error) => {
+16 -1
View File
@@ -2,6 +2,8 @@ import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import { AcpStructuredOptions } from './acp-structured-options'
import { GROK_ACP_DIALECT } from './acp-dialects/grok-dialect'
import { OMP_ACP_DIALECT } from './acp-dialects/omp-dialect'
import { OPENCODE_ACP_DIALECT } from './acp-dialects/opencode-dialect'
const configOptions = [
{
@@ -30,7 +32,8 @@ const configOptions = [
describe('ACP session options', () => {
it('keeps the session’s picks out of the catalog facts', () => {
const options = new AcpStructuredOptions()
// OpenCode refuses an effort the selected model lacks, so another model's empty menu stays strict.
const options = new AcpStructuredOptions(OPENCODE_ACP_DIALECT)
options.adoptSession({ configOptions })
const { models, current } = options.read()
// Current values supply an effort menu, never a catalog default.
@@ -49,6 +52,18 @@ describe('ACP session options', () => {
expect(current).toEqual({ model: 'model-b', effort: 'high', confirmed: ['model', 'effort'] })
})
it('leaves another model’s menu unknown for an agent that clamps unlisted efforts', () => {
const options = new AcpStructuredOptions(OMP_ACP_DIALECT)
options.adoptSession({ configOptions })
expect(options.read().models[0]).toEqual({
id: 'model-a',
label: 'Model A',
isDefault: false,
efforts: [],
effortsKnown: false
})
})
it('reads each model’s own menu where the agent advertises one', () => {
const options = new AcpStructuredOptions(GROK_ACP_DIALECT)
options.adoptSession({
+11 -2
View File
@@ -51,7 +51,10 @@ function isSelect(option: SessionConfigOption): option is SelectOption {
export class AcpStructuredOptions {
constructor(
private readonly dialect: Pick<AcpDialect, 'modelEfforts' | 'withheldCommands'> = {}
private readonly dialect: Pick<
AcpDialect,
'modelEfforts' | 'clampsUnlistedEffort' | 'withheldCommands'
> = {}
) {}
private configOptions: SessionConfigOption[] = []
@@ -158,7 +161,13 @@ export class AcpStructuredOptions {
const models: AgentSessionModelOption[] = listed.map((model) => {
const info = this.models?.availableModels.find((entry) => entry.modelId === model.id)
const advertised = info ? this.dialect.modelEfforts?.(info) : undefined
const sessionMenu = { efforts: model.id === currentModel ? sessionEfforts : [] }
// A clamping agent's session menu says nothing about another model's menu: unknown, not none.
const sessionMenu =
model.id === currentModel
? { efforts: sessionEfforts }
: this.dialect.clampsUnlistedEffort && effortOption && !advertised
? { efforts: [], effortsKnown: false as const }
: { efforts: [] }
return {
...model,
isDefault: false,
@@ -39,3 +39,26 @@ describe('the effort menu of a model whose configured effort one listing lacks',
expect(merged).not.toHaveProperty('defaultEffort')
})
})
const unknownChat = (at: number): AgentModelCatalogListing => ({
...listing('live-session', at, []),
models: [
{ id: 'openai/gpt-6', label: 'GPT-6', isDefault: false, efforts: [], effortsKnown: false }
]
})
describe('a model whose newer chat listing does not know its efforts', () => {
it('takes the menu the older listing supplies', () => {
const merged = model(listing('probe', 1, menu('off', 'high')), unknownChat(2))
expect(merged?.efforts).toEqual(menu('off', 'high'))
expect(merged).not.toHaveProperty('effortsKnown')
})
it('keeps an older listing’s explicit empty menu', () => {
expect(model(listing('probe', 1, []), unknownChat(2))).not.toHaveProperty('effortsKnown')
})
it('stays unknown when no listing knows its menu', () => {
expect(model(unknownChat(1), unknownChat(2))).toMatchObject({ effortsKnown: false })
})
})
@@ -139,7 +139,8 @@ function mergedModels(
return (newer?.models ?? []).map((model) => {
const listed = discovered?.models.find((entry) => entry.id === model.id)
const reported = live?.models.find((entry) => entry.id === model.id)
const olderEfforts = older?.models.find((entry) => entry.id === model.id)?.efforts ?? []
const olderModel = older?.models.find((entry) => entry.id === model.id)
const olderEfforts = olderModel?.efforts ?? []
const runsConfigured =
configured?.modelId === model.id || configured?.sameModelIds?.includes(model.id) === true
const configuredEffort = runsConfigured ? configured?.effort : undefined
@@ -152,9 +153,13 @@ function mergedModels(
const defaultEffort = [configuredEffort, listed?.defaultEffort, reported?.defaultEffort].find(
(effort) => effort !== undefined && efforts.some((choice) => choice.value === effort)
)
const { defaultEffort: _own, ...rest } = model
const { defaultEffort: _own, effortsKnown, ...rest } = model
return {
...rest,
// Efforts stay unknown only when neither listing knows this model's menu.
...(effortsKnown === false && (!olderModel || olderModel.effortsKnown === false)
? { effortsKnown }
: {}),
// Without discovery/configuration, live flags are the only available default evidence.
isDefault: configured
? model.id === configured.modelId