mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user