Skip to content

Commit d1f0946

Browse files
zeroliuclaude
andcommitted
refactor(agent-mode): drop opencode's hardcoded effort vocabulary
`KNOWN_OPENCODE_EFFORTS` and the trailing-segment branch it gated are unreachable on every opencode version Copilot will talk to. opencode reports effort as a sibling `category:"thought_level"` config option and its model ids carry none, which `applySelection` and `prefetchEffortCatalog` already rely on; installs below `OPENCODE_MIN_ACP_VERSION` (1.16.0) resolve to `incompatible`, and both `AgentModelPreloader` and `isBackendInstalled` require `ready`, so an older binary never spawns or reports a catalog. The suffix branch is also never reached from persisted state: `opencodeEnabledModelEntries` builds its base id from the provider mapping, not `wire.decode`, whose only callers are `translateBackendState` over live agent-reported ids. Deleting it also removes the reason the vocabulary existed — with no trailing segment peeled off, an umbrella id like `openrouter/anthropic/claude-3.5-haiku` can no longer be mis-split, so nothing needs to gate against it. `encode` drops effort to match, as Claude's codec already does. Net -68 lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent bf9d198 commit d1f0946

2 files changed

Lines changed: 39 additions & 107 deletions

File tree

src/agentMode/backends/opencode/descriptor.test.ts

Lines changed: 26 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -24,77 +24,42 @@ describe("OpencodeBackendDescriptor.wire.decode", () => {
2424
});
2525
});
2626

27-
it("parses 3-segment ids as variants when the suffix is a known effort", () => {
28-
expect(decode("anthropic/claude-sonnet-4-5/medium")).toEqual({
29-
selection: { baseModelId: "anthropic/claude-sonnet-4-5", effort: "medium" },
30-
provider: "anthropic",
31-
});
32-
expect(decode("openai/gpt-5/minimal")).toEqual({
33-
selection: { baseModelId: "openai/gpt-5", effort: "minimal" },
34-
provider: "openai",
35-
});
36-
});
37-
38-
it("recognizes opencode's full effort vocabulary (none/minimal/low/medium/high/xhigh/max)", () => {
39-
// Opencode advertises Anthropic models with `/max` and `/xhigh` and
40-
// OpenRouter reasoning models with `/none`. Each must collapse onto
41-
// its bare base.
42-
for (const effort of ["none", "minimal", "low", "medium", "high", "xhigh", "max"]) {
43-
expect(decode(`anthropic/claude-opus-4-7/${effort}`)).toEqual({
44-
selection: { baseModelId: "anthropic/claude-opus-4-7", effort },
45-
provider: "anthropic",
46-
});
47-
}
48-
});
49-
50-
it("returns no-effort representation for 3-segment ids whose suffix isn't a known effort", () => {
51-
// OpenRouter-style 3-segment ids without an effort suffix — the
52-
// trailing segment is part of the model name. The whole id is the
53-
// baseModelId; provider is still attributed from the leading segment.
54-
expect(decode("openrouter/anthropic/claude-sonnet-4-5")).toEqual({
55-
selection: { baseModelId: "openrouter/anthropic/claude-sonnet-4-5", effort: null },
56-
provider: "openrouterai",
57-
});
27+
it("keeps an umbrella provider's id whole, attributing only the leading segment", () => {
28+
// `claude-3.5-haiku` and `gpt-5` are part of the model name, not an effort.
29+
// opencode reports effort as a separate `thought_level` config option, so no
30+
// trailing segment is ever peeled off.
5831
expect(decode("openrouter/anthropic/claude-3.5-haiku")).toEqual({
5932
selection: { baseModelId: "openrouter/anthropic/claude-3.5-haiku", effort: null },
6033
provider: "openrouterai",
6134
});
62-
});
63-
64-
it("parses 4-segment umbrella ids as variants when the last segment is a known effort", () => {
65-
// OpenRouter wraps native ids under `openrouter/`, so its variants
66-
// are 4-segment: `openrouter/<sub>/<model>/<effort>`. Without this
67-
// case the picker would render seven duplicate rows per OpenRouter
68-
// reasoning model.
69-
expect(decode("openrouter/anthropic/claude-sonnet-4.5/high")).toEqual({
70-
selection: { baseModelId: "openrouter/anthropic/claude-sonnet-4.5", effort: "high" },
71-
provider: "openrouterai",
72-
});
73-
expect(decode("openrouter/anthropic/claude-sonnet-4.5/none")).toEqual({
74-
selection: { baseModelId: "openrouter/anthropic/claude-sonnet-4.5", effort: "none" },
35+
expect(decode("openrouter/openai/gpt-5")).toEqual({
36+
selection: { baseModelId: "openrouter/openai/gpt-5", effort: null },
7537
provider: "openrouterai",
7638
});
77-
expect(decode("openrouter/openai/gpt-5/xhigh")).toEqual({
78-
selection: { baseModelId: "openrouter/openai/gpt-5", effort: "xhigh" },
39+
// OpenRouter route variants like `:exacto` live inside the model segment.
40+
expect(decode("openrouter/openai/gpt-oss-120b:exacto")).toEqual({
41+
selection: { baseModelId: "openrouter/openai/gpt-oss-120b:exacto", effort: null },
7942
provider: "openrouterai",
8043
});
81-
// OpenRouter route variants like `:exacto` live inside the model
82-
// segment — the effort suffix still attaches at the trailing slash.
83-
expect(decode("openrouter/openai/gpt-oss-120b:exacto/none")).toEqual({
84-
selection: { baseModelId: "openrouter/openai/gpt-oss-120b:exacto", effort: "none" },
85-
provider: "openrouterai",
44+
});
45+
46+
it("keeps a trailing segment that reads like an effort, since effort never rides the id", () => {
47+
// A model literally named `.../high` would once have been mis-split into a
48+
// base model plus an effort. The vocabulary that made that possible is gone.
49+
expect(decode("anthropic/claude-sonnet-4-5/high")).toEqual({
50+
selection: { baseModelId: "anthropic/claude-sonnet-4-5/high", effort: null },
51+
provider: "anthropic",
8652
});
8753
});
8854

89-
it("returns no-effort representation for unparseable shapes (1 segment or unknown trailing segment)", () => {
90-
// 1-segment ids have no provider segment to attribute.
55+
it("attributes no provider to a single-segment id", () => {
9156
expect(decode("just-a-name")).toEqual({
9257
selection: { baseModelId: "just-a-name", effort: null },
9358
provider: null,
9459
});
95-
// 4+ segment ids whose trailing segment isn't a known effort fall
96-
// through to a no-effort representation. The leading segment still
97-
// attributes a provider when it maps.
60+
});
61+
62+
it("attributes a provider only when the leading segment maps to a Copilot one", () => {
9863
expect(decode("anthropic/foo/bar/baz")).toEqual({
9964
selection: { baseModelId: "anthropic/foo/bar/baz", effort: null },
10065
provider: "anthropic",
@@ -115,27 +80,22 @@ describe("OpencodeBackendDescriptor.wire.encode", () => {
11580
);
11681
});
11782

118-
it("appends the variant when effort is set", () => {
83+
it("drops effort, which opencode carries in its own config option rather than the id", () => {
11984
expect(encode({ baseModelId: "anthropic/claude-sonnet-4-5", effort: "high" })).toBe(
120-
"anthropic/claude-sonnet-4-5/high"
85+
"anthropic/claude-sonnet-4-5"
12186
);
12287
});
12388

12489
it("round-trips via wire.decode", () => {
12590
const ids = [
12691
"anthropic/claude-sonnet-4-5",
127-
"anthropic/claude-sonnet-4-5/low",
128-
"openai/gpt-5/high",
129-
"anthropic/claude-opus-4-7/max",
13092
"openrouter/anthropic/claude-sonnet-4.5",
131-
"openrouter/anthropic/claude-sonnet-4.5/none",
132-
"openrouter/anthropic/claude-sonnet-4.5/high",
93+
"openrouter/openai/gpt-oss-120b:exacto",
13394
// Catalog-less BYOK (openai-compatible) — provider id is the synthetic
13495
// copilot providerId, and the model id may itself contain slashes
13596
// (LM Studio repo-prefixed ids like `lmstudio-community/Qwen-…-GGUF`).
136-
// The trailing segment isn't a known effort, so decode treats the
137-
// whole string as `baseModelId` with `effort: null` — and encode
138-
// reproduces it verbatim.
97+
// decode treats the whole string as `baseModelId`, and encode reproduces
98+
// it verbatim.
13999
"lmstudio-byok-id/lmstudio-community/Qwen2.5-7B-Instruct-GGUF",
140100
"ollama-byok-id/llama3.2",
141101
];

src/agentMode/backends/opencode/descriptor.ts

Lines changed: 13 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -50,46 +50,23 @@ const EMPTY_EFFORT_CATALOG: Record<string, EffortOption[]> = Object.freeze({});
5050
let managerRef: OpencodeBinaryManager | null = null;
5151

5252
/**
53-
* Effort suffixes opencode appends to model ids. Used to disambiguate
54-
* genuine effort variants from ids whose trailing segment is part of
55-
* the model name (e.g. `openrouter/anthropic/claude-3.5-haiku` — the
56-
* last segment `claude-3.5-haiku` is the model, not an effort).
57-
*/
58-
const KNOWN_OPENCODE_EFFORTS = new Set([
59-
"none",
60-
"minimal",
61-
"low",
62-
"medium",
63-
"high",
64-
"xhigh",
65-
"max",
66-
]);
67-
68-
/**
69-
* Wire-format codec for Opencode. Native providers emit
70-
* `<provider>/<model>[/<effort>]` (3 segments with effort); umbrella
71-
* providers like OpenRouter emit `<provider>/<sub>/<model>[/<effort>]`
72-
* (4 segments with effort). The leading segment is always the opencode
73-
* provider id, mapped onto a Copilot `ChatModelProviders` value via
74-
* `OPENCODE_PROVIDER_MAP` for picker section grouping. We classify the
75-
* trailing segment as effort iff it's in the known effort vocabulary —
76-
* that gates out 3-seg umbrella ids whose last segment is part of the
77-
* model name (e.g. `openrouter/anthropic/claude-3.5-haiku`).
53+
* Wire-format codec for Opencode. A model id is `<provider>/<model>` for native
54+
* providers and `<provider>/<sub>/<model>` for umbrella providers like
55+
* OpenRouter; the leading segment is always the opencode provider id, mapped
56+
* onto a Copilot `ChatModelProviders` value via `OPENCODE_PROVIDER_MAP` for
57+
* picker section grouping.
58+
*
59+
* Effort never rides the id: opencode reports it as a sibling
60+
* `category:"thought_level"` config option, scoped to the active model, which
61+
* `applySelection` and `prefetchEffortCatalog` read. So `decode` leaves the id
62+
* whole — no trailing segment is an effort, and none is guessed.
7863
*/
7964
const opencodeWire: ModelWireCodec = {
80-
encode: (selection: ModelSelection) =>
81-
selection.effort ? `${selection.baseModelId}/${selection.effort}` : selection.baseModelId,
65+
encode: (selection: ModelSelection) => selection.baseModelId,
8266
decode: (wireId: string) => {
8367
if (!wireId) return { selection: { baseModelId: wireId, effort: null }, provider: null };
8468
const segments = wireId.split("/");
8569
const provider = segments.length >= 2 ? opencodeProviderToCopilot(segments[0]) : null;
86-
const last = segments[segments.length - 1];
87-
if (segments.length >= 3 && KNOWN_OPENCODE_EFFORTS.has(last)) {
88-
return {
89-
selection: { baseModelId: segments.slice(0, -1).join("/"), effort: last },
90-
provider,
91-
};
92-
}
9370
return { selection: { baseModelId: wireId, effort: null }, provider };
9471
},
9572
};
@@ -207,9 +184,7 @@ export const OpencodeBackendDescriptor: BackendDescriptor = {
207184
? context.backendReportedCurrent?.baseModelId
208185
: session.getState()?.model?.current.baseModelId;
209186
if (currentBase !== selection.baseModelId) {
210-
await session.applyModelWireId(
211-
opencodeWire.encode({ baseModelId: selection.baseModelId, effort: null })
212-
);
187+
await session.applyModelWireId(opencodeWire.encode(selection));
213188
}
214189
if (selection.effort !== null) {
215190
const refreshedApply = session.getState()?.model?.apply;
@@ -243,10 +218,7 @@ export const OpencodeBackendDescriptor: BackendDescriptor = {
243218
// the effort options the refreshed state reports for it.
244219
if (modelState.apply.kind !== "setConfigOption") return EMPTY_EFFORT_CATALOG;
245220
const configId = modelState.apply.configId;
246-
const originalWire = opencodeWire.encode({
247-
baseModelId: modelState.current.baseModelId,
248-
effort: null,
249-
});
221+
const originalWire = opencodeWire.encode(modelState.current);
250222
const out: Record<string, EffortOption[]> = {};
251223
try {
252224
for (const model of enabledModels) {

0 commit comments

Comments
 (0)