Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342import { Duration, Effect, Schema, Semaphore, Stream } from "effect"import type { Scope } from "effect"import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"import { define } from "@opencode-ai/plugin/effect/plugin"import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"import { Bus } from "../../bus.js"import { Credential } from "../../credential.js"import { Integration } from "../../integration.js"import { Model } from "../../model.js"import { Provider } from "../../provider.js"import { ConfigProviderV1 } from "../../v1/config/provider.js"import { Money } from "@opencode-ai/schema/money"import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options.js"import { ConfigV1 } from "../../v1/config/config.js"
const defaultServer = "https://opencode.ai/console"const clientID = "opencode-cli"const methodID = Integration.MethodID.make("device")const RemoteResponse = Schema.Struct({ config: ConfigV1.Info })const Device = Schema.Struct({ device_code: Schema.String, user_code: Schema.String, verification_uri_complete: Schema.String, expires_in: Schema.Number, interval: Schema.Number,})const Token = Schema.Struct({ access_token: Schema.String, refresh_token: Schema.String, expires_in: Schema.Number,})const TokenPending = Schema.Struct({ error: Schema.String })const DeviceToken = Schema.Union([Token, TokenPending])const User = Schema.Struct({ id: Schema.String, email: Schema.String })const Org = Schema.Struct({ id: Schema.String, name: Schema.String })
function oauth(http: HttpClient.HttpClient) { return { integrationID: Integration.ID.make("opencode"), method: { id: methodID, type: "oauth", label: "OpenCode Console account", }, authorize: (answer) => Effect.gen(function* () { const server = yield* normalizeServer(answer.server ?? defaultServer) const device = yield* post(http, `${server}/auth/device/code`, { client_id: clientID }, Device) const verification = yield* Effect.try({ try: () => { const url = new URL(device.verification_uri_complete, `${server}/`) if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("expected HTTP(S)") return url }, catch: (cause) => new Error(`Invalid device verification URL: ${cause instanceof Error ? cause.message : String(cause)}`), }) return { mode: "auto" as const, url: verification.href, instructions: `Enter code: ${device.user_code}`, callback: poll(http, server, device.device_code, Duration.seconds(device.interval)), } }), refresh: (credential) => Effect.gen(function* () { const server = typeof credential.metadata?.server === "string" ? credential.metadata.server : defaultServer const token = yield* post( http, `${server}/auth/device/token`, { grant_type: "refresh_token", refresh_token: credential.refresh, client_id: clientID }, Token, ) return { ...credential, access: token.access_token, refresh: token.refresh_token, expires: Date.now() + token.expires_in * 1000, } }), label: (credential) => (typeof credential.metadata?.orgName === "string" ? credential.metadata.orgName : undefined), } satisfies IntegrationOAuthMethodRegistration}
export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope.Scope>({ id: "opencode.provider.opencode", effect: Effect.fn(function* (ctx) { const bus = yield* Bus.Service const http = yield* HttpClient.HttpClient const loading = Semaphore.makeUnsafe(1) let connected = false let providers: typeof ConfigV1.Info.Type.provider | undefined
const load = Effect.fn("OpencodePlugin.load")(function* () { const connection = yield* ctx.integration.connection.active("opencode") const credential = connection ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.orElseSucceed(() => undefined)) : undefined connected = connection !== undefined providers = credential ? yield* fetchProviders(http, credential).pipe( Effect.catch((cause) => Effect.logWarning("failed to load OpenCode provider config", { cause }).pipe(Effect.as(undefined)), ), ) : undefined })
yield* ctx.integration.transform((draft) => { draft.update("opencode", (integration) => { integration.name = "OpenCode" }) draft.method.update(oauth(http)) draft.method.update({ integrationID: "opencode", method: { type: "key", label: "API key (service account)" } }) })
yield* load() yield* ctx.catalog.transform((catalog) => { for (const [providerID, item] of Object.entries(providers ?? {})) { catalog.provider.update(providerID, (provider) => { provider.integrationID = Integration.ID.make("opencode") if (item.name !== undefined) provider.name = item.name provider.package = item.npm ? Provider.aisdk(item.npm) : "" provider.settings = { ...provider.settings, ...withoutCredentials(item.options), ...(item.api ? { baseURL: item.api } : {}), } provider.headers = { ...provider.headers, ...item.options?.headers } })
for (const [modelID, config] of Object.entries(item.models ?? {})) { catalog.model.update(providerID, modelID, (model) => { if (config.family !== undefined) model.family = Model.Family.make(config.family) if (config.name !== undefined) model.name = config.name if (config.id !== undefined) model.modelID = Model.ID.make(config.id) model.compatibility = Model.compatibility(config.interleaved) ?? model.compatibility if (config.provider !== undefined) { model.package = config.provider.npm ? Provider.aisdk(config.provider.npm) : undefined if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api } } if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input] if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output] model.headers = { ...model.headers, ...config.headers } model.settings = { ...model.settings, ...ConfigProviderOptionsV1.model(withoutCredentials(config.options)) } if (config.variants !== undefined) { model.variants ??= [] for (const [id, options] of Object.entries(config.variants)) { const variantID = Model.VariantID.make(id) let existing = model.variants.find((item) => item.id === variantID) if (!existing) { existing = { id: variantID } model.variants.push(existing) } existing.headers = { ...existing.headers, ...options.headers } existing.settings = { ...existing.settings, ...ConfigProviderOptionsV1.model(withoutCredentials(options)), } } } if (config.release_date !== undefined) { const released = Date.parse(config.release_date) model.time.released = Number.isFinite(released) ? released : 0 } if (config.cost !== undefined) { model.cost = remoteCost(config.cost) } model.status = config.status ?? "active" model.enabled = config.status !== "deprecated" if (config.limit !== undefined) model.limit = { ...config.limit } }) } }
const item = catalog.provider.get(Provider.ID.opencode) if (!item) return const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.settings?.apiKey) catalog.provider.update(item.provider.id, (provider) => { if (!hasKey) { provider.activation = "enabled" provider.settings = { ...provider.settings, apiKey: "public" } } }) if (hasKey) return for (const model of item.models.values()) { if (!model.cost.some((cost) => cost.input > 0)) continue catalog.model.update(item.provider.id, model.id, (draft) => { draft.enabled = false }) } })
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) yield* bus.subscribe(Credential.Event.Switched).pipe( Stream.filter((event) => event.data.integrationID === Integration.ID.make("opencode")), Stream.runForEach(refresh), Effect.forkScoped({ startImmediately: true }), ) }),})
function fetchProviders(http: HttpClient.HttpClient, value: Credential.Value) { const metadata = value.metadata const server = typeof metadata?.server === "string" ? metadata.server : defaultServer const orgID = typeof metadata?.orgID === "string" ? metadata.orgID : undefined const token = value.type === "oauth" ? value.access : value.key return http .execute( HttpClientRequest.get(`${server}/api/config`).pipe( HttpClientRequest.acceptJson, HttpClientRequest.bearerToken(token), HttpClientRequest.setHeaders(orgID ? { "x-org-id": orgID } : {}), ), ) .pipe( Effect.flatMap((response) => { if (response.status === 404) return Effect.undefined return HttpClientResponse.filterStatusOk(response).pipe( Effect.flatMap(HttpClientResponse.schemaBodyJson(RemoteResponse)), Effect.map((remote) => remote.config.provider), ) }), )}
function withoutCredentials(body: Readonly<Record<string, unknown>> | undefined) { return Object.fromEntries(Object.entries(body ?? {}).filter(([key]) => key !== "apiKey" && key !== "headers"))}
function normalizeServer(input: unknown) { return Effect.try({ try: () => { if (typeof input !== "string") throw new Error("expected string") const url = new URL(input) if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("expected HTTP(S)") return `${url.origin}${url.pathname.replace(/\/+$/, "")}` }, catch: (cause) => new Error(`Invalid OpenCode server URL: ${cause instanceof Error ? cause.message : String(cause)}`), })}
function remoteCost(input: NonNullable<(typeof ConfigProviderV1.Model.Type)["cost"]>) { const base = { input: Money.USDPerMillionTokens.make(input.input), output: Money.USDPerMillionTokens.make(input.output), cache: { read: Money.USDPerMillionTokens.make(input.cache_read ?? 0), write: Money.USDPerMillionTokens.make(input.cache_write ?? 0), }, } if (!input.context_over_200k) return [base] return [ base, { tier: { type: "context" as const, size: 200_000 }, input: Money.USDPerMillionTokens.make(input.context_over_200k.input), output: Money.USDPerMillionTokens.make(input.context_over_200k.output), cache: { read: Money.USDPerMillionTokens.make(input.context_over_200k.cache_read ?? 0), write: Money.USDPerMillionTokens.make(input.context_over_200k.cache_write ?? 0), }, }, ]}
function poll(http: HttpClient.HttpClient, server: string, deviceCode: string, interval: Duration.Duration) { const loop = (wait: Duration.Duration): Effect.Effect<Credential.OAuth, unknown> => Effect.gen(function* () { yield* Effect.sleep(wait) const result = yield* post( http, `${server}/auth/device/token`, { grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: deviceCode, client_id: clientID, }, DeviceToken, false, ) if ("access_token" in result) return yield* credential(http, server, result) if (result.error === "authorization_pending") return yield* loop(wait) if (result.error === "slow_down") { return yield* loop(Duration.sum(wait, Duration.seconds(5))) } return yield* Effect.fail(new Error(`Device authorization failed: ${result.error}`)) }) return loop(interval)}
function credential(http: HttpClient.HttpClient, server: string, token: typeof Token.Type) { return Effect.gen(function* () { const [user, orgs] = yield* Effect.all( [ get(http, `${server}/api/user`, token.access_token, User), get(http, `${server}/api/orgs`, token.access_token, Schema.Array(Org)), ], { concurrency: 2 }, ) const org = orgs.toSorted((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id))[0] return Credential.OAuth.make({ type: "oauth" as const, methodID, access: token.access_token, refresh: token.refresh_token, expires: Date.now() + token.expires_in * 1000, metadata: { server, accountID: user.id, email: user.email, orgID: org?.id, orgName: org?.name, }, }) })}
function get<S extends Schema.Top>(http: HttpClient.HttpClient, url: string, token: string, schema: S) { return HttpClient.filterStatusOk(http) .execute(HttpClientRequest.get(url).pipe(HttpClientRequest.acceptJson, HttpClientRequest.bearerToken(token))) .pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(schema)))}
function post<S extends Schema.Top>( http: HttpClient.HttpClient, url: string, body: Record<string, string>, schema: S, statusOk = true,) { return HttpClientRequest.post(url).pipe( HttpClientRequest.acceptJson, HttpClientRequest.schemaBodyJson(Schema.Record(Schema.String, Schema.String))(body), Effect.flatMap((request) => http.execute(request)), Effect.flatMap((response) => (statusOk ? HttpClientResponse.filterStatusOk(response) : Effect.succeed(response))), Effect.flatMap(HttpClientResponse.schemaBodyJson(schema)), )}