|
|
@@ -10,58 +10,13 @@ import { Integration } from "../../integration"
|
|
|
import { ModelV2 } from "../../model"
|
|
|
import { ModelRequest } from "../../model-request"
|
|
|
import { ProviderV2 } from "../../provider"
|
|
|
+import { ConfigProviderV1 } from "../../v1/config/provider"
|
|
|
+import { ConfigV1 } from "../../v1/config/config"
|
|
|
|
|
|
const defaultServer = "https://console.opencode.ai"
|
|
|
const clientID = "opencode-cli"
|
|
|
const methodID = Integration.MethodID.make("device")
|
|
|
-const RemoteRequest = Schema.Struct({
|
|
|
- headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
|
|
- body: Schema.Record(Schema.String, Schema.Any).pipe(Schema.optional),
|
|
|
-})
|
|
|
-const RemoteModelApi = Schema.Union([
|
|
|
- Schema.Struct({ id: ModelV2.ID.pipe(Schema.optional), ...ProviderV2.AISDK.fields }),
|
|
|
- Schema.Struct({ id: ModelV2.ID.pipe(Schema.optional), ...ProviderV2.Native.fields }),
|
|
|
- Schema.Struct({ id: ModelV2.ID }),
|
|
|
-])
|
|
|
-const RemoteCost = Schema.Struct({
|
|
|
- tier: Schema.Struct({ type: Schema.Literal("context"), size: Schema.Int }).pipe(Schema.optional),
|
|
|
- input: Schema.Finite,
|
|
|
- output: Schema.Finite,
|
|
|
- cache: Schema.Struct({
|
|
|
- read: Schema.Finite.pipe(Schema.optional),
|
|
|
- write: Schema.Finite.pipe(Schema.optional),
|
|
|
- }).pipe(Schema.optional),
|
|
|
-})
|
|
|
-const RemoteModel = Schema.Struct({
|
|
|
- family: ModelV2.Family.pipe(Schema.optional),
|
|
|
- name: Schema.String.pipe(Schema.optional),
|
|
|
- api: RemoteModelApi.pipe(Schema.optional),
|
|
|
- capabilities: ModelV2.Capabilities.pipe(Schema.optional),
|
|
|
- request: Schema.Struct({ ...RemoteRequest.fields, variant: Schema.String.pipe(Schema.optional) }).pipe(
|
|
|
- Schema.optional,
|
|
|
- ),
|
|
|
- variants: Schema.Struct({
|
|
|
- id: ModelV2.VariantID,
|
|
|
- ...RemoteRequest.fields,
|
|
|
- }).pipe(Schema.Array, Schema.optional),
|
|
|
- cost: Schema.Union([RemoteCost, Schema.Array(RemoteCost)]).pipe(Schema.optional),
|
|
|
- disabled: Schema.Boolean.pipe(Schema.optional),
|
|
|
- limit: Schema.Struct({
|
|
|
- context: Schema.Int.pipe(Schema.optional),
|
|
|
- input: Schema.Int.pipe(Schema.optional),
|
|
|
- output: Schema.Int.pipe(Schema.optional),
|
|
|
- }).pipe(Schema.optional),
|
|
|
-})
|
|
|
-const RemoteProvider = Schema.Struct({
|
|
|
- name: Schema.String.pipe(Schema.optional),
|
|
|
- api: ProviderV2.Api.pipe(Schema.optional),
|
|
|
- request: RemoteRequest.pipe(Schema.optional),
|
|
|
- models: Schema.Record(Schema.String, RemoteModel).pipe(Schema.optional),
|
|
|
-})
|
|
|
-const RemoteConfig = Schema.Struct({
|
|
|
- providers: Schema.Record(Schema.String, RemoteProvider),
|
|
|
-})
|
|
|
-const RemoteResponse = Schema.Struct({ config: RemoteConfig })
|
|
|
+const RemoteResponse = Schema.Struct({ config: ConfigV1.Info })
|
|
|
const Device = Schema.Struct({
|
|
|
device_code: Schema.String,
|
|
|
user_code: Schema.String,
|
|
|
@@ -125,7 +80,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
|
|
const events = yield* EventV2.Service
|
|
|
const http = yield* HttpClient.HttpClient
|
|
|
let connected = false
|
|
|
- let providers: typeof RemoteConfig.Type.providers | undefined
|
|
|
+ let providers: typeof ConfigV1.Info.Type.provider | undefined
|
|
|
|
|
|
const load = Effect.fn("OpencodePlugin.load")(function* () {
|
|
|
const connection = yield* ctx.integration.connection.active("opencode")
|
|
|
@@ -154,59 +109,60 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
|
|
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
|
|
|
- if (item.api !== undefined) provider.api = { ...item.api }
|
|
|
- if (item.request !== undefined) {
|
|
|
- Object.assign(provider.request.headers, item.request.headers)
|
|
|
- Object.assign(provider.request.body, item.request.body)
|
|
|
- }
|
|
|
+ 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))
|
|
|
})
|
|
|
- const providerApi = catalog.provider.get(providerID)?.provider.api
|
|
|
- const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined
|
|
|
+
|
|
|
+ const modelIDs = new Set(Object.keys(item.models ?? {}))
|
|
|
+ for (const model of catalog.provider.get(providerID)?.models.values() ?? []) {
|
|
|
+ if (!modelIDs.has(model.id)) catalog.model.remove(providerID, model.id)
|
|
|
+ }
|
|
|
|
|
|
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.api !== undefined) model.api = { ...model.api, ...config.api }
|
|
|
- const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
|
|
|
- if (config.capabilities !== undefined) {
|
|
|
- model.capabilities = {
|
|
|
- tools: config.capabilities.tools,
|
|
|
- input: [...config.capabilities.input],
|
|
|
- output: [...config.capabilities.output],
|
|
|
- }
|
|
|
- }
|
|
|
- if (config.request !== undefined) {
|
|
|
- ModelRequest.assign(model.request, {
|
|
|
- headers: config.request.headers,
|
|
|
- ...ModelRequest.normalizeAiSdkOptions(packageName, config.request.body ?? {}),
|
|
|
- })
|
|
|
- if (config.request.variant !== undefined) model.request.variant = config.request.variant
|
|
|
+ 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
|
|
|
+ ModelRequest.assign(model.request, {
|
|
|
+ headers: config.headers,
|
|
|
+ ...ModelRequest.normalizeAiSdkOptions(packageName, withoutCredentials(config.options)),
|
|
|
+ })
|
|
|
if (config.variants !== undefined) {
|
|
|
- for (const variant of config.variants) {
|
|
|
- let existing = model.variants.find((item) => item.id === variant.id)
|
|
|
- if (!existing) {
|
|
|
- existing = { id: variant.id, headers: {}, body: {}, generation: {}, options: {} }
|
|
|
- model.variants.push(existing)
|
|
|
- }
|
|
|
- ModelRequest.assign(existing, {
|
|
|
- headers: variant.headers,
|
|
|
- ...ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {}),
|
|
|
- })
|
|
|
- }
|
|
|
+ model.variants = Object.entries(config.variants).map(([id, options]) => ({
|
|
|
+ id: ModelV2.VariantID.make(id),
|
|
|
+ headers: { ...(options.headers ?? {}) },
|
|
|
+ ...ModelRequest.normalizeAiSdkOptions(packageName, 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 = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
|
|
|
- tier: cost.tier && { ...cost.tier },
|
|
|
- input: cost.input,
|
|
|
- output: cost.output,
|
|
|
- cache: { read: cost.cache?.read ?? 0, write: cost.cache?.write ?? 0 },
|
|
|
- }))
|
|
|
+ model.cost = remoteCost(config.cost)
|
|
|
}
|
|
|
- if (config.disabled !== undefined) model.enabled = !config.disabled
|
|
|
- if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
|
|
|
+ model.status = config.status ?? "active"
|
|
|
+ model.enabled = config.status !== "deprecated"
|
|
|
+ if (config.limit !== undefined) model.limit = { ...config.limit }
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
@@ -252,12 +208,37 @@ function fetchProviders(http: HttpClient.HttpClient, value: CredentialValue) {
|
|
|
if (response.status === 404) return Effect.succeed(undefined)
|
|
|
return HttpClientResponse.filterStatusOk(response).pipe(
|
|
|
Effect.flatMap(HttpClientResponse.schemaBodyJson(RemoteResponse)),
|
|
|
- Effect.map((remote) => remote.config.providers),
|
|
|
+ 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* () {
|