mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 19:49:48 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dedf933b3e | |||
| ed473870ab | |||
| 60a24a760b |
@@ -1,19 +0,0 @@
|
||||
import type { ProviderPackage } from "../provider-package"
|
||||
import type { OpenAIProviderOptionsInput } from "./openai-options"
|
||||
import { CloudflareWorkersAI } from "./cloudflare"
|
||||
|
||||
export interface Settings extends ProviderPackage.Settings {
|
||||
readonly accountId?: string
|
||||
readonly apiKey?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
|
||||
CloudflareWorkersAI.configure({
|
||||
...(typeof settings.baseURL === "string" ? { baseURL: settings.baseURL } : { accountId: settings.accountId ?? "" }),
|
||||
apiKey: settings.apiKey,
|
||||
headers: settings.headers === undefined ? undefined : { ...settings.headers },
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
limits: settings.limits,
|
||||
providerOptions: settings.providerOptions,
|
||||
}).model(modelID)
|
||||
@@ -1,16 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { model } from "../../src/providers/cloudflare-workers-ai"
|
||||
|
||||
describe("Cloudflare Workers AI provider package", () => {
|
||||
test("derives the endpoint from accountId", () => {
|
||||
const resolved = model("@cf/model", { accountId: "account", apiKey: "secret" })
|
||||
|
||||
expect(resolved.route.endpoint.baseURL).toBe("https://api.cloudflare.com/client/v4/accounts/account/ai/v1")
|
||||
})
|
||||
|
||||
test("preserves an explicit endpoint", () => {
|
||||
const resolved = model("@cf/model", { baseURL: "https://proxy.example/v1", apiKey: "secret" })
|
||||
|
||||
expect(resolved.route.endpoint.baseURL).toBe("https://proxy.example/v1")
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { type Virtualizer } from "@tanstack/solid-virtual"
|
||||
import { Node, Window } from "happy-dom"
|
||||
import { Window } from "happy-dom"
|
||||
import { mutationNodesContainElement, observeElementOffsetReconnectAware } from "./observe-element-offset"
|
||||
|
||||
test("matches only the scroll element or an ancestor containing it", () => {
|
||||
@@ -18,7 +18,6 @@ test("matches only the scroll element or an ancestor containing it", () => {
|
||||
|
||||
test("reports a divergent native offset once and ignores equal offsets and unrelated mutations", async () => {
|
||||
const targetWindow = new Window()
|
||||
const mutations = controlledMutations(targetWindow)
|
||||
const route = targetWindow.document.createElement("section")
|
||||
const viewport = targetWindow.document.createElement("div")
|
||||
const unrelated = targetWindow.document.createElement("div")
|
||||
@@ -41,24 +40,24 @@ test("reports a divergent native offset once and ignores equal offsets and unrel
|
||||
instance.scrollOffset = offset
|
||||
})
|
||||
|
||||
try {
|
||||
mutations.append(targetWindow.document.body, unrelated)
|
||||
mutations.remove(unrelated)
|
||||
expect(calls).toEqual([])
|
||||
targetWindow.document.body.append(unrelated)
|
||||
unrelated.remove()
|
||||
await frames(2, targetWindow)
|
||||
expect(calls).toEqual([])
|
||||
|
||||
mutations.remove(route)
|
||||
mutations.append(targetWindow.document.body, route)
|
||||
await frames(2, targetWindow)
|
||||
expect(calls).toEqual([[0, false]])
|
||||
route.remove()
|
||||
targetWindow.document.body.append(route)
|
||||
await waitFor(() => calls.length === 1, targetWindow)
|
||||
expect(calls).toEqual([[0, false]])
|
||||
|
||||
mutations.remove(route)
|
||||
mutations.append(targetWindow.document.body, route)
|
||||
await frames(2, targetWindow)
|
||||
expect(calls).toEqual([[0, false]])
|
||||
} finally {
|
||||
cleanup?.()
|
||||
await targetWindow.happyDOM.close()
|
||||
}
|
||||
route.remove()
|
||||
targetWindow.document.body.append(route)
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
await frames(3, targetWindow)
|
||||
expect(calls).toEqual([[0, false]])
|
||||
|
||||
cleanup?.()
|
||||
await targetWindow.happyDOM.close()
|
||||
})
|
||||
|
||||
test("keeps checking until stale reset-delay callbacks can no longer win", async () => {
|
||||
@@ -205,33 +204,7 @@ async function frames(count: number, targetWindow: FrameWindow = window) {
|
||||
}
|
||||
}
|
||||
|
||||
function controlledMutations(targetWindow: Window) {
|
||||
let emit: (record: MutationRecord) => void = () => {
|
||||
throw new Error("Mutation observer is not active")
|
||||
}
|
||||
class ControlledMutationObserver {
|
||||
constructor(callback: MutationCallback) {
|
||||
emit = (record) => callback([record], this as unknown as MutationObserver)
|
||||
}
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
takeRecords() {
|
||||
return []
|
||||
}
|
||||
}
|
||||
Object.defineProperty(targetWindow, "MutationObserver", { value: ControlledMutationObserver })
|
||||
const record = (target: Node, addedNodes: Node[], removedNodes: Node[]) =>
|
||||
({ type: "childList", target, addedNodes, removedNodes }) as unknown as MutationRecord
|
||||
return {
|
||||
append(parent: Node, node: Node) {
|
||||
parent.appendChild(node)
|
||||
emit(record(parent, [node], []))
|
||||
},
|
||||
remove(node: Node) {
|
||||
const parent = node.parentNode
|
||||
if (!parent) throw new Error("Mutation target has no parent")
|
||||
parent.removeChild(node)
|
||||
emit(record(parent, [], [node]))
|
||||
},
|
||||
}
|
||||
async function waitFor(condition: () => boolean, targetWindow: FrameWindow = window) {
|
||||
const deadline = targetWindow.performance.now() + 1_000
|
||||
while (!condition() && targetWindow.performance.now() < deadline) await frames(1, targetWindow)
|
||||
}
|
||||
|
||||
@@ -11,9 +11,7 @@ import { createAcpFixture, expectOk, initialize, newSession, selectConfigOption
|
||||
describe("acp lifecycle subprocess", () => {
|
||||
test("stdin EOF exits cleanly", async () => {
|
||||
await using fixture = await createAcpFixture()
|
||||
const acp = fixture.spawn()
|
||||
await initialize(acp)
|
||||
expect(await acp.close()).toBe(0)
|
||||
expect(await fixture.spawn().close()).toBe(0)
|
||||
}, 60_000)
|
||||
|
||||
test("close capability and close request", async () => {
|
||||
|
||||
@@ -8,14 +8,13 @@ import { iife } from "../../util/iife"
|
||||
import { configuredSettings } from "./configured"
|
||||
|
||||
const providerID = Provider.ID.make("cloudflare-workers-ai")
|
||||
const nativePackage = "@opencode-ai/ai/providers/cloudflare-workers-ai"
|
||||
|
||||
export const CloudflareWorkersAIPlugin = define({
|
||||
id: "opencode.provider.cloudflare-workers-ai",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const configured = yield* configuredSettings(providerID)
|
||||
const form = iife(() => {
|
||||
if (hasExplicitEndpoint(configured?.baseURL) || resolveAccountId(configured ?? {})) return
|
||||
if (typeof configured?.baseURL === "string" || resolveAccountId(configured ?? {})) return
|
||||
return Form.Fields.make([
|
||||
{
|
||||
type: "string",
|
||||
@@ -39,24 +38,12 @@ export const CloudflareWorkersAIPlugin = define({
|
||||
yield* ctx.catalog.transform((evt) => {
|
||||
const item = evt.provider.get(providerID)
|
||||
if (!item) return
|
||||
const compatible =
|
||||
Provider.isAISDK(item.provider.package) &&
|
||||
Provider.packageName(item.provider.package) === "@ai-sdk/openai-compatible"
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
if (!compatible) return
|
||||
provider.package = nativePackage
|
||||
provider.settings = nativeSettings(provider.settings)
|
||||
if (!Provider.isAISDK(provider.package)) return
|
||||
if (typeof provider.settings?.baseURL === "string") return
|
||||
const accountId = resolveAccountId(provider.settings ?? {})
|
||||
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
|
||||
})
|
||||
for (const model of item.models.values()) {
|
||||
evt.model.update(item.provider.id, model.id, (draft) => {
|
||||
if (!draft.package && !compatible) return
|
||||
if (draft.package === nativePackage) return
|
||||
if (draft.package && !Provider.isAISDK(draft.package)) return
|
||||
if (draft.package && Provider.packageName(draft.package) !== "@ai-sdk/openai-compatible") return
|
||||
if (draft.package) draft.package = nativePackage
|
||||
draft.settings = nativeSettings(draft.settings)
|
||||
})
|
||||
}
|
||||
})
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
@@ -96,17 +83,6 @@ function workersEndpoint(accountId: string) {
|
||||
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
|
||||
}
|
||||
|
||||
function hasExplicitEndpoint(baseURL: unknown) {
|
||||
return typeof baseURL === "string" && !baseURL.includes("${CLOUDFLARE_ACCOUNT_ID}")
|
||||
}
|
||||
|
||||
function nativeSettings(settings: Record<string, unknown> | undefined) {
|
||||
const result = { ...settings }
|
||||
if (process.env.CLOUDFLARE_ACCOUNT_ID) result.baseURL = workersEndpoint(process.env.CLOUDFLARE_ACCOUNT_ID)
|
||||
else if (!hasExplicitEndpoint(result.baseURL)) delete result.baseURL
|
||||
return result
|
||||
}
|
||||
|
||||
function hasWorkersEndpoint(model: {
|
||||
readonly package?: string
|
||||
readonly settings?: Readonly<Record<string, unknown>>
|
||||
@@ -117,7 +93,7 @@ function hasWorkersEndpoint(model: {
|
||||
function sdkOptions(options: Record<string, any>, app: App.Info) {
|
||||
return {
|
||||
...options,
|
||||
baseURL: expandAccountId(options.baseURL, resolveAccountId(options)),
|
||||
baseURL: expandAccountId(options.baseURL),
|
||||
apiKey: process.env.CLOUDFLARE_API_KEY ?? options.apiKey,
|
||||
headers: {
|
||||
"User-Agent": `${App.useragent(app)} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
@@ -127,9 +103,9 @@ function sdkOptions(options: Record<string, any>, app: App.Info) {
|
||||
}
|
||||
}
|
||||
|
||||
function expandAccountId(baseURL: unknown, accountId: string | undefined) {
|
||||
function expandAccountId(baseURL: unknown) {
|
||||
if (typeof baseURL !== "string") return baseURL
|
||||
return baseURL.replaceAll("${CLOUDFLARE_ACCOUNT_ID}", accountId ?? "${CLOUDFLARE_ACCOUNT_ID}")
|
||||
return baseURL.replaceAll("${CLOUDFLARE_ACCOUNT_ID}", process.env.CLOUDFLARE_ACCOUNT_ID ?? "${CLOUDFLARE_ACCOUNT_ID}")
|
||||
}
|
||||
|
||||
function stringOption(options: Record<string, unknown>, key: string) {
|
||||
|
||||
@@ -465,7 +465,6 @@ Use native v2 fields.`,
|
||||
},
|
||||
}),
|
||||
)
|
||||
yield* Effect.yieldNow
|
||||
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(directory, "reviewer.md"), "Review once"))
|
||||
yield* configTest.emitChange({ type: "create", path: path.join(directory, "reviewer.md") })
|
||||
|
||||
@@ -185,7 +185,6 @@ Review files`,
|
||||
},
|
||||
}),
|
||||
)
|
||||
yield* Effect.yieldNow
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(directory, "review.md"), "Review once"))
|
||||
yield* configTest.emitChange({ type: "create", path: path.join(directory, "review.md") })
|
||||
yield* configTest.emitChange({ type: "update", path: path.join(directory, "review.md") })
|
||||
|
||||
@@ -2,8 +2,6 @@ import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
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 { ModelResolver } from "@opencode-ai/core/model-resolver"
|
||||
import { Model } from "@opencode-ai/core/model"
|
||||
import { Plugin } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
@@ -18,6 +16,7 @@ const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* CloudflareWorkersAIPlugin.effect(host)
|
||||
})
|
||||
@@ -104,13 +103,15 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("maps the environment account ID to the native endpoint", () =>
|
||||
it.effect("maps account ID to endpoint URL and creates an OpenAI-compatible SDK", () =>
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = Provider.aisdk("test-provider")
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
@@ -118,10 +119,21 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
(yield* (yield* Integration.Service).get(Integration.ID.make("cloudflare-workers-ai")))?.methods,
|
||||
).toContainEqual({ type: "key", label: "API key" })
|
||||
const provider = required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai")))
|
||||
const sdk = yield* aisdk.runSDK({
|
||||
model: Model.Info.make({
|
||||
...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")),
|
||||
modelID: Model.ID.make("@cf/model"),
|
||||
package: provider.package,
|
||||
settings: provider.settings,
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
|
||||
})
|
||||
expect(provider).toMatchObject({
|
||||
package: "@opencode-ai/ai/providers/cloudflare-workers-ai",
|
||||
package: "aisdk:test-provider",
|
||||
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1" },
|
||||
})
|
||||
expect(sdk.sdk).toBeDefined()
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -181,72 +193,19 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = Provider.aisdk("test-provider")
|
||||
provider.settings = { ...provider.settings, accountId: "configured-acct" }
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai")))).toMatchObject({
|
||||
package: "@opencode-ai/ai/providers/cloudflare-workers-ai",
|
||||
settings: {
|
||||
accountId: "configured-acct",
|
||||
baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1",
|
||||
},
|
||||
package: "aisdk:test-provider",
|
||||
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1" },
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("passes the connected account ID to the native provider at runtime", () =>
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const providerID = Provider.ID.make("cloudflare-workers-ai")
|
||||
yield* catalog.transform((draft) => {
|
||||
draft.provider.update(providerID, (provider) => {
|
||||
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.settings = {
|
||||
accountId: "configured-acct",
|
||||
baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||
}
|
||||
})
|
||||
draft.model.update(providerID, Model.ID.make("@cf/model"), (model) => {
|
||||
model.settings = {
|
||||
accountId: "model-acct",
|
||||
baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||
}
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
|
||||
const selected = required(yield* catalog.model.get(providerID, Model.ID.make("@cf/model")))
|
||||
const { model } = yield* Effect.promise(() => import("@opencode-ai/ai/providers/cloudflare-workers-ai"))
|
||||
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||
selected,
|
||||
Credential.Key.make({
|
||||
type: "key",
|
||||
key: "secret",
|
||||
configuration: { accountId: "connected-acct" },
|
||||
}),
|
||||
{ loadPackage: () => Effect.succeed({ model }) },
|
||||
)
|
||||
|
||||
expect(required(yield* catalog.provider.get(providerID))).toMatchObject({
|
||||
package: "@opencode-ai/ai/providers/cloudflare-workers-ai",
|
||||
settings: { accountId: "configured-acct" },
|
||||
})
|
||||
expect(selected).toMatchObject({
|
||||
package: "@opencode-ai/ai/providers/cloudflare-workers-ai",
|
||||
settings: { accountId: "model-acct" },
|
||||
})
|
||||
expect(selected.settings).not.toHaveProperty("baseURL")
|
||||
expect(resolved.route.endpoint.baseURL).toBe(
|
||||
"https://api.cloudflare.com/client/v4/accounts/connected-acct/ai/v1",
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("uses env API key over auth or configured API key and keeps the Cloudflare User-Agent", () =>
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -286,30 +286,27 @@ describe("ShellTool", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"permissions compound commands separately",
|
||||
() =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
reset()
|
||||
return withSession(tmp.path, (registry) =>
|
||||
executeTool(registry, call({ command: "printf one && printf two" }, "call-compound")),
|
||||
).pipe(
|
||||
Effect.andThen(
|
||||
Effect.sync(() => {
|
||||
expect(assertions).toHaveLength(1)
|
||||
expect(assertions[0]).toMatchObject({
|
||||
resources: ["printf one", "printf two"],
|
||||
save: ["printf *", "printf *"],
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)),
|
||||
),
|
||||
{ timeout: 15_000 },
|
||||
it.live("permissions compound commands separately", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
reset()
|
||||
return withSession(tmp.path, (registry) =>
|
||||
executeTool(registry, call({ command: "printf one && printf two" }, "call-compound")),
|
||||
).pipe(
|
||||
Effect.andThen(
|
||||
Effect.sync(() => {
|
||||
expect(assertions).toHaveLength(1)
|
||||
expect(assertions[0]).toMatchObject({
|
||||
resources: ["printf one", "printf two"],
|
||||
save: ["printf *", "printf *"],
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)),
|
||||
),
|
||||
)
|
||||
|
||||
it.live(
|
||||
|
||||
Reference in New Issue
Block a user