/
Alex_Ural
/
opencode
Обзор
Документация
Войти
/
Alex_Ural
/
opencode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
packages/core/src/plugin/provider/opencode.ts
311 строк
13 KB
Dax Raad
feat(core): port v2 runtime fixes onto dev
27 июн 2026, 07:07
27 июн 2026, 07:07
93159bc
Код
Авторство
О чём код?
import { Duration, Effect, Schema, Semaphore, Stream } from "effect" import type { Scope } from "effect" import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" import { define } from "@opencode-ai/plugin/v2/effect/plugin" import type { CredentialValue } from "@opencode-ai/sdk/v2/types" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { EventV2 } from "../../event" import { Credential } from "../../credential" import { Integration } from "../../integration" import { ModelV2 } from "../../model" import { ProviderV2 } from "../../provider" import { ConfigProviderV1 } from "../../v1/config/provider" import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options" import { ConfigV1 } from "../../v1/config/config" const defaultServer = "https://console.opencode.ai" 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: () => Effect.gen(function* () { const device = yield* post(http, `${defaultServer}/auth/device/code`, { client_id: clientID }, Device) return { mode: "auto" as const, url: `${defaultServer}${device.verification_uri_complete}`, instructions: `Enter code: ${device.user_code}`, callback: poll(http, defaultServer, 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) => { return typeof credential.metadata?.orgName === "string" ? credential.metadata.orgName : undefined }, } satisfies IntegrationOAuthMethodRegistration } export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | Scope.Scope>({ id: "opencode", effect: Effect.fn(function* (ctx) { const events = yield* EventV2.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.catch(() => Effect.succeed(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)" } }) }) connected = (yield* ctx.integration.connection.active("opencode")) !== undefined 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.api = item.npm ? { type: "aisdk", package: item.npm, url: item.api } : { type: "native", url: item.api, settings: {} } Object.assign(provider.request.headers, item.options?.headers) Object.assign(provider.request.body, withoutCredentials(item.options)) }) for (const [modelID, config] of Object.entries(item.models ?? {})) { catalog.model.update(providerID, modelID, (model) => { if (config.family !== undefined) model.family = config.family if (config.name !== undefined) model.name = config.name if (config.id !== undefined) model.api.id = config.id if (config.provider !== undefined) { model.api = config.provider.npm ? { id: model.api.id, type: "aisdk", package: config.provider.npm, url: config.provider.api, } : { id: model.api.id, type: "native", url: config.provider.api, settings: {} } } 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] const packageName = config.provider?.npm ?? item.npm const lowerer = ConfigProviderOptionsV1.get(packageName) Object.assign(model.request.headers, config.headers) Object.assign(model.request.body, lowerer.request(withoutCredentials(config.options))) if (config.variants !== undefined) { model.variants = Object.entries(config.variants).map(([id, options]) => ({ id: ModelV2.VariantID.make(id), headers: { ...(options.headers ?? {}) }, body: lowerer.request(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(ProviderV2.ID.opencode) if (!item) return const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.request.body.apiKey) catalog.provider.update(item.provider.id, (provider) => { if (!hasKey) provider.request.body.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* events.subscribe(Integration.Event.ConnectionUpdated).pipe( Stream.filter((event) => event.data.integrationID === Integration.ID.make("opencode")), Stream.runForEach(refresh), Effect.forkScoped({ startImmediately: true }), ) yield* refresh().pipe(Effect.forkScoped) }), }) function fetchProviders(http: HttpClient.HttpClient, value: CredentialValue) { 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.succeed(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 remoteCost(input: NonNullable<(typeof ConfigProviderV1.Model.Type)["cost"]>) { const base = { input: input.input, output: input.output, cache: { read: input.cache_read ?? 0, write: input.cache_write ?? 0 }, } if (!input.context_over_200k) return [base] return [ base, { tier: { type: "context" as const, size: 200_000 }, input: input.context_over_200k.input, output: input.context_over_200k.output, cache: { read: input.context_over_200k.cache_read ?? 0, write: 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)), ) }