Skip to content

Commit 3cf6941

Browse files
committed
fix(provider-opencode): tell OpenCode Zen and Go models apart
One OpenCode instance fronts several providers, and their catalogues overlap: Step 5 Preview Free is listed by OpenCode Zen and by OpenCode Go. The 2.x status check dropped the provider name when it built the snapshot's model list, so both models reach the composer with the instance's own display name and nothing beside it. A user choosing between them cannot tell which subscription a model bills (#17100). The status check now reads the server's provider list alongside its model list, in the same connection, and labels each model with the provider that listed it. Clients already render that label: web, desktop, and mobile show it under the model name, and the picker's search matches it. A provider the server calls plain "OpenCode" keeps no label, since that would repeat the instance name the row already shows. The provider id stands in until the server has named its providers, which it does lazily with the rest of its catalog. A server that cannot name its providers still lists its models, so a failed provider read degrades to those ids instead of failing the check.
1 parent 43f8a8d commit 3cf6941

3 files changed

Lines changed: 124 additions & 11 deletions

File tree

‎packages/provider-opencode/src/server/driver.ts‎

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -295,10 +295,39 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
295295
Effect.provideService(OpenCode2Server.OpenCode2Server, openCode2Server),
296296
),
297297
});
298+
// A model's sub-provider is what tells OpenCode Zen, OpenCode Go, and a
299+
// provider the user added themselves apart: their catalogues overlap, and
300+
// the model name alone is identical. A server that cannot name its
301+
// providers still lists its models, so those rows just keep the instance
302+
// name on their own.
298303
const loadOpenCode2Models = yield* makeOpenCode2ModelLoader(
299-
openCode2Server.withConnection((connection) =>
300-
connection.client.model.list({ location: { directory: host.paths.cwd } }).pipe(
301-
Effect.map((models) => models.data),
304+
openCode2Server.withConnection((connection) => {
305+
const location = { directory: host.paths.cwd };
306+
return Effect.all(
307+
{
308+
models: connection.client.model.list({ location }),
309+
providers: connection.client.provider.list({ location }).pipe(Effect.option),
310+
},
311+
{ concurrency: "unbounded" },
312+
).pipe(
313+
Effect.map(({ models, providers }) => {
314+
const providerNames = new Map(
315+
(Option.getOrUndefined(providers)?.data ?? []).map((provider) => [
316+
provider.id,
317+
provider.name,
318+
]),
319+
);
320+
return models.data.map((model) => {
321+
const providerName = providerNames.get(model.providerID);
322+
return {
323+
providerID: model.providerID,
324+
id: model.id,
325+
name: model.name,
326+
...(providerName ? { providerName } : {}),
327+
variants: model.variants.map((variant) => ({ id: variant.id })),
328+
};
329+
});
330+
}),
302331
Effect.mapError(
303332
(cause) =>
304333
new OpenCodeRuntime.OpenCodeRuntimeError({
@@ -307,8 +336,8 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
307336
cause,
308337
}),
309338
),
310-
),
311-
),
339+
);
340+
}),
312341
);
313342
// A 2.x server lists skills and commands per directory, so one server
314343
// answers every workspace. Its event stream says when a directory it had

‎packages/provider-opencode/src/server/status.test.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,62 @@ it.layer(layerTest)("checkOpenCodeProviderStatus with configured server URL", (i
699699
}),
700700
);
701701

702+
it.effect("names the OpenCode sub-provider each listed model came from", () =>
703+
Effect.gen(function* () {
704+
// Zen and Go both list Step 5 Preview Free, and the model name alone
705+
// cannot tell them apart.
706+
const snapshot = yield* checkProvider(
707+
makeOpenCodeSettings({
708+
serverUrl: "http://127.0.0.1:9999",
709+
serverPassword: "secret-password",
710+
}),
711+
process.cwd(),
712+
undefined,
713+
replayOpenCodeServer(OPENCODE_2_RESPONSES, "secret-password"),
714+
Effect.succeed([
715+
{
716+
providerID: "opencode",
717+
id: "step-5-preview-free",
718+
name: "Step 5 Preview Free",
719+
providerName: "OpenCode Zen",
720+
variants: [],
721+
},
722+
{
723+
providerID: "opencode-go",
724+
id: "step-5-preview-free",
725+
name: "Step 5 Preview Free",
726+
providerName: "OpenCode Go",
727+
variants: [],
728+
},
729+
{
730+
providerID: "openai",
731+
id: "gpt-5.4",
732+
name: "GPT-5.4",
733+
providerName: "OpenAI",
734+
variants: [],
735+
},
736+
{
737+
providerID: "custom",
738+
id: "m1",
739+
name: "Custom 1",
740+
providerName: "OpenCode",
741+
variants: [],
742+
},
743+
{ providerID: "anthropic", id: "claude", name: "Claude", variants: [] },
744+
]),
745+
);
746+
747+
const subProviderFor = (slug: string) =>
748+
snapshot.models.find((model) => model.slug === slug)?.subProvider;
749+
NodeAssert.equal(subProviderFor("opencode/step-5-preview-free"), "OpenCode Zen");
750+
NodeAssert.equal(subProviderFor("opencode-go/step-5-preview-free"), "OpenCode Go");
751+
NodeAssert.equal(subProviderFor("openai/gpt-5.4"), "OpenAI");
752+
NodeAssert.equal(subProviderFor("custom/m1"), undefined);
753+
// The id stands in until the server names its providers.
754+
NodeAssert.equal(subProviderFor("anthropic/claude"), "anthropic");
755+
}),
756+
);
757+
702758
it.effect("reports a rejected OpenCode 2 password as an auth error, not a version", () =>
703759
Effect.gen(function* () {
704760
const snapshot = yield* checkProvider(

‎packages/provider-opencode/src/server/status.ts‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,9 +383,33 @@ export interface OpenCode2Model {
383383
readonly providerID: string;
384384
readonly id: string;
385385
readonly name: string;
386+
/**
387+
* Name of the provider the model belongs to, when the server names it. One
388+
* OpenCode instance fronts several: OpenCode Zen, OpenCode Go, and whatever
389+
* the user added themselves.
390+
*/
391+
readonly providerName?: string;
386392
readonly variants: ReadonlyArray<{ readonly id: string }>;
387393
}
388394

395+
/**
396+
* One OpenCode instance fronts several providers, and their catalogues
397+
* overlap: OpenCode Zen and OpenCode Go both list Step 5 Preview Free. Naming
398+
* the sub-provider is what tells two such rows apart. The label only earns its
399+
* place when it adds to the instance name the row already shows, which is why a
400+
* provider called plain "OpenCode" keeps none, and the id stands in while the
401+
* server has not named its providers yet.
402+
*/
403+
function subProviderLabelFor(input: {
404+
readonly providerID: string;
405+
readonly providerName?: string;
406+
}): string | undefined {
407+
const label = nonEmptyTrimmed(input.providerName) ?? input.providerID;
408+
return label.toLowerCase() === OPENCODE_PRESENTATION.displayName.toLowerCase()
409+
? undefined
410+
: label;
411+
}
412+
389413
/**
390414
* A server loads its catalog lazily and lists nothing for its first few
391415
* hundred milliseconds, and a local instance starts a fresh server for each
@@ -541,12 +565,16 @@ const checkOpenCode2 = Effect.fn("checkOpenCode2")(function* (
541565
}
542566
const models = providerModelsFromSettings(
543567
result.value
544-
.map((model) => ({
545-
slug: `${model.providerID}/${model.id}`,
546-
name: model.name,
547-
isCustom: false,
548-
capabilities: openCode2ModelCapabilities(model),
549-
}))
568+
.map((model) => {
569+
const subProvider = subProviderLabelFor(model);
570+
return {
571+
slug: `${model.providerID}/${model.id}`,
572+
name: model.name,
573+
...(subProvider ? { subProvider } : {}),
574+
isCustom: false,
575+
capabilities: openCode2ModelCapabilities(model),
576+
};
577+
})
550578
.toSorted((left, right) => left.name.localeCompare(right.name)),
551579
settings.customModels,
552580
DEFAULT_OPENCODE_MODEL_CAPABILITIES,

0 commit comments

Comments
 (0)