feat(core): add opencode integration (#33555)
This commit is contained in:
parent
c17b9557f1
commit
cf80b5c470
26 changed files with 1061 additions and 439 deletions
|
|
@ -332,7 +332,7 @@ describe("Integration", () => {
|
|||
},
|
||||
{ type: "env", name: "INTEGRATION_TEST_ACME_KEY" },
|
||||
])
|
||||
expect(yield* integrations.connection.forIntegration(integrationID)).toEqual({
|
||||
expect(yield* integrations.connection.active(integrationID)).toEqual({
|
||||
type: "credential",
|
||||
id: personal.id,
|
||||
label: "Personal",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { RepositoryCache } from "@opencode-ai/core/repository-cache"
|
|||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { tempLocationLayer } from "../fixture/location"
|
||||
|
||||
export const PluginTestLayer = Layer.mergeAll(FileSystem.locationLayer, PluginV2.locationLayer).pipe(
|
||||
|
|
@ -16,6 +17,7 @@ export const PluginTestLayer = Layer.mergeAll(FileSystem.locationLayer, PluginV2
|
|||
Layer.mergeAll(
|
||||
Credential.defaultLayer,
|
||||
EventV2.defaultLayer,
|
||||
FetchHttpClient.layer,
|
||||
FSUtil.defaultLayer,
|
||||
Global.defaultLayer,
|
||||
Layer.succeed(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
|
@ -31,6 +32,10 @@ export function host(overrides: Overrides = {}): PluginContext {
|
|||
integration: overrides.integration ?? {
|
||||
transform: () => Effect.die("unused integration.transform"),
|
||||
reload: () => Effect.die("unused integration.reload"),
|
||||
connection: {
|
||||
active: () => Effect.die("unused integration.connection.active"),
|
||||
resolve: () => Effect.die("unused integration.connection.resolve"),
|
||||
},
|
||||
},
|
||||
plugin: overrides.plugin ?? {
|
||||
add: () => Effect.die("unused plugin.add"),
|
||||
|
|
@ -138,6 +143,13 @@ export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"
|
|||
export function integrationHost(integration: Integration.Interface): PluginContext["integration"] {
|
||||
return {
|
||||
reload: integration.reload,
|
||||
connection: {
|
||||
active: (id) => integration.connection.active(Integration.ID.make(id)),
|
||||
resolve: (connection) =>
|
||||
integration.connection.resolve(
|
||||
connection.type === "credential" ? { ...connection, id: Credential.ID.make(connection.id) } : connection,
|
||||
),
|
||||
},
|
||||
transform: (callback) =>
|
||||
integration.transform((draft) =>
|
||||
callback({
|
||||
|
|
@ -150,16 +162,75 @@ export function integrationHost(integration: Integration.Interface): PluginConte
|
|||
remove: (id) => draft.remove(Integration.ID.make(id)),
|
||||
method: {
|
||||
list: (id) => draft.method.list(Integration.ID.make(id)).map(method),
|
||||
update: (input) =>
|
||||
input.method.type === "env"
|
||||
? draft.method.update({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
method: { ...input.method, names: [...input.method.names] },
|
||||
})
|
||||
: draft.method.update({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
method: input.method,
|
||||
}),
|
||||
update: (input) => {
|
||||
if ("authorize" in input) {
|
||||
const methodID = Integration.MethodID.make(input.method.id)
|
||||
const refresh = input.refresh
|
||||
draft.method.update({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
method: { ...input.method, id: methodID },
|
||||
authorize: (inputs) =>
|
||||
input.authorize(inputs).pipe(
|
||||
Effect.map((authorization) => {
|
||||
if (authorization.mode === "auto") {
|
||||
return {
|
||||
...authorization,
|
||||
callback: authorization.callback.pipe(
|
||||
Effect.map(
|
||||
(credential) =>
|
||||
new Credential.OAuth({
|
||||
...credential,
|
||||
methodID: Integration.MethodID.make(credential.methodID),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
return {
|
||||
...authorization,
|
||||
callback: (code: string) =>
|
||||
authorization.callback(code).pipe(
|
||||
Effect.map(
|
||||
(credential) =>
|
||||
new Credential.OAuth({
|
||||
...credential,
|
||||
methodID: Integration.MethodID.make(credential.methodID),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}
|
||||
}),
|
||||
),
|
||||
...(refresh
|
||||
? {
|
||||
refresh: (value: Credential.OAuth) =>
|
||||
refresh(value).pipe(
|
||||
Effect.map(
|
||||
(next) =>
|
||||
new Credential.OAuth({
|
||||
...next,
|
||||
methodID: Integration.MethodID.make(next.methodID),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(input.label ? { label: input.label } : {}),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (input.method.type === "env") {
|
||||
draft.method.update({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
method: { ...input.method, names: [...input.method.names] },
|
||||
})
|
||||
return
|
||||
}
|
||||
draft.method.update({
|
||||
integrationID: Integration.ID.make(input.integrationID),
|
||||
method: input.method,
|
||||
})
|
||||
},
|
||||
remove: (id, item) => draft.method.remove(Integration.ID.make(id), internalMethod(item)),
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
|
|
@ -48,6 +49,79 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
|
|||
const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }]
|
||||
|
||||
describe("OpencodePlugin", () => {
|
||||
it.effect("registers OAuth and API key methods", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* addPlugin()
|
||||
expect((yield* (yield* Integration.Service).get(Integration.ID.make("opencode")))?.methods).toEqual([
|
||||
{
|
||||
id: Integration.MethodID.make("device"),
|
||||
type: "oauth",
|
||||
label: "Sign in with OpenCode",
|
||||
prompts: [
|
||||
{
|
||||
type: "text",
|
||||
key: "server",
|
||||
message: "OpenCode server",
|
||||
placeholder: "https://console.opencode.ai",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: "key", label: "API key" },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("loads providers and models from the connected OpenCode server", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const authorization: Array<string | null> = []
|
||||
return {
|
||||
authorization,
|
||||
server: Bun.serve({
|
||||
port: 0,
|
||||
fetch: (request) => {
|
||||
authorization.push(request.headers.get("authorization"))
|
||||
return Response.json({
|
||||
config: {
|
||||
providers: {
|
||||
remote: {
|
||||
name: "Remote",
|
||||
models: {
|
||||
model: { name: "Remote Model" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
}),
|
||||
}
|
||||
}),
|
||||
({ authorization, server }) =>
|
||||
Effect.gen(function* () {
|
||||
const credentials = yield* Credential.Service
|
||||
yield* credentials.create({
|
||||
integrationID: Integration.ID.make("opencode"),
|
||||
value: new Credential.Key({
|
||||
type: "key",
|
||||
key: "secret",
|
||||
metadata: { server: server.url.origin },
|
||||
}),
|
||||
})
|
||||
|
||||
yield* addPlugin()
|
||||
|
||||
const catalog = yield* Catalog.Service
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("remote")))?.name).toBe("Remote")
|
||||
expect((yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model")))?.name).toBe(
|
||||
"Remote Model",
|
||||
)
|
||||
expect(authorization).toContain("Bearer secret")
|
||||
}),
|
||||
({ server }) => Effect.promise(() => server.stop(true)),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("uses a public key and disables paid models without credentials", () =>
|
||||
withEnv({ OPENCODE_API_KEY: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue