| 1 | import type { OpenCodeClient } from "@opencode/client/effect"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Option from "effect/Option"; |
| 4 | |
| 5 | import * as OpenCodeRuntime from "./OpenCodeRuntime.ts"; |
| 6 | import 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 | */ |
| 14 | const 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 | */ |
| 33 | export 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 | }; |