1import type { OpenCodeClient } from "@opencode/client/effect";
2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
4
5import * as OpenCodeRuntime from "./OpenCodeRuntime.ts";
6import type { OpenCode2Model } from "./status.ts";
7
8/**
9 * How long a provider read may take before the models are listed without it.
10 * A server loads its provider list with the rest of its catalog, which takes a
11 * couple of seconds on a cold start, and the model loader retries while the
12 * catalog is empty anyway. Past that, the names are not worth waiting for.
13 */
14const PROVIDER_NAME_TIMEOUT = "3 seconds";
15
16/**
17 * Reads the model catalog of an OpenCode 2 server, and names the provider each
18 * model belongs to.
19 *
20 * One instance fronts several providers: OpenCode Zen, OpenCode Go, and
21 * whatever the user added themselves. Their catalogues overlap, so a model's
22 * name alone cannot say which provider listed it and two rows for it read the
23 * same. Reading the server's provider list in the same connection as its
24 * models is what tells them apart.
25 *
26 * A server that cannot name its providers still lists its models, so the
27 * provider read is optional: a model whose provider went unnamed keeps only
28 * its id, which the status check then uses as the label. The models carry the
29 * payload and the names are decoration, so a provider read is bounded as well
30 * as optional: one that never answers must not hold the models back. Both
31 * reads are issued at once, and only the slower of the two is waited on.
32 */
33export const loadOpenCode2Catalog = (
34 client: OpenCodeClient,
35 directory: string,
36): Effect.Effect<ReadonlyArray<OpenCode2Model>, OpenCodeRuntime.OpenCodeRuntimeError> => {
37 const location = { directory };
38 return Effect.all(
39 {
40 models: client.model.list({ location }),
41 providers: client.provider
42 .list({ location })
43 .pipe(Effect.timeout(PROVIDER_NAME_TIMEOUT), Effect.option),
44 },
45 { concurrency: "unbounded" },
46 ).pipe(
47 Effect.map(({ models, providers }) => {
48 const providerNames = new Map(
49 (Option.getOrUndefined(providers)?.data ?? []).map((provider) => [
50 provider.id,
51 provider.name,
52 ]),
53 );
54 return models.data.map((model) => {
55 const providerName = providerNames.get(model.providerID);
56 return {
57 providerID: model.providerID,
58 id: model.id,
59 name: model.name,
60 ...(providerName ? { providerName } : {}),
61 variants: model.variants.map((variant) => ({ id: variant.id })),
62 };
63 });
64 }),
65 Effect.mapError(
66 (cause) =>
67 new OpenCodeRuntime.OpenCodeRuntimeError({
68 operation: "model.list",
69 detail: "The OpenCode server could not list its models.",
70 cause,
71 }),
72 ),
73 );
74};