Compare commits

..
Author SHA1 Message Date
Kit Langton 9f290d4381 effect(cli): kill TuiConfig/Installation/SessionCompaction facades
Remove makeRuntime + async facade exports from TuiConfig, Installation, and
SessionCompaction. Migrate production callers to AppRuntime.runPromise with
yielded services, and route tests through dedicated mockable hooks where
they previously spied on the facade.
2026-05-12 16:29:18 -04:00
476 changed files with 73494 additions and 214516 deletions
+2 -10
View File
@@ -33,9 +33,8 @@ runs:
shell: bash
run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
- name: Restore Bun dependencies
id: bun-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
- name: Cache Bun dependencies
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ steps.cache.outputs.dir }}
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
@@ -57,10 +56,3 @@ runs:
bun install ${{ inputs.install-flags }}
fi
shell: bash
- name: Save Bun dependencies
if: steps.bun-cache.outputs.cache-hit != 'true' && github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ steps.cache.outputs.dir }}
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
+2 -2
View File
@@ -244,9 +244,9 @@ jobs:
- host: "blacksmith-4vcpu-ubuntu-2404"
target: x86_64-unknown-linux-gnu
platform_flag: --linux
- host: "blacksmith-4vcpu-ubuntu-2404-arm"
- host: "blacksmith-4vcpu-ubuntu-2404"
target: aarch64-unknown-linux-gnu
platform_flag: --linux --arm64
platform_flag: --linux
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
-23
View File
@@ -73,29 +73,6 @@ function foo() {
}
```
### Complex Logic
When a function has several validation branches or supporting details, make the main function read as the happy path and move supporting details into small helpers below it.
```ts
// Good
export function loadThing(input: unknown) {
const config = requireConfig(input)
const metadata = readMetadata(input)
return createThing({ config, metadata })
}
function requireConfig(input: unknown) {
...
}
```
- Keep helpers close to the code they support, below the main export when that improves readability.
- Do not over-abstract simple expressions into many single-use helpers; extract only when it names a real concept like `requireConfig` or `readMetadata`.
- Do not return `Effect` from helpers unless they actually perform effectful work. Synchronous parsing, validation, and option building should stay synchronous.
- Prefer Effect schema helpers such as `Schema.UnknownFromJsonString` and `Schema.decodeUnknownOption` over manual `JSON.parse` wrapped in `Effect.try` when parsing untrusted JSON strings.
- Add comments for non-obvious constraints and surprising behavior, not for obvious assignments or control flow.
### Schema Definitions (Drizzle)
Use snake_case for field names so column names don't need to be redefined as strings.
+387 -80
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -2,7 +2,6 @@
exact = true
# Only install newly resolved package versions published at least 3 days ago.
minimumReleaseAge = 259200
minimumReleaseAgeExcludes = ["@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-x64", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid"]
[test]
root = "./do-not-run-tests-from-root"
-10
View File
@@ -293,13 +293,3 @@ new sst.cloudflare.x.SolidStart("Console", {
},
},
})
////////////////
// HELPERS
////////////////
export const stat = new sst.cloudflare.Worker("Stat", {
handler: "packages/console/function/src/stat.ts",
link: [database],
url: true,
})
-66
View File
@@ -111,34 +111,6 @@ const providerHttpErrorsQuery = (product: "go" | "zen") => {
}).json
}
const modelLowTpsQuery = (product: "go" | "zen") => {
const filters = [
{ column: "model", op: "exists" },
{ column: "event_type", op: "=", value: "completions" },
{ column: "user_agent", op: "contains", value: "opencode" },
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
{ column: "status", op: ">=", value: "200" },
{ column: "status", op: "<", value: "400" },
{ column: "tps.output", op: "exists" },
]
return honeycomb.getQuerySpecificationOutput({
breakdowns: ["model"],
calculations: [
{ op: "COUNT", name: "TOTAL", filterCombination: "AND", filters },
{
op: "P50",
name: "TPS",
column: "tps.output",
filterCombination: "AND",
filters,
},
],
formulas: [{ name: "LOW_TPS", expression: "IF(GTE($TOTAL, 100), $TPS, 999)" }],
timeRange: 1800,
}).json
}
new honeycomb.Trigger("IncreasedModelHttpErrorsGo", {
name: "Increased Model HTTP Errors [Go]",
description,
@@ -177,44 +149,6 @@ new honeycomb.Trigger("IncreasedModelHttpErrorsZen", {
],
})
new honeycomb.Trigger("LowModelTpsGo", {
name: "Low Model TPS [Go]",
description,
queryJson: modelLowTpsQuery("go"),
alertType: "on_change",
frequency: 600,
thresholds: [{ op: "<=", value: 10, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "model_low_tps" }],
},
],
},
],
})
new honeycomb.Trigger("LowModelTpsZen", {
name: "Low Model TPS [Zen]",
description,
queryJson: modelLowTpsQuery("zen"),
alertType: "on_change",
frequency: 600,
thresholds: [{ op: "<=", value: 10, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "model_low_tps" }],
},
],
},
],
})
new honeycomb.Trigger("IncreasedProviderHttpErrorsGo", {
name: "Increased Provider HTTP Errors [Go]",
description,
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-QIj9PhOXR/GngV/dPjCF7n5rKro2fcTNGzJ47a41Z2Q=",
"aarch64-linux": "sha256-fQl7BjjTYtRKT3HRVhubaIVww/puUFSTzVV5bTy8II8=",
"aarch64-darwin": "sha256-81IAmdjiYZz8IgMJt0+VxzdOS80gTHc5SendwEW/vD4=",
"x86_64-darwin": "sha256-5OMX4VVBMfEmkYvzd09oksAt5hKkxDs84miO804LBI8="
"x86_64-linux": "sha256-MUHog06sZEi6bXR1m8exdkjSNW9bHEv9bPQXACJ7SFw=",
"aarch64-linux": "sha256-3dwdZ3It++OsdGT8xMOQ10Arz8eeODp/LXOrI4DLEhY=",
"aarch64-darwin": "sha256-TmUPGDCewjsrT13npVH6B55J43NKKut67p/HgPJpQNM=",
"x86_64-darwin": "sha256-j8I7t3MZoUQUMFRWyaFO75TRbAw5TauSZAa4yKOHFMA="
}
}
+3 -6
View File
@@ -35,9 +35,9 @@
"@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
"@opentui/core": "0.2.9",
"@opentui/keymap": "0.2.9",
"@opentui/solid": "0.2.9",
"@opentui/core": "0.2.6",
"@opentui/keymap": "0.2.6",
"@opentui/solid": "0.2.6",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
"@types/luxon": "3.7.1",
@@ -128,9 +128,6 @@
"electron"
],
"overrides": {
"@opentui/core": "catalog:",
"@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.14.49",
"version": "1.14.48",
"description": "",
"type": "module",
"exports": {
+7 -1
View File
@@ -240,7 +240,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return paths
})
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const working = createMemo(() => sync.data.session_working(params.id ?? ""))
const status = createMemo(
() =>
sync.data.session_status[params.id ?? ""] ?? {
type: "idle",
},
)
const working = createMemo(() => status()?.type !== "idle")
const imageAttachments = createMemo(() =>
prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"),
)
@@ -208,10 +208,6 @@ export function createChildStoreManager(input: {
session: [],
sessionTotal: 0,
session_status: {},
session_working(id: string) {
const type = this.session_status[id]?.type
return (type ?? "idle") !== "idle"
},
session_diff: {},
todo: {},
permission: {},
@@ -46,7 +46,6 @@ export type State = {
session_status: {
[sessionID: string]: SessionStatus
}
session_working(id: string): boolean
session_diff: {
[sessionID: string]: SnapshotFileDiff[]
}
@@ -166,7 +166,18 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
})
const isWorking = createMemo(() => {
if (hasPermissions()) return false
return sessionStore.session_working(props.session.id)
const pending = (sessionStore.message[props.session.id] ?? []).findLast(
(message) =>
message.role === "assistant" &&
typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number",
)
const status = sessionStore.session_status[props.session.id]
return (
pending !== undefined ||
status?.type === "busy" ||
status?.type === "retry" ||
(status !== undefined && status.type !== "idle")
)
})
const tint = createMemo(() => messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent))
@@ -305,7 +305,7 @@ export const SortableProject = (props: {
const isWorking = createMemo(() =>
dirs().some((directory) => {
const [store] = globalSync.child(directory, { bootstrap: false })
return Object.keys(store.session_status).some((id) => store.session_working(id))
return Object.values(store.session_status).some((status) => status?.type === "busy" || status?.type === "retry")
}),
)
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
+6 -1
View File
@@ -1496,7 +1496,12 @@ export default function Page() {
return out
})
const busy = (sessionID: string) => sync.data.session_working(sessionID)
const busy = (sessionID: string) => {
if ((sync.data.session_status[sessionID] ?? { type: "idle" as const }).type !== "idle") return true
return (sync.data.message[sessionID] ?? []).some(
(item) => item.role === "assistant" && typeof item.time.completed !== "number",
)
}
const queuedFollowups = createMemo(() => {
const id = params.id
@@ -57,7 +57,14 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
() => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
)
const live = createMemo(() => sync.data.session_working(params.id ?? "") || blocked())
const status = createMemo(() => {
const id = params.id
if (!id) return idle
return sync.data.session_status[id] ?? idle
})
const busy = createMemo(() => status().type !== "idle")
const live = createMemo(() => busy() || blocked())
const [store, setStore] = createStore({
responding: undefined as string | undefined,
@@ -32,7 +32,7 @@ export interface SessionReviewTabProps {
focusedComment?: { file: string; id: string } | null
onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void
focusedFile?: string
onScrollRef?: (el: HTMLDivElement | undefined) => void
onScrollRef?: (el: HTMLDivElement) => void
commentMentions?: {
items: (query: string) => string[] | Promise<string[]>
}
@@ -126,7 +126,6 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
onCleanup(() => {
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
props.onScrollRef?.(undefined)
})
return (
@@ -221,241 +221,239 @@ export function SessionSidePanel(props: {
}}
style={{ width: panelWidth() }}
>
<Show when={open()}>
<div class="size-full flex border-l border-border-weaker-base">
<div
aria-hidden={!reviewOpen()}
inert={!reviewOpen()}
class="relative min-w-0 h-full flex-1 overflow-hidden bg-background-base"
classList={{
"pointer-events-none": !reviewOpen(),
}}
>
<div class="size-full min-w-0 h-full bg-background-base">
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={activeTab()} onChange={openTab}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List
ref={(el: HTMLDivElement) => {
const stop = createFileTabListSync({ el, contextOpen })
onCleanup(stop)
}}
>
<Show when={reviewTab() && props.canReview()}>
<Tabs.Trigger value="review">
<div class="flex items-center gap-1.5">
<div>{language.t("session.tab.review")}</div>
<Show when={props.hasReview()}>
<div>{props.reviewCount()}</div>
</Show>
</div>
</Tabs.Trigger>
</Show>
<Show when={contextOpen()}>
<Tabs.Trigger
value="context"
closeButton={
<TooltipKeybind
title={language.t("common.closeTab")}
keybind={command.keybind("tab.close")}
placement="bottom"
gutter={10}
>
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={() => tabs().close("context")}
aria-label={language.t("common.closeTab")}
/>
</TooltipKeybind>
}
hideCloseButton
onMiddleClick={() => tabs().close("context")}
>
<div class="flex items-center gap-2">
<SessionContextUsage variant="indicator" />
<div>{language.t("session.tab.context")}</div>
</div>
</Tabs.Trigger>
</Show>
<SortableProvider ids={openedTabs()}>
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
</SortableProvider>
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
<TooltipKeybind
title={language.t("command.file.open")}
keybind={command.keybind("file.open")}
class="flex items-center"
>
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
class="!rounded-md"
onClick={() => {
void import("@/components/dialog-select-file").then((x) => {
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
})
}}
aria-label={language.t("command.file.open")}
/>
</TooltipKeybind>
</div>
</Tabs.List>
</div>
<Show when={reviewTab() && props.canReview()}>
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={reviewOpen() && activeTab() === "review"}>{props.reviewPanel()}</Show>
</Tabs.Content>
</Show>
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "empty"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-6">
<Mark class="w-14 opacity-10" />
<div class="text-14-regular text-text-weak max-w-56">
{language.t("session.files.selectToOpen")}
</div>
<div class="size-full flex border-l border-border-weaker-base">
<div
aria-hidden={!reviewOpen()}
inert={!reviewOpen()}
class="relative min-w-0 h-full flex-1 overflow-hidden bg-background-base"
classList={{
"pointer-events-none": !reviewOpen(),
}}
>
<div class="size-full min-w-0 h-full bg-background-base">
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={activeTab()} onChange={openTab}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List
ref={(el: HTMLDivElement) => {
const stop = createFileTabListSync({ el, contextOpen })
onCleanup(stop)
}}
>
<Show when={reviewTab() && props.canReview()}>
<Tabs.Trigger value="review">
<div class="flex items-center gap-1.5">
<div>{language.t("session.tab.review")}</div>
<Show when={props.hasReview()}>
<div>{props.reviewCount()}</div>
</Show>
</div>
</Tabs.Trigger>
</Show>
<Show when={contextOpen()}>
<Tabs.Trigger
value="context"
closeButton={
<TooltipKeybind
title={language.t("common.closeTab")}
keybind={command.keybind("tab.close")}
placement="bottom"
gutter={10}
>
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={() => tabs().close("context")}
aria-label={language.t("common.closeTab")}
/>
</TooltipKeybind>
}
hideCloseButton
onMiddleClick={() => tabs().close("context")}
>
<div class="flex items-center gap-2">
<SessionContextUsage variant="indicator" />
<div>{language.t("session.tab.context")}</div>
</div>
</Tabs.Trigger>
</Show>
<SortableProvider ids={openedTabs()}>
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
</SortableProvider>
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
<TooltipKeybind
title={language.t("command.file.open")}
keybind={command.keybind("file.open")}
class="flex items-center"
>
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
class="!rounded-md"
onClick={() => {
void import("@/components/dialog-select-file").then((x) => {
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
})
}}
aria-label={language.t("command.file.open")}
/>
</TooltipKeybind>
</div>
</Tabs.List>
</div>
<Show when={reviewTab() && props.canReview()}>
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
</Tabs.Content>
</Show>
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "empty"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-6">
<Mark class="w-14 opacity-10" />
<div class="text-14-regular text-text-weak max-w-56">
{language.t("session.files.selectToOpen")}
</div>
</div>
</div>
</Show>
</Tabs.Content>
<Show when={contextOpen()}>
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "context"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<SessionContextTab />
</div>
</Show>
</Tabs.Content>
</Show>
<Show when={contextOpen()}>
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "context"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<SessionContextTab />
</div>
</Show>
</Tabs.Content>
</Show>
<Show when={activeFileTab()} keyed>
{(tab) => <FileTabContent tab={tab} />}
</Show>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable} keyed>
{(tab) => {
const path = file.pathFromTab(tab)
return (
<div data-component="tabs-drag-preview">
<Show when={path}>{(p) => <FileVisual active path={p()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
</div>
<Show when={activeFileTab()} keyed>
{(tab) => <FileTabContent tab={tab} />}
</Show>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable} keyed>
{(tab) => {
const path = file.pathFromTab(tab)
return (
<div data-component="tabs-drag-preview">
<Show when={path}>{(p) => <FileVisual active path={p()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
</div>
</div>
<Show when={shown()}>
<Show when={shown()}>
<div
id="file-tree-panel"
aria-hidden={!fileOpen()}
inert={!fileOpen()}
class="relative min-w-0 h-full shrink-0 overflow-hidden"
classList={{
"pointer-events-none": !fileOpen(),
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!props.size.active(),
}}
style={{ width: treeWidth() }}
>
<div
id="file-tree-panel"
aria-hidden={!fileOpen()}
inert={!fileOpen()}
class="relative min-w-0 h-full shrink-0 overflow-hidden"
classList={{
"pointer-events-none": !fileOpen(),
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!props.size.active(),
}}
style={{ width: treeWidth() }}
class="h-full flex flex-col overflow-hidden group/filetree"
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
>
<div
class="h-full flex flex-col overflow-hidden group/filetree"
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
<Tabs
variant="pill"
value={fileTreeTab()}
onChange={setFileTreeTabValue}
class="h-full"
data-scope="filetree"
>
<Tabs
variant="pill"
value={fileTreeTab()}
onChange={setFileTreeTabValue}
class="h-full"
data-scope="filetree"
>
<Tabs.List>
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
{props.reviewCount()}{" "}
{language.t(
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
)}
</Tabs.Trigger>
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
{language.t("session.files.all")}
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
<Switch>
<Match when={props.hasReview() || !props.diffsReady()}>
<Show
when={props.diffsReady()}
fallback={
<div class="px-2 py-2 text-12-regular text-text-weak">
{language.t("common.loading")}
{language.t("common.loading.ellipsis")}
</div>
}
>
<FileTree
path=""
class="pt-3"
allowed={diffFiles()}
kinds={kinds()}
draggable={false}
active={props.activeDiff}
onFileClick={(node) => props.focusReviewDiff(node.path)}
/>
</Show>
</Match>
</Switch>
</Tabs.Content>
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
<Switch>
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
<Match when={true}>
<Tabs.List>
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
{props.reviewCount()}{" "}
{language.t(
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
)}
</Tabs.Trigger>
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
{language.t("session.files.all")}
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
<Switch>
<Match when={props.hasReview() || !props.diffsReady()}>
<Show
when={props.diffsReady()}
fallback={
<div class="px-2 py-2 text-12-regular text-text-weak">
{language.t("common.loading")}
{language.t("common.loading.ellipsis")}
</div>
}
>
<FileTree
path=""
class="pt-3"
modified={diffFiles()}
allowed={diffFiles()}
kinds={kinds()}
onFileClick={(node) => openTab(file.tab(node.path))}
draggable={false}
active={props.activeDiff}
onFileClick={(node) => props.focusReviewDiff(node.path)}
/>
</Match>
</Switch>
</Tabs.Content>
</Tabs>
</div>
<Show when={fileOpen()}>
<div onPointerDown={() => props.size.start()}>
<ResizeHandle
direction="horizontal"
edge="start"
size={layout.fileTree.width()}
min={200}
max={480}
onResize={(width) => {
props.size.touch()
layout.fileTree.resize(width)
}}
/>
</div>
</Show>
</Show>
</Match>
</Switch>
</Tabs.Content>
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
<Switch>
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
<Match when={true}>
<FileTree
path=""
class="pt-3"
modified={diffFiles()}
kinds={kinds()}
onFileClick={(node) => openTab(file.tab(node.path))}
/>
</Match>
</Switch>
</Tabs.Content>
</Tabs>
</div>
</Show>
</div>
</Show>
<Show when={fileOpen()}>
<div onPointerDown={() => props.size.start()}>
<ResizeHandle
direction="horizontal"
edge="start"
size={layout.fileTree.width()}
min={200}
max={480}
onResize={(width) => {
props.size.touch()
layout.fileTree.resize(width)
}}
/>
</div>
</Show>
</div>
</Show>
</div>
</aside>
</Show>
)
@@ -75,6 +75,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
settings.general.showFileTree()
const idle = { type: "idle" as const }
const status = () => sync.data.session_status[params.id ?? ""] ?? idle
const messages = () => {
const id = params.id
if (!id) return []
@@ -288,7 +290,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const sessionID = params.id
if (!sessionID) return
if (sync.data.session_working(params.id ?? "")) {
if (status().type !== "idle") {
await sdk.client.session.abort({ sessionID }).catch(() => {})
}
@@ -128,17 +128,4 @@ describe("formatServerError", () => {
["Modelo nao encontrado: x/y", "Voce quis dizer: x/y2, x/y3", "Revise provider/model no config"].join("\n"),
)
})
test("unwraps SDK-wrapped errors from cause.body", () => {
const body = {
name: "ConfigInvalidError",
data: {
message: "Missing host",
},
} satisfies ConfigInvalidError
const wrapped = new Error("ConfigInvalidError", { cause: { body, status: 400 } })
expect(formatServerError(wrapped, language.t)).toBe("Arquivo de config em config invalido: Missing host")
})
})
+2 -10
View File
@@ -26,22 +26,14 @@ function tr(translator: Translator | undefined, key: string, text: string, vars?
}
export function formatServerError(error: unknown, translate?: Translator, fallback?: string) {
const unwrapped = unwrapNamedError(error)
if (isConfigInvalidErrorLike(unwrapped)) return parseReadableConfigInvalidError(unwrapped, translate)
if (isProviderModelNotFoundErrorLike(unwrapped)) return parseReadableProviderModelNotFoundError(unwrapped, translate)
if (isConfigInvalidErrorLike(error)) return parseReadableConfigInvalidError(error, translate)
if (isProviderModelNotFoundErrorLike(error)) return parseReadableProviderModelNotFoundError(error, translate)
if (error instanceof Error && error.message) return error.message
if (typeof error === "string" && error) return error
if (fallback) return fallback
return tr(translate, "error.chain.unknown", "Unknown error")
}
function unwrapNamedError(error: unknown): unknown {
if (error instanceof Error && error.cause && typeof error.cause === "object" && "body" in error.cause) {
return (error.cause as Record<string, unknown>).body
}
return error
}
function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
if (typeof error !== "object" || error === null) return false
const o = error as Record<string, unknown>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.14.49",
"version": "1.14.48",
"type": "module",
"license": "MIT",
"scripts": {
@@ -12,22 +12,13 @@ const basePayload = z.object({
url: z.string(),
})
const groups = z
.object({
result: z.union([z.number(), z.string()]).nullish(),
group: z.object({ key: z.string(), value: z.string() }).array(),
})
.array()
const groups = z.object({ group: z.object({ key: z.string(), value: z.string() }).array() }).array()
const honeycombWebhookPayload = z.discriminatedUnion("type", [
basePayload.extend({
type: z.literal("model_http_errors"),
groups,
}),
basePayload.extend({
type: z.literal("model_low_tps"),
groups,
}),
basePayload.extend({
type: z.literal("provider_http_errors"),
groups,
@@ -38,25 +29,14 @@ const honeycombWebhookPayload = z.discriminatedUnion("type", [
])
const postDiscordMessage = async (payload: z.infer<typeof honeycombWebhookPayload>) => {
const names =
payload.type === "custom"
? []
: payload.groups.flatMap((item) =>
item.group.map((g) => {
const result = item.result == null ? undefined : Number(item.result)
return `- ${g.value}${
result !== undefined && Number.isFinite(result)
? payload.type === "model_low_tps"
? ` (${Math.round(result)} TPS)`
: ` (${Math.round(result * 100)}% errors)`
: ""
}`
}),
)
const group =
payload.type === "model_http_errors" ? "model" : payload.type === "provider_http_errors" ? "provider" : undefined
const names = payload.type === "custom" ? [] : payload.groups.flatMap((item) => item.group.map((g) => g.value))
const content = [
`[**${payload.isTest ? "[TEST] " : ""}${payload.name ?? "Honeycomb alert"}**](${payload.url})`,
...names,
group && names.length > 0 ? `Affected ${group}s:` : undefined,
...names.map((name) => `- ${name}`),
"",
`<@&${DISCORD_ALERT_ROLE_ID}>`,
]
@@ -123,7 +123,7 @@ export async function handler(
? createIpRateLimiter(modelInfo.id, modelInfo.rateLimit, ip, input.request)
: createKeyRateLimiter(modelInfo.id, modelInfo.rateLimit, zenApiKey, input.request)
await rateLimiter?.check()
const stickyTracker = createStickyTracker(modelInfo.id, modelInfo.stickyProvider, sessionId)
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
const stickyProvider = await stickyTracker?.get()
const authInfo = await authenticate(modelInfo, zenApiKey)
const billingSource = validateBilling(authInfo, modelInfo)
@@ -216,7 +216,7 @@ export async function handler(
// ie. 400 error is usually provider error like malformed request
res.status !== 400 &&
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
!(modelInfo.id.startsWith("gpt-") && res.status === 404) &&
res.status !== 404 &&
// ie. cannot change codex model providers mid-session
modelInfo.stickyProvider !== "strict" &&
modelInfo.fallbackProvider &&
@@ -238,7 +238,7 @@ export async function handler(
dataDumper?.provideRequest(reqBody)
// Store sticky provider
if (res.status === 200) await stickyTracker?.set(providerInfo.id)
await stickyTracker?.set(providerInfo.id)
// Temporarily change 404 to 400 status code b/c solid start automatically override 404 response
const resStatus = res.status === 404 ? 400 : res.status
@@ -320,7 +320,6 @@ export async function handler(
await modelTpsLimiter?.track(
providerInfo.id,
providerInfo.model,
providerInfo.tpsGoal,
timestampFirstByte,
timestampLastByte,
usageInfo,
@@ -526,7 +525,7 @@ export async function handler(
})
.filter((provider) => {
if (!provider.tpsGoal) return true
const isLowTps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? false
const isLowTps = modelTpsLimits?.[`${provider.id}/${provider.model}`] ?? false
return !isLowTps
})
.map((provider) => {
@@ -5,7 +5,7 @@ import { UsageInfo } from "./provider/provider"
export function createModelTpsLimiter(providers: { id: string; model: string; tpsGoal?: number }[]) {
const tpsGoals = Object.fromEntries(
providers.flatMap((p) => {
return p.tpsGoal ? [[`${p.id}/${p.model}/${p.tpsGoal}`, p.tpsGoal]] : []
return p.tpsGoal ? [[`${p.id}/${p.model}`, p.tpsGoal]] : []
}),
)
const ids = Object.keys(tpsGoals)
@@ -56,17 +56,11 @@ export function createModelTpsLimiter(providers: { id: string; model: string; tp
}),
)
},
track: async (
provider: string,
model: string,
tpsGoal: number | undefined,
tsFirstByte: number,
tsLastByte: number,
usageInfo: UsageInfo,
) => {
if (!tpsGoal) return
const id = `${provider}/${model}/${tpsGoal}`
track: async (provider: string, model: string, tsFirstByte: number, tsLastByte: number, usageInfo: UsageInfo) => {
const id = `${provider}/${model}`
if (!ids.includes(id)) return
const tpsGoal = tpsGoals[id]
if (!tpsGoal) return
if (tsFirstByte <= 0 || tsLastByte <= 0) return
const tokens = usageInfo.outputTokens
if (tokens <= 10) return
@@ -1,42 +1,16 @@
import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js"
import { ModelStickyProviderTable } from "@opencode-ai/console-core/schema/ip.sql.js"
import { Resource } from "@opencode-ai/console-resource"
export function createStickyTracker(modelId: string, stickyProvider: "strict" | "prefer" | undefined, session: string) {
export function createStickyTracker(stickyProvider: "strict" | "prefer" | undefined, session: string) {
if (!stickyProvider) return
if (!session) return
const id = `${modelId}/${session}`
let _providerId: string | undefined
const key = `sticky:${session}`
return {
get: async () => {
const data = await Database.use((tx) =>
tx
.select({
providerId: ModelStickyProviderTable.providerId,
})
.from(ModelStickyProviderTable)
.where(eq(ModelStickyProviderTable.id, id))
.limit(1),
)
_providerId = data[0]?.providerId
return _providerId
return await Resource.GatewayKv.get(key)
},
set: async (providerId: string) => {
if (_providerId === providerId) return
await Database.use((tx) =>
tx
.insert(ModelStickyProviderTable)
.values({
id,
providerId,
})
.onDuplicateKeyUpdate({
set: {
providerId,
},
}),
)
await Resource.GatewayKv.put(key, providerId, { expirationTtl: 86400 })
},
}
}
@@ -1,7 +0,0 @@
CREATE TABLE `model_sticky_provider` (
`id` varchar(255) PRIMARY KEY,
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`time_deleted` timestamp(3),
`provider_id` varchar(255) NOT NULL
);
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.14.49",
"version": "1.14.48",
"private": true,
"type": "module",
"license": "MIT",
@@ -51,13 +51,3 @@ export const ModelTpsRateLimitTable = mysqlTable(
},
(table) => [primaryKey({ columns: [table.id, table.interval] })],
)
export const ModelStickyProviderTable = mysqlTable(
"model_sticky_provider",
{
id: varchar("id", { length: 255 }).notNull(),
...timestamps,
providerId: varchar("provider_id", { length: 255 }).notNull(),
},
(table) => [primaryKey({ columns: [table.id] })],
)
-1
View File
@@ -298,7 +298,6 @@ declare module "sst" {
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"Stat": cloudflare.Service
"ZenData": cloudflare.R2Bucket
"ZenDataNew": cloudflare.R2Bucket
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.14.49",
"version": "1.14.48",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
-43
View File
@@ -1,43 +0,0 @@
import { and, Database, inArray } from "@opencode-ai/console-core/drizzle/index.js"
import { ModelTpsRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
type Result = Record<string, { interval: number; qualify: number; unqualify: number }[]>
export default {
async fetch(request: Request) {
if (request.method !== "POST") return new Response("Method Not Allowed", { status: 405 })
const body = (await request.json()) as { ids: string[] }
const ids = body.ids
if (ids.length === 0) return Response.json({} satisfies Result)
const toInterval = (date: Date) =>
parseInt(
date
.toISOString()
.replace(/[^0-9]/g, "")
.substring(0, 12),
)
const now = Date.now()
const intervals = Array.from({ length: 30 }, (_, i) => toInterval(new Date(now - i * 60 * 1000)))
const rows = await Database.use((tx) =>
tx
.select()
.from(ModelTpsRateLimitTable)
.where(and(inArray(ModelTpsRateLimitTable.id, ids), inArray(ModelTpsRateLimitTable.interval, intervals))),
)
const rowsByKey = new Map(rows.map((row) => [`${row.id}:${row.interval}`, row]))
const result: Result = Object.fromEntries(
ids.map((id) => [
id,
intervals.map((interval) => {
const row = rowsByKey.get(`${id}:${interval}`)
return { interval, qualify: row?.qualify ?? 0, unqualify: row?.unqualify ?? 0 }
}),
]),
)
return Response.json(result)
},
}
-1
View File
@@ -298,7 +298,6 @@ declare module "sst" {
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"Stat": cloudflare.Service
"ZenData": cloudflare.R2Bucket
"ZenDataNew": cloudflare.R2Bucket
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.14.49",
"version": "1.14.48",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
-1
View File
@@ -298,7 +298,6 @@ declare module "sst" {
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"Stat": cloudflare.Service
"ZenData": cloudflare.R2Bucket
"ZenDataNew": cloudflare.R2Bucket
}
+3 -31
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.49",
"version": "1.14.48",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
@@ -26,27 +26,6 @@
"@types/semver": "catalog:"
},
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.96",
"@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.63",
"@ai-sdk/google-vertex": "4.0.112",
"@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53",
"@ai-sdk/openai-compatible": "2.0.41",
"@ai-sdk/perplexity": "3.0.26",
"@ai-sdk/provider": "3.0.8",
"@ai-sdk/provider-utils": "4.0.23",
"@ai-sdk/togetherai": "2.0.41",
"@ai-sdk/vercel": "2.0.39",
"@ai-sdk/xai": "3.0.82",
"@aws-sdk/credential-providers": "3.993.0",
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
"@npmcli/arborist": "9.4.0",
@@ -55,21 +34,14 @@
"@opentelemetry/context-async-hooks": "2.6.1",
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
"@opentelemetry/sdk-trace-base": "2.6.1",
"@openrouter/ai-sdk-provider": "2.8.1",
"ai-gateway-provider": "3.1.2",
"cross-spawn": "catalog:",
"effect": "catalog:",
"gitlab-ai-provider": "6.6.0",
"cross-spawn": "catalog:",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"immer": "11.1.4",
"mime-types": "3.0.2",
"minimatch": "10.2.5",
"npm-package-arg": "13.0.2",
"semver": "^7.6.3",
"venice-ai-sdk-provider": "2.0.1",
"xdg-basedir": "5.1.0",
"zod": "catalog:"
"xdg-basedir": "5.1.0"
},
"overrides": {
"drizzle-orm": "catalog:"
-172
View File
@@ -1,172 +0,0 @@
export * as AISDK from "./aisdk"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Cause, Context, Effect, Layer, Schema } from "effect"
import { ModelV2 } from "./model"
import { PluginV2 } from "./plugin"
import { ProviderV2 } from "./provider"
type SDK = any
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
if (typeof ms !== "number" || ms <= 0) return res
if (!res.body) return res
if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
const reader = res.body.getReader()
const body = new ReadableStream<Uint8Array>({
async pull(ctrl) {
const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
const id = setTimeout(() => {
const err = new Error("SSE read timed out")
ctl.abort(err)
void reader.cancel(err)
reject(err)
}, ms)
reader.read().then(
(part) => {
clearTimeout(id)
resolve(part)
},
(err) => {
clearTimeout(id)
reject(err)
},
)
})
if (part.done) {
ctrl.close()
return
}
ctrl.enqueue(part.value)
},
async cancel(reason) {
ctl.abort(reason)
await reader.cancel(reason)
},
})
return new Response(body, {
headers: new Headers(res.headers),
status: res.status,
statusText: res.statusText,
})
}
function prepareOptions(model: ModelV2.Info, pkg: string) {
const options: Record<string, any> = { name: model.providerID, ...model.options.aisdk.provider }
if (model.endpoint.type === "aisdk" && model.endpoint.url) options.baseURL = model.endpoint.url
const customFetch = options.fetch
const chunkTimeout = options.chunkTimeout
delete options.chunkTimeout
options.fetch = async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
const opts = { ...(init ?? {}) }
const signals = [
opts.signal,
typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined,
options.timeout !== undefined && options.timeout !== null && options.timeout !== false
? AbortSignal.timeout(options.timeout)
: undefined,
].filter((item): item is AbortSignal | AbortController => Boolean(item))
const chunkAbortCtl = signals.find((item): item is AbortController => item instanceof AbortController)
const abortSignals = signals.map((item) => (item instanceof AbortController ? item.signal : item))
if (abortSignals.length === 1) opts.signal = abortSignals[0]
if (abortSignals.length > 1) opts.signal = AbortSignal.any(abortSignals)
if ((pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure") && opts.body && opts.method === "POST") {
const body = JSON.parse(opts.body as string)
if (body.store !== true && Array.isArray(body.input)) {
for (const item of body.input) {
if ("id" in item) delete item.id
}
opts.body = JSON.stringify(body)
}
}
const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, {
...opts,
timeout: false,
})
if (!chunkAbortCtl || typeof chunkTimeout !== "number") return res
return wrapSSE(res, chunkTimeout, chunkAbortCtl)
}
return options
}
export class InitError extends Schema.TaggedErrorClass<InitError>()("AISDK.InitError", {
providerID: ProviderV2.ID,
cause: Schema.Defect,
}) {}
function initError(providerID: ProviderV2.ID) {
return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) })))
}
export interface Interface {
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const languages = new Map<string, LanguageModelV3>()
const sdks = new Map<string, SDK>()
return Service.of({
language: Effect.fn("AISDK.language")(function* (model) {
const key = `${model.providerID}/${model.id}/${model.options.variant ?? "default"}`
const existing = languages.get(key)
if (existing) return existing
if (model.endpoint.type !== "aisdk")
return yield* new InitError({
providerID: model.providerID,
cause: new Error(`Unsupported endpoint ${model.endpoint.type}`),
})
const options = prepareOptions(model, model.endpoint.package)
const sdkKey = JSON.stringify({
providerID: model.providerID,
endpoint: model.endpoint,
options,
})
const sdk =
sdks.get(sdkKey) ??
(yield* plugin
.trigger("aisdk.sdk", { model, package: model.endpoint.package, options }, {})
.pipe(initError(model.providerID))).sdk
if (!sdk)
return yield* new InitError({
providerID: model.providerID,
cause: new Error("No AISDK provider plugin returned an SDK"),
})
sdks.set(sdkKey, sdk)
const result = yield* plugin
.trigger(
"aisdk.language",
{
model,
sdk,
options,
},
{},
)
.pipe(initError(model.providerID))
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.apiID)).pipe(
initError(model.providerID),
)
languages.set(key, language)
return language
}),
})
}),
)
export const defaultLayer = layer.pipe(Layer.provide(PluginV2.defaultLayer))
-260
View File
@@ -1,260 +0,0 @@
export * as Catalog from "./catalog"
import { Context, Effect, HashMap, Layer, Option, Order, pipe, Schema, Array } from "effect"
import { produce, type Draft } from "immer"
import { ModelV2 } from "./model"
import { PluginV2 } from "./plugin"
import { ProviderV2 } from "./provider"
import { Instance } from "./instance"
type ProviderRecord = {
provider: ProviderV2.Info
models: HashMap.HashMap<ModelV2.ID, ModelV2.Info>
}
export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFoundError>()(
"CatalogV2.ProviderNotFound",
{
providerID: ProviderV2.ID,
},
) {}
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("CatalogV2.ModelNotFound", {
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
}) {}
export interface Interface {
readonly provider: {
readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info, ProviderNotFoundError>
readonly update: (providerID: ProviderV2.ID, fn: (provider: Draft<ProviderV2.Info>) => void) => Effect.Effect<void>
readonly all: () => Effect.Effect<ProviderV2.Info[]>
readonly available: () => Effect.Effect<ProviderV2.Info[]>
}
readonly model: {
readonly get: (
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
) => Effect.Effect<ModelV2.Info, ProviderNotFoundError | ModelNotFoundError>
readonly update: (
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
fn: (model: Draft<ModelV2.Info>) => void,
) => Effect.Effect<void, ProviderNotFoundError>
readonly all: () => Effect.Effect<ModelV2.Info[]>
readonly available: () => Effect.Effect<ModelV2.Info[]>
readonly default: () => Effect.Effect<Option.Option<ModelV2.Info>>
readonly setDefault: (
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
) => Effect.Effect<void, ProviderNotFoundError | ModelNotFoundError>
readonly small: (providerID: ProviderV2.ID) => Effect.Effect<Option.Option<ModelV2.Info>>
}
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
yield* Instance.Service
let records = HashMap.empty<ProviderV2.ID, ProviderRecord>()
let defaultModel: { providerID: ProviderV2.ID; modelID: ModelV2.ID } | undefined
const plugin = yield* PluginV2.Service
const resolve = (model: ModelV2.Info) => {
const provider = Option.getOrThrow(HashMap.get(records, model.providerID)).provider
const endpoint =
model.endpoint.type === "unknown"
? provider.endpoint
: model.endpoint.type === "aisdk" && provider.endpoint.type === "aisdk" && !model.endpoint.url
? { ...model.endpoint, url: provider.endpoint.url }
: model.endpoint
const options = {
headers: {
...provider.options.headers,
...model.options.headers,
},
body: {
...provider.options.body,
...model.options.body,
},
aisdk: {
provider: {
...provider.options.aisdk.provider,
...model.options.aisdk.provider,
},
request: model.options.aisdk.request,
},
variant: model.options.variant,
}
return new ModelV2.Info({
...model,
endpoint,
options,
})
}
function* getRecord(providerID: ProviderV2.ID) {
const match = HashMap.get(records, providerID)
if (!match.valueOrUndefined) return yield* new ProviderNotFoundError({ providerID })
return match.value
}
const result: Interface = {
provider: {
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
const record = yield* getRecord(providerID)
return record.provider
}),
update: Effect.fnUntraced(function* (providerID, fn) {
const current = Option.getOrUndefined(HashMap.get(records, providerID))
const provider = produce(current?.provider ?? ProviderV2.Info.empty(providerID), (draft) => {
fn(draft)
if (draft.endpoint.type === "aisdk" && typeof draft.options.aisdk.provider.baseURL === "string") {
draft.endpoint.url = draft.options.aisdk.provider.baseURL
delete draft.options.aisdk.provider.baseURL
}
})
const updated = yield* plugin.trigger("provider.update", {}, { provider, cancel: false })
records = HashMap.set(records, providerID, {
provider: updated.provider,
models: current?.models ?? HashMap.empty<ModelV2.ID, ModelV2.Info>(),
})
}),
all: Effect.fn("CatalogV2.provider.all")(function* () {
return globalThis.Array.from(HashMap.values(records)).map((record) => record.provider)
}),
available: Effect.fn("CatalogV2.provider.available")(function* () {
return globalThis.Array.from(HashMap.values(records))
.map((record) => record.provider)
.filter((provider) => provider.enabled)
}),
},
model: {
get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) {
const record = yield* getRecord(providerID)
const model = Option.getOrUndefined(HashMap.get(record.models, modelID))
if (!model) return yield* new ModelNotFoundError({ providerID, modelID })
return resolve(model)
}),
update: Effect.fnUntraced(function* (providerID, modelID, fn) {
const record = yield* getRecord(providerID)
const model = produce(
HashMap.get(record.models, modelID).pipe(Option.getOrElse(() => ModelV2.Info.empty(providerID, modelID))),
(draft) => {
fn(draft)
if (draft.endpoint.type === "aisdk" && typeof draft.options.aisdk.provider.baseURL === "string") {
draft.endpoint.url = draft.options.aisdk.provider.baseURL
delete draft.options.aisdk.provider.baseURL
}
},
)
const updated = yield* plugin.trigger("model.update", {}, { model, cancel: false })
if (updated.cancel) return
records = HashMap.set(records, providerID, {
provider: record.provider,
models: HashMap.set(
record.models,
modelID,
new ModelV2.Info({ ...updated.model, id: modelID, providerID }),
),
})
return
}),
all: Effect.fn("CatalogV2.model.all")(function* () {
return pipe(
records,
HashMap.toValues,
Array.flatMap((record) => HashMap.toValues(record.models)),
Array.map(resolve),
Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)),
)
}),
available: Effect.fn("CatalogV2.model.available")(function* () {
return (yield* result.model.all()).filter((model) => {
const record = Option.getOrUndefined(HashMap.get(records, model.providerID))
return record?.provider.enabled !== false && model.enabled
})
}),
default: Effect.fn("CatalogV2.model.default")(function* () {
if (defaultModel) {
const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option)
if (Option.isSome(model) && model.value.enabled) return model
}
return pipe(
yield* result.model.available(),
Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)),
Array.head,
)
}),
setDefault: Effect.fn("CatalogV2.model.setDefault")(function* (providerID, modelID) {
yield* result.model.get(providerID, modelID)
defaultModel = { providerID, modelID }
}),
small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
const record = Option.getOrUndefined(HashMap.get(records, providerID))
if (!record) return Option.none<ModelV2.Info>()
if (providerID === ProviderV2.ID.opencode) {
const gpt5Nano = Option.getOrUndefined(HashMap.get(record.models, ModelV2.ID.make("gpt-5-nano")))
if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(resolve(gpt5Nano))
}
const candidates = pipe(
HashMap.toValues(record.models),
Array.filter(
(model) =>
model.providerID === providerID &&
model.enabled &&
model.status === "active" &&
model.capabilities.input.some((item) => item.startsWith("text")) &&
model.capabilities.output.some((item) => item.startsWith("text")),
),
Array.map((model) => ({
model,
cost: model.cost[0] ? model.cost[0].input + model.cost[0].output : 999,
age: (Date.now() - model.time.released.epochMilliseconds) / (1000 * 60 * 60 * 24 * 30),
small: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()),
})),
Array.filter((item) => item.cost > 0 && item.age <= 18),
)
const pick = (items: typeof candidates) => {
const maxCost = Math.max(...items.map((item) => item.cost), 0.01)
const maxAge = Math.max(...items.map((item) => item.age), 0.01)
return pipe(
items,
Array.sortWith((item) => (item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2, Order.Number),
Array.map((item) => resolve(item.model)),
Array.head,
)
}
return pipe(
candidates,
Array.filter((item) => item.small),
(items) => (items.length > 0 ? pick(items) : pick(candidates)),
)
}),
},
}
return Service.of(result)
}),
)
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
export const defaultLayer = layer.pipe(Layer.provide(PluginV2.defaultLayer))
-12
View File
@@ -1,12 +0,0 @@
import { Layer, LayerMap } from "effect"
import { Instance } from "./instance"
import { Catalog } from "./catalog"
import { PluginBoot } from "./plugin/boot"
export class InstanceServiceMap extends LayerMap.Service<InstanceServiceMap>()("@opencode/example/InstanceServiceMap", {
lookup: (ref: Instance.Ref) => {
const instance = Layer.succeed(Instance.Service, Instance.Service.of(ref))
return Layer.mergeAll(Catalog.defaultLayer, PluginBoot.defaultLayer).pipe(Layer.provide(instance))
},
idleTimeToLive: "5 minutes",
}) {}
-10
View File
@@ -1,10 +0,0 @@
import { Context } from "effect"
export * as Instance from "./instance"
export type Ref = {
readonly directory: string
readonly workspaceID?: string
}
export class Service extends Context.Service<Service, Ref>()("@opencode/Instance") {}
-116
View File
@@ -1,116 +0,0 @@
import { DateTime, Schema } from "effect"
import { DateTimeUtcFromMillis } from "effect/Schema"
import { ProviderV2 } from "./provider"
export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID"))
export type ID = typeof ID.Type
export const VariantID = Schema.String.pipe(Schema.brand("VariantID"))
export type VariantID = typeof VariantID.Type
// Grouping of models, eg claude opus, claude sonnet
export const Family = Schema.String.pipe(Schema.brand("Family"))
export type Family = typeof Family.Type
export const Capabilities = Schema.Struct({
tools: Schema.Boolean,
// mime patterns, image, audio, video/*, text/*
input: Schema.String.pipe(Schema.Array),
output: Schema.String.pipe(Schema.Array),
})
export type Capabilities = typeof Capabilities.Type
export const Cost = 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,
write: Schema.Finite,
}),
})
export const Ref = Schema.Struct({
id: ID,
providerID: ProviderV2.ID,
variant: VariantID,
})
export type Ref = typeof Ref.Type
export class Info extends Schema.Class<Info>("ModelV2.Info")({
id: ID,
apiID: ID,
providerID: ProviderV2.ID,
family: Family.pipe(Schema.optional),
name: Schema.String,
endpoint: ProviderV2.Endpoint,
capabilities: Capabilities,
options: Schema.Struct({
...ProviderV2.Options.fields,
variant: Schema.String.pipe(Schema.optional),
}),
variants: Schema.Struct({
id: VariantID,
...ProviderV2.Options.fields,
}).pipe(Schema.Array),
time: Schema.Struct({
released: DateTimeUtcFromMillis,
}),
cost: Cost.pipe(Schema.Array),
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
enabled: Schema.Boolean,
limit: Schema.Struct({
context: Schema.Int,
input: Schema.Int.pipe(Schema.optional),
output: Schema.Int,
}),
}) {
static empty(providerID: ProviderV2.ID, modelID: ID) {
return new Info({
id: modelID,
apiID: modelID,
providerID,
name: modelID,
endpoint: {
type: "unknown",
},
capabilities: {
tools: false,
input: [],
output: [],
},
options: {
headers: {},
body: {},
aisdk: {
provider: {},
request: {},
},
},
variants: [],
time: {
released: DateTime.makeUnsafe(0),
},
cost: [],
status: "active",
enabled: true,
limit: {
context: 0,
output: 0,
},
})
}
}
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
const [providerID, ...modelID] = input.split("/")
return {
providerID: ProviderV2.ID.make(providerID),
modelID: ID.make(modelID.join("/")),
}
}
export * as ModelV2 from "./model"
-2
View File
@@ -1,2 +0,0 @@
// Auto-generated by build.ts - do not edit
export declare const snapshot: Record<string, unknown>
File diff suppressed because it is too large Load Diff
-146
View File
@@ -1,146 +0,0 @@
export * as PluginV2 from "./plugin"
import { createDraft, finishDraft, type Draft } from "immer"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { type ProviderV2 } from "./provider"
import { Context, Effect, Layer, Schema } from "effect"
import type { ModelV2 } from "./model"
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
export type ID = typeof ID.Type
type HookSpec = {
"provider.update": {
input: {}
output: {
provider: ProviderV2.Info
cancel: boolean
}
}
"model.update": {
input: {}
output: {
model: ModelV2.Info
cancel: boolean
}
}
"aisdk.language": {
input: {
model: ModelV2.Info
sdk: any
options: Record<string, any>
}
output: {
language?: LanguageModelV3
}
}
"aisdk.sdk": {
input: {
model: ModelV2.Info
package: string
options: Record<string, any>
}
output: {
sdk?: any
}
}
}
export type Hooks = {
[Name in keyof HookSpec]: Readonly<HookSpec[Name]["input"]> & {
-readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object
? Draft<HookSpec[Name]["output"][Field]>
: HookSpec[Name]["output"][Field]
}
}
export type HookFunctions = {
[key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect<void>
}
export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
export type Effect = Effect.Effect<HookFunctions | void, never, never>
export function define<R>(input: { id: ID; effect: Effect.Effect<HookFunctions | void, never, R> }) {
return input
}
export interface Interface {
readonly add: (input: { id: ID; effect: Effect }) => Effect.Effect<void>
readonly remove: (id: ID) => Effect.Effect<void>
readonly trigger: <Name extends keyof Hooks>(
name: Name,
input: HookInput<Name>,
output: HookOutput<Name>,
) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
let hooks: {
id: ID
hooks: HookFunctions
}[] = []
const svc = Service.of({
add: Effect.fn("Plugin.add")(function* (input) {
const result = yield* input.effect
if (!result) return
hooks = [
...hooks.filter((item) => item.id !== input.id),
{
id: input.id,
hooks: result,
},
]
}),
trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) {
const draftEntries = new Map<string, ReturnType<typeof createDraft>>()
const event = {
...input,
...output,
} as Record<string, unknown>
for (const [field, value] of Object.entries(output)) {
if (value && typeof value === "object") {
draftEntries.set(field, createDraft(value))
event[field] = draftEntries.get(field)
}
}
for (const item of hooks) {
const match = item.hooks[name]
if (!match) continue
yield* match(event as any).pipe(
Effect.withSpan(`Plugin.hook.${name}`, {
attributes: {
plugin: item.id,
hook: name,
},
}),
)
}
for (const [field, draft] of draftEntries) {
event[field] = finishDraft(draft)
}
return event as any
}),
remove: Effect.fn("Plugin.remove")(function* (id) {
hooks = hooks.filter((item) => item.id !== id)
}),
})
return svc
}),
)
export const defaultLayer = layer
// opencode
// sdcok
-27
View File
@@ -1,27 +0,0 @@
import { Effect } from "effect"
import { AuthV2 } from "../auth"
import { PluginV2 } from "../plugin"
export const AuthPlugin = PluginV2.define({
id: PluginV2.ID.make("auth"),
effect: Effect.gen(function* () {
const auth = yield* AuthV2.Service
return {
"provider.update": Effect.fn(function* (evt) {
const account = yield* auth.active(AuthV2.ServiceID.make(evt.provider.id)).pipe(Effect.orDie)
if (!account) return
evt.provider.enabled = {
via: "auth",
service: account.serviceID,
}
if (account.credential.type === "api") {
evt.provider.options.aisdk.provider.apiKey = account.credential.key
Object.assign(evt.provider.options.aisdk.provider, account.credential.metadata ?? {})
}
if (account.credential.type === "oauth") {
evt.provider.options.aisdk.provider.apiKey = account.credential.access
}
}),
}
}),
})
-71
View File
@@ -1,71 +0,0 @@
export * as PluginBoot from "./boot"
import { Context, Deferred, Effect, Layer } from "effect"
import { AuthV2 } from "../auth"
import { Catalog } from "../catalog"
import { Npm } from "../npm"
import { PluginV2 } from "../plugin"
import { AuthPlugin } from "./auth"
import { EnvPlugin } from "./env"
import { ModelsDevPlugin } from "./models-dev"
import { ProviderPlugins } from "./provider"
type Plugin = {
id: PluginV2.ID
effect: Effect.Effect<PluginV2.HookFunctions | void, never, Catalog.Service | AuthV2.Service | Npm.Service>
}
export interface Interface {
readonly wait: () => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginBoot") {}
export const layer: Layer.Layer<Service, never, Catalog.Service | PluginV2.Service | AuthV2.Service | Npm.Service> =
Layer.effect(
Service,
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const plugin = yield* PluginV2.Service
const auth = yield* AuthV2.Service
const npm = yield* Npm.Service
const done = yield* Deferred.make<void>()
const add = Effect.fn("PluginBoot.add")(function* (input: Plugin) {
yield* plugin.add({
id: input.id,
effect: input.effect.pipe(
Effect.provideService(Catalog.Service, catalog),
Effect.provideService(AuthV2.Service, auth),
Effect.provideService(Npm.Service, npm),
),
})
})
const boot = Effect.gen(function* () {
yield* add(EnvPlugin)
yield* add(AuthPlugin)
for (const item of ProviderPlugins) {
yield* add(item)
}
yield* add(ModelsDevPlugin)
}).pipe(Effect.withSpan("PluginBoot.boot"))
yield* boot.pipe(
Effect.exit,
Effect.flatMap((exit) => Deferred.done(done, exit)),
Effect.forkScoped,
)
return Service.of({
wait: () => Deferred.await(done),
})
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Catalog.defaultLayer),
Layer.provide(PluginV2.defaultLayer),
Layer.provide(Layer.orDie(AuthV2.defaultLayer)),
Layer.provide(Npm.defaultLayer),
)
-18
View File
@@ -1,18 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../plugin"
export const EnvPlugin = PluginV2.define({
id: PluginV2.ID.make("env"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
const key = evt.provider.env.find((item) => process.env[item])
if (!key) return
evt.provider.enabled = {
via: "env",
name: key,
}
}),
}
}),
})
@@ -1,94 +0,0 @@
export * as LayerMapExample from "./layer-map.example"
import { Context, Effect, Layer, LayerMap } from "effect"
import { Npm } from "../npm"
/**
* Tutorial: split global services from context-specific services.
*
* Use this pattern when part of the app should be constructed once at the app edge,
* while another part should be cached per request/project/workspace key.
*
* In this example:
* - Npm.Service is the global service. It is not keyed by request context and should
* be provided once by the application runtime.
* - ConfigService is context-specific. It is built from a RequestContext key and is
* cached by LayerMap for that key.
* - ConfigServiceMap.layer owns the cache. Provide it once globally, then each
* request can provide ConfigServiceMap.get(context) to select the right instance.
*
* Lifetime model:
* - ConfigServiceMap.layer has the app/global lifetime and depends on Npm.Service.
* - ConfigServiceMap.get(context) has the request/context lifetime and provides
* ConfigService for exactly that context key.
* - The cached ConfigService entry stays alive while something is using it. Once idle,
* it remains cached for idleTimeToLive, then its scope is finalized.
* - invalidate(context) removes the cache entry for future lookups. Active users keep
* running on the old instance; the next lookup can create a fresh instance.
*
* Key model:
* - Keys can be strings, structs, classes, arrays, etc.
* - Prefer primitive or immutable keys. Effect uses Hash / Equal semantics for cache
* lookup, so mutating an object after it has been used as a key is a bug.
*/
export type RequestContext = {
readonly directory: string
readonly workspace: string
}
export class RequestContextRef extends Context.Service<RequestContextRef, RequestContext>()(
"@opencode/example/RequestContextRef",
) {}
export interface ConfigServiceShape {
readonly directory: string
readonly workspace: string
readonly nextUse: () => Effect.Effect<number>
readonly which: Npm.Interface["which"]
}
export class ConfigService extends Context.Service<ConfigService, ConfigServiceShape>()(
"@opencode/example/ConfigService",
) {}
const configServiceLayer = Layer.effect(
ConfigService,
Effect.gen(function* () {
const context = yield* RequestContextRef
const npm = yield* Npm.Service
let useCount = 0
return ConfigService.of({
directory: context.directory,
workspace: context.workspace,
nextUse: () => Effect.succeed(++useCount),
which: npm.which,
})
}),
)
export class ConfigServiceMap extends LayerMap.Service<ConfigServiceMap>()("@opencode/example/ConfigServiceMap", {
lookup: (context: RequestContext) =>
configServiceLayer.pipe(Layer.provide(Layer.succeed(RequestContextRef, RequestContextRef.of(context)))),
idleTimeToLive: "5 minutes",
}) {}
export const appLayer = ConfigServiceMap.layer
export const readConfig = Effect.fn("LayerMapExample.readConfig")(function* () {
const config = yield* ConfigService
return {
directory: config.directory,
workspace: config.workspace,
useCount: yield* config.nextUse(),
}
})
export const handleRequest = Effect.fn("LayerMapExample.handleRequest")(function* (context: RequestContext) {
return yield* readConfig().pipe(Effect.provide(ConfigServiceMap.get(context)))
})
export const invalidateContext = (context: RequestContext) => ConfigServiceMap.invalidate(context)
-108
View File
@@ -1,108 +0,0 @@
import { DateTime, Effect } from "effect"
import { Catalog } from "../catalog"
import { ModelV2 } from "../model"
import { ModelsDev } from "../models"
import { PluginV2 } from "../plugin"
import { ProviderV2 } from "../provider"
function released(date: string) {
const time = Date.parse(date)
return DateTime.makeUnsafe(Number.isFinite(time) ? time : 0)
}
function cost(input: ModelsDev.Model["cost"]) {
const base = {
input: input?.input ?? 0,
output: input?.output ?? 0,
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 variants(model: ModelsDev.Model) {
return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => ({
id: ModelV2.VariantID.make(id),
headers: { ...(item.provider?.headers ?? {}) },
body: { ...(item.provider?.body ?? {}) },
aisdk: {
provider: {},
request: {},
},
}))
}
export const ModelsDevPlugin = PluginV2.define({
id: PluginV2.ID.make("models-dev"),
effect: Effect.gen(function* () {
const catalog = yield* Catalog.Service
const modelsDev = yield* ModelsDev.Service
for (const item of Object.values(yield* modelsDev.get())) {
const providerID = ProviderV2.ID.make(item.id)
yield* catalog.provider.update(providerID, (provider) => {
provider.name = item.name
provider.env = [...item.env]
provider.endpoint = item.npm
? {
type: "aisdk",
package: item.npm,
url: item.api,
}
: {
type: "unknown",
}
})
for (const model of Object.values(item.models)) {
const modelID = ModelV2.ID.make(model.id)
yield* catalog.model
.update(providerID, modelID, (draft) => {
draft.name = model.name
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
draft.endpoint = model.provider?.npm
? {
type: "aisdk",
package: model.provider?.npm,
url: model.provider.api,
}
: {
type: "unknown",
}
draft.capabilities = {
tools: model.tool_call,
input: [...(model.modalities?.input ?? [])],
output: [...(model.modalities?.output ?? [])],
}
draft.variants = variants(model)
draft.time.released = released(model.release_date)
draft.cost = cost(model.cost)
draft.status = model.status ?? "active"
draft.enabled = true
draft.limit = {
context: model.limit.context,
input: model.limit.input,
output: model.limit.output,
}
})
.pipe(Effect.orDie)
}
}
}).pipe(Effect.provide(ModelsDev.defaultLayer)),
})
-1
View File
@@ -1 +0,0 @@
export { ProviderPlugins } from "./provider/index"
@@ -1,15 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
export const AlibabaPlugin = PluginV2.define({
id: PluginV2.ID.make("alibaba"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/alibaba") return
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
evt.sdk = mod.createAlibaba(evt.options)
}),
}
}),
})
@@ -1,94 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
// Bedrock cross-region inference profiles require regional prefixes only for
// specific model/region combinations. Keep the mapping narrow and avoid
// double-prefixing model IDs that models.dev already marks as global/us/eu/etc.
function resolveModelID(modelID: string, region: string | undefined) {
const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) return modelID
const resolvedRegion = region ?? "us-east-1"
const regionPrefix = resolvedRegion.split("-")[0]
if (regionPrefix === "us") {
const requiresPrefix = ["nova-micro", "nova-lite", "nova-pro", "nova-premier", "nova-2", "claude", "deepseek"].some(
(item) => modelID.includes(item),
)
if (requiresPrefix && !resolvedRegion.startsWith("us-gov")) return `${regionPrefix}.${modelID}`
return modelID
}
if (regionPrefix === "eu") {
const regionRequiresPrefix = [
"eu-west-1",
"eu-west-2",
"eu-west-3",
"eu-north-1",
"eu-central-1",
"eu-south-1",
"eu-south-2",
].some((item) => resolvedRegion.includes(item))
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((item) =>
modelID.includes(item),
)
return regionRequiresPrefix && modelRequiresPrefix ? `${regionPrefix}.${modelID}` : modelID
}
if (regionPrefix !== "ap") return modelID
const australia = ["ap-southeast-2", "ap-southeast-4"].includes(resolvedRegion)
if (australia && ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((item) => modelID.includes(item))) {
return `au.${modelID}`
}
const prefix = resolvedRegion === "ap-northeast-1" ? "jp" : "apac"
return ["claude", "nova-lite", "nova-micro", "nova-pro"].some((item) => modelID.includes(item))
? `${prefix}.${modelID}`
: modelID
}
export const AmazonBedrockPlugin = PluginV2.define({
id: PluginV2.ID.make("amazon-bedrock"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.amazonBedrock) return
if (evt.provider.endpoint.type !== "aisdk") return
if (typeof evt.provider.options.aisdk.provider.endpoint !== "string") return
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
// endpoints as `endpoint`; move it into the catalog endpoint URL once.
evt.provider.endpoint.url = evt.provider.options.aisdk.provider.endpoint
delete evt.provider.options.aisdk.provider.endpoint
}),
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/amazon-bedrock") return
const options = { ...evt.options }
const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE
const region = typeof options.region === "string" ? options.region : (process.env.AWS_REGION ?? "us-east-1")
const bearerToken =
process.env.AWS_BEARER_TOKEN_BEDROCK ??
(typeof options.bearerToken === "string" ? options.bearerToken : undefined)
if (bearerToken && !process.env.AWS_BEARER_TOKEN_BEDROCK) process.env.AWS_BEARER_TOKEN_BEDROCK = bearerToken
const containerCreds = Boolean(
process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI,
)
options.region = region
if (typeof options.endpoint === "string") options.baseURL = options.endpoint
if (!bearerToken && options.credentialProvider === undefined) {
// Do not gate SDK creation on explicit AWS env vars. The default chain
// also handles ~/.aws/credentials, SSO, process creds, and instance roles.
const { fromNodeProviderChain } = yield* Effect.promise(() => import("@aws-sdk/credential-providers"))
options.credentialProvider = fromNodeProviderChain(profile ? { profile } : {})
}
const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock"))
evt.sdk = mod.createAmazonBedrock(options)
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.apiID, region))
}),
}
}),
})
@@ -1,21 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const AnthropicPlugin = PluginV2.define({
id: PluginV2.ID.make("anthropic"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.anthropic) return
evt.provider.options.headers["anthropic-beta"] =
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
}),
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/anthropic") return
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
evt.sdk = mod.createAnthropic(evt.options)
}),
}
}),
})
@@ -1,64 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
if (useChat && sdk.chat) return sdk.chat(modelID)
if (sdk.responses) return sdk.responses(modelID)
if (sdk.messages) return sdk.messages(modelID)
if (sdk.chat) return sdk.chat(modelID)
return sdk.languageModel(modelID)
}
export const AzurePlugin = PluginV2.define({
id: PluginV2.ID.make("azure"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.azure) return
const configured = evt.provider.options.aisdk.provider.resourceName
const resourceName =
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
if (resourceName) evt.provider.options.aisdk.provider.resourceName = resourceName
}),
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/azure") return
if (evt.model.providerID === ProviderV2.ID.azure) {
if (
!evt.options.resourceName &&
!evt.options.baseURL &&
(evt.model.endpoint.type !== "aisdk" || !evt.model.endpoint.url)
) {
throw new Error(
"AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
)
}
}
const mod = yield* Effect.promise(() => import("@ai-sdk/azure"))
evt.sdk = mod.createAzure(evt.options)
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.azure) return
evt.language = selectLanguage(evt.sdk, evt.model.apiID, Boolean(evt.options.useCompletionUrls))
}),
}
}),
})
export const AzureCognitiveServicesPlugin = PluginV2.define({
id: PluginV2.ID.make("azure-cognitive-services"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.make("azure-cognitive-services")) return
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
if (resourceName)
evt.provider.options.aisdk.provider.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai`
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
evt.language = selectLanguage(evt.sdk, evt.model.apiID, Boolean(evt.options.useCompletionUrls))
}),
}
}),
})
@@ -1,20 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const CerebrasPlugin = PluginV2.define({
id: PluginV2.ID.make("cerebras"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.make("cerebras")) return
evt.provider.options.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
}),
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/cerebras") return
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
evt.sdk = mod.createCerebras(evt.options)
}),
}
}),
})
@@ -1,81 +0,0 @@
import os from "os"
import { InstallationVersion } from "../../installation/version"
import { Effect, Option, Schema } from "effect"
import { PluginV2 } from "../../plugin"
export const CloudflareAIGatewayPlugin = PluginV2.define({
id: PluginV2.ID.make("cloudflare-ai-gateway"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "ai-gateway-provider") return
if (evt.options.baseURL) return
const config = gatewayConfig(evt.options)
if (!config) return
const metadata = gatewayMetadata(evt.options)
const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider")).pipe(Effect.orDie)
const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified")).pipe(
Effect.orDie,
)
const gateway = createAiGateway({
accountId: config.accountId,
gateway: config.gatewayId,
apiKey: config.apiKey,
options: gatewayOptions(evt.options, metadata),
} as any)
const unified = createUnified()
evt.sdk = {
languageModel(modelID: string) {
return gateway(unified(modelID))
},
}
}),
}
}),
})
type GatewayConfig = {
accountId: string
gatewayId: string
apiKey: string
}
const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
function gatewayConfig(options: Record<string, unknown>): GatewayConfig | undefined {
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId")
// AuthPlugin copies CLI prompt metadata into options. The prompt stores the
// gateway as gatewayId, while older config examples may use gateway.
const gatewayId =
process.env.CLOUDFLARE_GATEWAY_ID ?? stringOption(options, "gatewayId") ?? stringOption(options, "gateway")
const apiKey = process.env.CLOUDFLARE_API_TOKEN ?? process.env.CF_AIG_TOKEN ?? stringOption(options, "apiKey")
if (!accountId || !gatewayId || !apiKey) return undefined
return { accountId, gatewayId, apiKey }
}
function gatewayMetadata(options: Record<string, unknown>) {
// Preserve the legacy cf-aig-metadata header escape hatch for gateway logging
// metadata, but prefer the typed metadata option when present.
if (options.metadata !== undefined) return options.metadata
const raw = (options.headers as Record<string, string> | undefined)?.["cf-aig-metadata"]
return raw ? Option.getOrUndefined(decodeJson(raw)) : undefined
}
function gatewayOptions(options: Record<string, unknown>, metadata: unknown) {
return {
metadata,
cacheTtl: options.cacheTtl,
cacheKey: options.cacheKey,
skipCache: options.skipCache,
collectLog: options.collectLog,
headers: {
"User-Agent": `opencode/${InstallationVersion} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`,
},
}
}
function stringOption(options: Record<string, unknown>, key: string) {
return typeof options[key] === "string" ? options[key] : undefined
}
@@ -1,69 +0,0 @@
import os from "os"
import { InstallationVersion } from "../../installation/version"
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
export const CloudflareWorkersAIPlugin = PluginV2.define({
id: PluginV2.ID.make("cloudflare-workers-ai"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== providerID) return
if (evt.provider.endpoint.type !== "aisdk") return
if (evt.provider.endpoint.url) return
const accountId = resolveAccountId(evt.provider.options.aisdk.provider)
if (accountId) evt.provider.endpoint.url = workersEndpoint(accountId)
}),
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return
if (evt.package !== "@ai-sdk/openai-compatible") return
if (!hasWorkersEndpoint(evt.model.endpoint)) return
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
evt.sdk = mod.createOpenAICompatible(sdkOptions(evt.options) as any)
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return
evt.language = evt.sdk.languageModel(evt.model.apiID)
}),
}
}),
})
function resolveAccountId(options: Record<string, unknown>) {
return process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId")
}
function workersEndpoint(accountId: string) {
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
}
function hasWorkersEndpoint(endpoint: ProviderV2.Endpoint) {
return endpoint.type === "aisdk" && Boolean(endpoint.url)
}
function sdkOptions(options: Record<string, any>) {
return {
...options,
baseURL: expandAccountId(options.baseURL),
apiKey: process.env.CLOUDFLARE_API_KEY ?? options.apiKey,
headers: {
"User-Agent": `opencode/${InstallationVersion} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
...options.headers,
},
name: providerID,
}
}
function expandAccountId(baseURL: unknown) {
if (typeof baseURL !== "string") return baseURL
return baseURL.replaceAll("${CLOUDFLARE_ACCOUNT_ID}", process.env.CLOUDFLARE_ACCOUNT_ID ?? "${CLOUDFLARE_ACCOUNT_ID}")
}
function stringOption(options: Record<string, unknown>, key: string) {
return typeof options[key] === "string" ? options[key] : undefined
}
@@ -1,15 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
export const CoherePlugin = PluginV2.define({
id: PluginV2.ID.make("cohere"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/cohere") return
const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
evt.sdk = mod.createCohere(evt.options)
}),
}
}),
})
@@ -1,15 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
export const DeepInfraPlugin = PluginV2.define({
id: PluginV2.ID.make("deepinfra"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/deepinfra") return
const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
evt.sdk = mod.createDeepInfra(evt.options)
}),
}
}),
})
@@ -1,31 +0,0 @@
import { Npm } from "../../npm"
import { Effect, Option } from "effect"
import { pathToFileURL } from "url"
import { PluginV2 } from "../../plugin"
export const DynamicProviderPlugin = PluginV2.define({
id: PluginV2.ID.make("dynamic-provider"),
effect: Effect.gen(function* () {
const npm = yield* Npm.Service
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.sdk) return
const installedPath = evt.package.startsWith("file://")
? evt.package
: Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint)
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
const mod = yield* Effect.promise(async () => {
return (await import(
installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href
)) as Record<string, (options: any) => any>
}).pipe(Effect.orDie)
const match = Object.keys(mod).find((name) => name.startsWith("create"))
if (!match) throw new Error(`Package ${evt.package} has no provider factory export`)
evt.sdk = mod[match](evt.options)
}),
}
}),
})
@@ -1,15 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
export const GatewayPlugin = PluginV2.define({
id: PluginV2.ID.make("gateway"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/gateway") return
const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
evt.sdk = mod.createGateway(evt.options)
}),
}
}),
})
@@ -1,44 +0,0 @@
import { Effect } from "effect"
import { ModelV2 } from "../../model"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
function shouldUseResponses(modelID: string) {
// Copilot supports Responses for GPT-5 class models, except mini variants
// which still need the chat-completions endpoint.
const match = /^gpt-(\d+)/.exec(modelID)
if (!match) return false
return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
}
export const GithubCopilotPlugin = PluginV2.define({
id: PluginV2.ID.make("github-copilot"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.githubCopilot) return
}),
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/github-copilot") return
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
evt.sdk = mod.createOpenaiCompatible(evt.options)
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
evt.language = evt.sdk.languageModel(evt.model.apiID)
return
}
evt.language = shouldUseResponses(evt.model.apiID)
? evt.sdk.responses(evt.model.apiID)
: evt.sdk.chat(evt.model.apiID)
}),
"model.update": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
// This chat-only alias conflicts with the Copilot GPT-5 Responses route,
// so hide it only for Copilot rather than for every provider catalog.
if (evt.model.id === ModelV2.ID.make("gpt-5-chat-latest")) evt.cancel = true
}),
}
}),
})
@@ -1,65 +0,0 @@
import os from "os"
import { InstallationVersion } from "../../installation/version"
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const GitLabPlugin = PluginV2.define({
id: PluginV2.ID.make("gitlab"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "gitlab-ai-provider") return
const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
evt.sdk = mod.createGitLab({
...evt.options,
instanceUrl:
typeof evt.options.instanceUrl === "string"
? evt.options.instanceUrl
: (process.env.GITLAB_INSTANCE_URL ?? "https://gitlab.com"),
apiKey: typeof evt.options.apiKey === "string" ? evt.options.apiKey : process.env.GITLAB_TOKEN,
aiGatewayHeaders: {
"User-Agent": `opencode/${InstallationVersion} gitlab-ai-provider/${mod.VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
"anthropic-beta": "context-1m-2025-08-07",
...evt.options.aiGatewayHeaders,
},
featureFlags: {
duo_agent_platform_agentic_chat: true,
duo_agent_platform: true,
...evt.options.featureFlags,
},
})
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
const featureFlags =
typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
if (evt.model.apiID.startsWith("duo-workflow-")) {
const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie)
const workflowRef =
typeof evt.model.options.aisdk.request.workflowRef === "string"
? evt.model.options.aisdk.request.workflowRef
: undefined
const workflowDefinition =
typeof evt.model.options.aisdk.request.workflowDefinition === "string"
? evt.model.options.aisdk.request.workflowDefinition
: undefined
const language = evt.sdk.workflowChat(
gitlab.isWorkflowModel(evt.model.apiID) ? evt.model.apiID : "duo-workflow",
{
featureFlags,
workflowDefinition,
},
)
if (workflowRef) language.selectedModelRef = workflowRef
evt.language = language
return
}
evt.language = evt.sdk.agenticChat(evt.model.apiID, {
aiGatewayHeaders: evt.options.aiGatewayHeaders,
featureFlags,
})
}),
}
}),
})
@@ -1,141 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
function resolveProject(options: Record<string, any>) {
// models.dev advertises GOOGLE_VERTEX_PROJECT for Vertex, while Google SDKs
// and ADC examples commonly use the broader Google Cloud project aliases.
return (
options.project ??
process.env.GOOGLE_VERTEX_PROJECT ??
process.env.GOOGLE_CLOUD_PROJECT ??
process.env.GCP_PROJECT ??
process.env.GCLOUD_PROJECT
)
}
function resolveLocation(options: Record<string, any>) {
return (
options.location ??
process.env.GOOGLE_VERTEX_LOCATION ??
process.env.GOOGLE_CLOUD_LOCATION ??
process.env.VERTEX_LOCATION ??
"us-central1"
)
}
function vertexEndpoint(location: string) {
return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`
}
function replaceVertexVars(value: string, project: string | undefined, location: string) {
// Vertex OpenAI-compatible endpoints are stored as templates in the catalog;
// expand them after provider config/env project and location have been resolved.
return value
.replaceAll("${GOOGLE_VERTEX_PROJECT}", project ?? "${GOOGLE_VERTEX_PROJECT}")
.replaceAll("${GOOGLE_VERTEX_LOCATION}", location)
.replaceAll("${GOOGLE_VERTEX_ENDPOINT}", vertexEndpoint(location))
}
function authFetch(fetchWithRuntimeOptions?: unknown) {
// Native Vertex SDKs handle ADC internally. OpenAI-compatible Vertex endpoints
// do not, so inject a Google access token into their fetch path.
return async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
const { GoogleAuth } = await import("google-auth-library")
const auth = new GoogleAuth()
const client = await auth.getApplicationDefault()
const token = await client.credential.getAccessToken()
const headers = new Headers(init?.headers)
headers.set("Authorization", `Bearer ${token.token}`)
return typeof fetchWithRuntimeOptions === "function"
? fetchWithRuntimeOptions(input, { ...init, headers })
: fetch(input, { ...init, headers })
}
}
export const GoogleVertexPlugin = PluginV2.define({
id: PluginV2.ID.make("google-vertex"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.googleVertex) return
const project = resolveProject(evt.provider.options.aisdk.provider)
const location = String(resolveLocation(evt.provider.options.aisdk.provider))
if (project) evt.provider.options.aisdk.provider.project = project
evt.provider.options.aisdk.provider.location = location
if (evt.provider.endpoint.type === "aisdk" && evt.provider.endpoint.url) {
evt.provider.endpoint.url = replaceVertexVars(evt.provider.endpoint.url, project, location)
}
if (
evt.provider.endpoint.type === "aisdk" &&
evt.provider.endpoint.package.includes("@ai-sdk/openai-compatible")
) {
evt.provider.options.aisdk.provider.fetch = authFetch(evt.provider.options.aisdk.provider.fetch)
}
}),
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
evt.options.fetch = authFetch(evt.options.fetch)
return
}
if (evt.package !== "@ai-sdk/google-vertex") return
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex"))
const project = resolveProject(evt.options)
const location = resolveLocation(evt.options)
const options = { ...evt.options }
delete options.fetch
evt.sdk = mod.createVertex({
...options,
project,
location,
})
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
evt.language = evt.sdk.languageModel(String(evt.model.apiID).trim())
}),
}
}),
})
export const GoogleVertexAnthropicPlugin = PluginV2.define({
id: PluginV2.ID.make("google-vertex-anthropic"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.make("google-vertex-anthropic")) return
const project =
evt.provider.options.aisdk.provider.project ??
process.env.GOOGLE_CLOUD_PROJECT ??
process.env.GCP_PROJECT ??
process.env.GCLOUD_PROJECT
const location =
evt.provider.options.aisdk.provider.location ??
process.env.GOOGLE_CLOUD_LOCATION ??
process.env.VERTEX_LOCATION ??
"global"
if (project) evt.provider.options.aisdk.provider.project = project
evt.provider.options.aisdk.provider.location = location
}),
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
evt.sdk = mod.createVertexAnthropic({
...evt.options,
project:
typeof evt.options.project === "string"
? evt.options.project
: (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT),
location:
typeof evt.options.location === "string"
? evt.options.location
: (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global"),
})
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
evt.language = evt.sdk.languageModel(String(evt.model.apiID).trim())
}),
}
}),
})
@@ -1,15 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
export const GooglePlugin = PluginV2.define({
id: PluginV2.ID.make("google"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/google") return
const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
evt.sdk = mod.createGoogleGenerativeAI(evt.options)
}),
}
}),
})
-15
View File
@@ -1,15 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
export const GroqPlugin = PluginV2.define({
id: PluginV2.ID.make("groq"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/groq") return
const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
evt.sdk = mod.createGroq(evt.options)
}),
}
}),
})
@@ -1,67 +0,0 @@
import { AlibabaPlugin } from "./alibaba"
import { AmazonBedrockPlugin } from "./amazon-bedrock"
import { AnthropicPlugin } from "./anthropic"
import { AzureCognitiveServicesPlugin, AzurePlugin } from "./azure"
import { CerebrasPlugin } from "./cerebras"
import { CloudflareAIGatewayPlugin } from "./cloudflare-ai-gateway"
import { CloudflareWorkersAIPlugin } from "./cloudflare-workers-ai"
import { CoherePlugin } from "./cohere"
import { DeepInfraPlugin } from "./deepinfra"
import { DynamicProviderPlugin } from "./dynamic"
import { GatewayPlugin } from "./gateway"
import { GithubCopilotPlugin } from "./github-copilot"
import { GitLabPlugin } from "./gitlab"
import { GooglePlugin } from "./google"
import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "./google-vertex"
import { GroqPlugin } from "./groq"
import { KiloPlugin } from "./kilo"
import { LLMGatewayPlugin } from "./llmgateway"
import { MistralPlugin } from "./mistral"
import { NvidiaPlugin } from "./nvidia"
import { OpenAIPlugin } from "./openai"
import { OpenAICompatiblePlugin } from "./openai-compatible"
import { OpencodePlugin } from "./opencode"
import { OpenRouterPlugin } from "./openrouter"
import { PerplexityPlugin } from "./perplexity"
import { SapAICorePlugin } from "./sap-ai-core"
import { TogetherAIPlugin } from "./togetherai"
import { VercelPlugin } from "./vercel"
import { VenicePlugin } from "./venice"
import { XAIPlugin } from "./xai"
import { ZenmuxPlugin } from "./zenmux"
export const ProviderPlugins = [
AlibabaPlugin,
AmazonBedrockPlugin,
AnthropicPlugin,
AzureCognitiveServicesPlugin,
AzurePlugin,
CerebrasPlugin,
CloudflareAIGatewayPlugin,
CloudflareWorkersAIPlugin,
CoherePlugin,
DeepInfraPlugin,
GatewayPlugin,
GithubCopilotPlugin,
GitLabPlugin,
GooglePlugin,
GoogleVertexAnthropicPlugin,
GoogleVertexPlugin,
GroqPlugin,
KiloPlugin,
LLMGatewayPlugin,
MistralPlugin,
NvidiaPlugin,
OpencodePlugin,
OpenAICompatiblePlugin,
OpenAIPlugin,
OpenRouterPlugin,
PerplexityPlugin,
SapAICorePlugin,
TogetherAIPlugin,
VercelPlugin,
VenicePlugin,
XAIPlugin,
ZenmuxPlugin,
DynamicProviderPlugin,
]
-16
View File
@@ -1,16 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const KiloPlugin = PluginV2.define({
id: PluginV2.ID.make("kilo"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.make("kilo")) return
evt.provider.options.headers["HTTP-Referer"] = "https://opencode.ai/"
evt.provider.options.headers["X-Title"] = "opencode"
}),
}
}),
})
@@ -1,18 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const LLMGatewayPlugin = PluginV2.define({
id: PluginV2.ID.make("llmgateway"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.make("llmgateway")) return
if (evt.provider.enabled === false) return
evt.provider.options.headers["HTTP-Referer"] = "https://opencode.ai/"
evt.provider.options.headers["X-Title"] = "opencode"
evt.provider.options.headers["X-Source"] = "opencode"
}),
}
}),
})
@@ -1,15 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
export const MistralPlugin = PluginV2.define({
id: PluginV2.ID.make("mistral"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/mistral") return
const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
evt.sdk = mod.createMistral(evt.options)
}),
}
}),
})
@@ -1,16 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const NvidiaPlugin = PluginV2.define({
id: PluginV2.ID.make("nvidia"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.make("nvidia")) return
evt.provider.options.headers["HTTP-Referer"] = "https://opencode.ai/"
evt.provider.options.headers["X-Title"] = "opencode"
}),
}
}),
})
@@ -1,17 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
export const OpenAICompatiblePlugin = PluginV2.define({
id: PluginV2.ID.make("openai-compatible"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.sdk) return
if (!evt.package.includes("@ai-sdk/openai-compatible")) return
if (evt.options.includeUsage !== false) evt.options.includeUsage = true
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
evt.sdk = mod.createOpenAICompatible(evt.options as any)
}),
}
}),
})
@@ -1,27 +0,0 @@
import { Effect } from "effect"
import { ModelV2 } from "../../model"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const OpenAIPlugin = PluginV2.define({
id: PluginV2.ID.make("openai"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/openai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
evt.sdk = mod.createOpenAI(evt.options)
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.openai) return
evt.language = evt.sdk.responses(evt.model.apiID)
}),
"model.update": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.openai) return
// OpenAIPlugin sends OpenAI models through Responses; this alias is a
// chat-completions-only model, so remove it only from OpenAI's catalog.
if (evt.model.id === ModelV2.ID.make("gpt-5-chat-latest")) evt.cancel = true
}),
}
}),
})
@@ -1,27 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const OpencodePlugin = PluginV2.define({
id: PluginV2.ID.make("opencode"),
effect: Effect.gen(function* () {
let hasKey = false
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.opencode) return
hasKey = Boolean(
process.env.OPENCODE_API_KEY ||
evt.provider.env.some((item) => process.env[item]) ||
evt.provider.options.aisdk.provider.apiKey ||
(evt.provider.enabled && evt.provider.enabled.via === "auth"),
)
if (!hasKey) evt.provider.options.aisdk.provider.apiKey = "public"
}),
"model.update": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.opencode) return
if (hasKey) return
if (evt.model.cost.some((item) => item.input > 0)) evt.cancel = true
}),
}
}),
})
@@ -1,29 +0,0 @@
import { Effect } from "effect"
import { ModelV2 } from "../../model"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const OpenRouterPlugin = PluginV2.define({
id: PluginV2.ID.make("openrouter"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.openrouter) return
evt.provider.options.headers["HTTP-Referer"] = "https://opencode.ai/"
evt.provider.options.headers["X-Title"] = "opencode"
}),
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@openrouter/ai-sdk-provider") return
const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
evt.sdk = mod.createOpenRouter(evt.options)
}),
"model.update": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.openrouter) return
// These are OpenRouter-specific OpenAI chat aliases that do not work on
// the generic path. Keep custom providers with matching IDs untouched.
if (evt.model.id === ModelV2.ID.make("gpt-5-chat-latest")) evt.cancel = true
if (evt.model.id === ModelV2.ID.make("openai/gpt-5-chat")) evt.cancel = true
}),
}
}),
})
@@ -1,15 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
export const PerplexityPlugin = PluginV2.define({
id: PluginV2.ID.make("perplexity"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/perplexity") return
const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
evt.sdk = mod.createPerplexity(evt.options)
}),
}
}),
})
@@ -1,44 +0,0 @@
import { Npm } from "../../npm"
import { Effect, Option } from "effect"
import { pathToFileURL } from "url"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const SapAICorePlugin = PluginV2.define({
id: PluginV2.ID.make("sap-ai-core"),
effect: Effect.gen(function* () {
const npm = yield* Npm.Service
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
const serviceKey =
process.env.AICORE_SERVICE_KEY ??
(typeof evt.options.serviceKey === "string" ? evt.options.serviceKey : undefined)
if (serviceKey && !process.env.AICORE_SERVICE_KEY) process.env.AICORE_SERVICE_KEY = serviceKey
const installedPath = evt.package.startsWith("file://")
? evt.package
: Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint)
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
const mod = yield* Effect.promise(async () => {
return (await import(
installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href
)) as Record<string, (options: any) => any>
}).pipe(Effect.orDie)
const match = Object.keys(mod).find((name) => name.startsWith("create"))
if (!match) throw new Error(`Package ${evt.package} has no provider factory export`)
evt.sdk = mod[match](
serviceKey
? { deploymentId: process.env.AICORE_DEPLOYMENT_ID, resourceGroup: process.env.AICORE_RESOURCE_GROUP }
: {},
)
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
evt.language = evt.sdk(evt.model.apiID)
}),
}
}),
})
@@ -1,15 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
export const TogetherAIPlugin = PluginV2.define({
id: PluginV2.ID.make("togetherai"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/togetherai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai"))
evt.sdk = mod.createTogetherAI(evt.options)
}),
}
}),
})
@@ -1,15 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
export const VenicePlugin = PluginV2.define({
id: PluginV2.ID.make("venice"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "venice-ai-sdk-provider") return
const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider"))
evt.sdk = mod.createVenice(evt.options)
}),
}
}),
})
@@ -1,21 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const VercelPlugin = PluginV2.define({
id: PluginV2.ID.make("vercel"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.make("vercel")) return
evt.provider.options.headers["http-referer"] = "https://opencode.ai/"
evt.provider.options.headers["x-title"] = "opencode"
}),
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/vercel") return
const mod = yield* Effect.promise(() => import("@ai-sdk/vercel"))
evt.sdk = mod.createVercel(evt.options)
}),
}
}),
})
-20
View File
@@ -1,20 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const XAIPlugin = PluginV2.define({
id: PluginV2.ID.make("xai"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/xai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
evt.sdk = mod.createXai(evt.options)
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
evt.language = evt.sdk.responses(evt.model.apiID)
}),
}
}),
})
@@ -1,16 +0,0 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"
export const ZenmuxPlugin = PluginV2.define({
id: PluginV2.ID.make("zenmux"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.make("zenmux")) return
evt.provider.options.headers["HTTP-Referer"] ??= "https://opencode.ai/"
evt.provider.options.headers["X-Title"] ??= "opencode"
}),
}
}),
})
-234
View File
@@ -1,234 +0,0 @@
import { Context, Duration, Effect, Fiber, Layer, Schema, Stream } from "effect"
import type { PlatformError } from "effect/PlatformError"
import { ChildProcess } from "effect/unstable/process"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { CrossSpawnSpawner } from "./cross-spawn-spawner"
export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()("AppProcessError", {
command: Schema.String,
exitCode: Schema.optional(Schema.Number),
stderr: Schema.optional(Schema.String),
cause: Schema.optional(Schema.Defect),
}) {}
export interface RunOptions {
readonly maxOutputBytes?: number
readonly maxErrorBytes?: number
readonly signal?: AbortSignal
readonly timeout?: Duration.Input
readonly stdin?: string | Uint8Array | Stream.Stream<Uint8Array, PlatformError>
}
export interface RunStreamOptions {
readonly signal?: AbortSignal
readonly includeStderr?: boolean
readonly okExitCodes?: ReadonlyArray<number>
readonly maxErrorBytes?: number
}
export interface RunResult {
readonly command: string
readonly exitCode: number
readonly stdout: Buffer
readonly stderr: Buffer
readonly stdoutTruncated: boolean
readonly stderrTruncated: boolean
}
export type Interface = ChildProcessSpawner["Service"] & {
readonly run: (command: ChildProcess.Command, options?: RunOptions) => Effect.Effect<RunResult, AppProcessError>
readonly runStream: (
command: ChildProcess.Command,
options?: RunStreamOptions,
) => Stream.Stream<string, AppProcessError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/AppProcess") {}
export const requireSuccess = (result: RunResult): Effect.Effect<RunResult, AppProcessError> =>
result.exitCode === 0
? Effect.succeed(result)
: Effect.fail(
new AppProcessError({
command: result.command,
exitCode: result.exitCode,
stderr: result.stderr.toString("utf8"),
}),
)
export const requireExitIn =
(codes: ReadonlyArray<number>) =>
(result: RunResult): Effect.Effect<RunResult, AppProcessError> =>
codes.includes(result.exitCode)
? Effect.succeed(result)
: Effect.fail(
new AppProcessError({
command: result.command,
exitCode: result.exitCode,
stderr: result.stderr.toString("utf8"),
}),
)
const describeCommand = (command: ChildProcess.Command): string => {
if (command._tag === "StandardCommand") {
return command.args.length ? `${command.command} ${command.args.join(" ")}` : command.command
}
return `${describeCommand(command.left)} | ${describeCommand(command.right)}`
}
const wrapError = (description: string, cause: unknown): AppProcessError =>
cause instanceof AppProcessError ? cause : new AppProcessError({ command: description, cause })
const abortError = (signal: AbortSignal): Error => {
const reason = signal.reason
if (reason instanceof Error) return reason
const err = new Error("Aborted")
err.name = "AbortError"
return err
}
const waitForAbort = (signal: AbortSignal) =>
Effect.callback<never, Error>((resume) => {
if (signal.aborted) {
resume(Effect.fail(abortError(signal)))
return
}
const onabort = () => resume(Effect.fail(abortError(signal)))
signal.addEventListener("abort", onabort, { once: true })
return Effect.sync(() => signal.removeEventListener("abort", onabort))
})
const normalizeStdin = (
input: string | Uint8Array | Stream.Stream<Uint8Array, PlatformError>,
): Stream.Stream<Uint8Array, PlatformError> =>
typeof input === "string"
? Stream.make(new TextEncoder().encode(input))
: input instanceof Uint8Array
? Stream.make(input)
: input
const collectStream = (stream: Stream.Stream<Uint8Array, PlatformError>, maxOutputBytes: number | undefined) =>
Stream.runFold(
stream,
() => ({ chunks: [] as Uint8Array[], bytes: 0, truncated: false }),
(acc, chunk) => {
if (maxOutputBytes === undefined) {
acc.chunks.push(chunk)
acc.bytes += chunk.length
return acc
}
const remaining = maxOutputBytes - acc.bytes
if (remaining > 0) acc.chunks.push(remaining >= chunk.length ? chunk : chunk.slice(0, remaining))
acc.bytes += chunk.length
acc.truncated = acc.truncated || acc.bytes > maxOutputBytes
return acc
},
).pipe(Effect.map((x) => ({ buffer: Buffer.concat(x.chunks), truncated: x.truncated })))
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner
const runCommand = (command: ChildProcess.Command, options?: RunOptions) => {
const description = describeCommand(command)
const collect = Effect.scoped(
Effect.gen(function* () {
const handle = yield* spawner.spawn(command)
const [stdout, stderr, exitCode] = yield* Effect.all(
[
collectStream(handle.stdout, options?.maxOutputBytes),
collectStream(handle.stderr, options?.maxErrorBytes),
handle.exitCode,
],
{ concurrency: "unbounded" },
)
return {
command: description,
exitCode,
stdout: stdout.buffer,
stderr: stderr.buffer,
stdoutTruncated: stdout.truncated,
stderrTruncated: stderr.truncated,
} satisfies RunResult
}),
)
const timed = options?.timeout
? Effect.timeoutOrElse(collect, {
duration: options.timeout,
orElse: () => Effect.fail(new AppProcessError({ command: description, cause: new Error("Timed out") })),
})
: collect
const aborted = options?.signal
? timed.pipe(
Effect.raceFirst(
waitForAbort(options.signal).pipe(Effect.mapError((cause) => wrapError(description, cause))),
),
)
: timed
return aborted.pipe(Effect.catch((cause) => Effect.fail(wrapError(description, cause))))
}
const run = Effect.fn("AppProcess.run")(function* (command: ChildProcess.Command, options?: RunOptions) {
if (options?.stdin === undefined) return yield* runCommand(command, options)
if (command._tag !== "StandardCommand") {
return yield* new AppProcessError({
command: describeCommand(command),
cause: new Error("stdin option only supports StandardCommand; received PipedCommand"),
})
}
const next = ChildProcess.make(command.command, command.args, {
...command.options,
stdin: normalizeStdin(options.stdin),
})
return yield* runCommand(next, options)
})
const runStream = (
command: ChildProcess.Command,
options?: RunStreamOptions,
): Stream.Stream<string, AppProcessError> => {
const description = describeCommand(command)
const okExitCodes = options?.okExitCodes
const built: Stream.Stream<string, AppProcessError | PlatformError> = Stream.unwrap(
Effect.gen(function* () {
const handle = yield* spawner.spawn(command)
const stderrFiber = yield* Effect.forkScoped(
collectStream(handle.stderr, options?.maxErrorBytes).pipe(Effect.map((x) => x.buffer.toString("utf8"))),
)
const source = options?.includeStderr === true ? handle.all : handle.stdout
const lines = source.pipe(
Stream.decodeText,
Stream.splitLines,
Stream.filter((line) => line.length > 0),
)
const tail = Stream.unwrap(
Effect.gen(function* () {
const code = yield* handle.exitCode
if (okExitCodes && okExitCodes.length > 0 && !okExitCodes.includes(code)) {
const stderr = yield* Fiber.join(stderrFiber)
return Stream.fail(new AppProcessError({ command: description, exitCode: code, stderr }))
}
return Stream.empty
}),
)
return Stream.concat(lines, tail) as Stream.Stream<string, AppProcessError | PlatformError>
}),
)
const mapped = built.pipe(
Stream.catch((cause): Stream.Stream<string, AppProcessError> => Stream.fail(wrapError(description, cause))),
)
if (!options?.signal) return mapped
const signal = options.signal
return mapped.pipe(
Stream.interruptWhen(waitForAbort(signal).pipe(Effect.mapError((cause) => wrapError(description, cause)))),
)
}
return Service.of({ ...spawner, run, runStream })
}),
)
export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
export * as AppProcess from "./process"
-120
View File
@@ -1,120 +0,0 @@
export * as ProviderV2 from "./provider"
import { withStatics } from "./schema"
import { Schema } from "effect"
export const ID = Schema.String.pipe(
Schema.brand("ProviderV2.ID"),
withStatics((schema) => ({
// Well-known providers
opencode: schema.make("opencode"),
anthropic: schema.make("anthropic"),
openai: schema.make("openai"),
google: schema.make("google"),
googleVertex: schema.make("google-vertex"),
githubCopilot: schema.make("github-copilot"),
amazonBedrock: schema.make("amazon-bedrock"),
azure: schema.make("azure"),
openrouter: schema.make("openrouter"),
mistral: schema.make("mistral"),
gitlab: schema.make("gitlab"),
})),
)
export type ID = typeof ID.Type
const OpenAIResponses = Schema.Struct({
type: Schema.Literal("openai/responses"),
url: Schema.String,
websocket: Schema.optional(Schema.Boolean),
})
const OpenAICompletions = Schema.Struct({
type: Schema.Literal("openai/completions"),
url: Schema.String,
reasoning: Schema.Union([
Schema.Struct({
type: Schema.Literal("reasoning_content"),
}),
Schema.Struct({
type: Schema.Literal("reasoning_details"),
}),
]).pipe(Schema.optional),
})
export type OpenAICompletions = typeof OpenAICompletions.Type
const AISDK = Schema.Struct({
type: Schema.Literal("aisdk"),
package: Schema.String,
url: Schema.String.pipe(Schema.optional),
})
const AnthropicMessages = Schema.Struct({
type: Schema.Literal("anthropic/messages"),
url: Schema.String,
})
const UnknownEndpoint = Schema.Struct({
type: Schema.Literal("unknown"),
})
export const Endpoint = Schema.Union([
UnknownEndpoint,
OpenAIResponses,
OpenAICompletions,
AnthropicMessages,
AISDK,
]).pipe(Schema.toTaggedUnion("type"))
export type Endpoint = typeof Endpoint.Type
export const Options = Schema.Struct({
headers: Schema.Record(Schema.String, Schema.String),
body: Schema.Record(Schema.String, Schema.Any),
aisdk: Schema.Struct({
provider: Schema.Record(Schema.String, Schema.Any),
request: Schema.Record(Schema.String, Schema.Any),
}),
})
export type Options = typeof Options.Type
export class Info extends Schema.Class<Info>("ProviderV2.Info")({
id: ID,
name: Schema.String,
enabled: Schema.Union([
Schema.Literal(false),
Schema.Struct({
via: Schema.Literal("env"),
name: Schema.String,
}),
Schema.Struct({
via: Schema.Literal("auth"),
service: Schema.String,
}),
Schema.Struct({
via: Schema.Literal("custom"),
data: Schema.Record(Schema.String, Schema.Any),
}),
]),
env: Schema.String.pipe(Schema.Array),
endpoint: Endpoint,
options: Options,
}) {
static empty(providerID: ID) {
return new Info({
id: providerID,
name: providerID,
enabled: false,
env: [],
endpoint: {
type: "unknown",
},
options: {
headers: {},
body: {},
aisdk: {
provider: {},
request: {},
},
},
})
}
}
+1 -5
View File
@@ -20,7 +20,6 @@ const levelPriority: Record<Level, number> = {
ERROR: 3,
}
const keep = 10
const initializedRunID = "OPENCODE_LOG_INITIALIZED_RUN_ID"
let level: Level = "INFO"
@@ -71,10 +70,7 @@ export async function init(options: Options) {
Global.Path.log,
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
)
const runID = process.env.OPENCODE_RUN_ID
const shouldTruncate = !options.dev || !runID || process.env[initializedRunID] !== runID
if (shouldTruncate) await fs.truncate(logpath).catch(() => {})
if (options.dev && runID) process.env[initializedRunID] = runID
await fs.truncate(logpath).catch(() => {})
const stream = createWriteStream(logpath, { flags: "a" })
write = async (msg: any) => {
return new Promise((resolve, reject) => {
-201
View File
@@ -1,201 +0,0 @@
import { describe, expect } from "bun:test"
import { DateTime, Effect, Layer, Option } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Instance } from "@opencode-ai/core/instance"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "./lib/effect"
const instanceLayer = Layer.succeed(Instance.Service, Instance.Service.of({ directory: "test" }))
const it = testEffect(Catalog.layer.pipe(Layer.provideMerge(PluginV2.defaultLayer), Layer.provide(instanceLayer)))
describe("CatalogV2", () => {
it.effect("normalizes provider baseURL into endpoint url", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
yield* catalog.provider.update(providerID, (provider) => {
provider.endpoint = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://default.example.com",
}
provider.options.aisdk.provider.baseURL = "https://override.example.com"
})
const provider = yield* catalog.provider.get(providerID)
expect(provider.endpoint).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://override.example.com",
})
expect(provider.options.aisdk.provider.baseURL).toBeUndefined()
}),
)
it.effect("normalizes model baseURL into endpoint url", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model")
yield* catalog.provider.update(providerID, (provider) => {
provider.endpoint = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
}
})
yield* catalog.model.update(providerID, modelID, (model) => {
model.endpoint = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://model.example.com",
}
model.options.aisdk.provider.baseURL = "https://override.example.com"
})
const model = yield* catalog.model.get(providerID, modelID)
expect(model.endpoint).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://override.example.com",
})
expect(model.options.aisdk.provider.baseURL).toBeUndefined()
}),
)
it.effect("resolves unknown model endpoint from provider endpoint", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model")
yield* catalog.provider.update(providerID, (provider) => {
provider.endpoint = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
}
})
yield* catalog.model.update(providerID, modelID, () => {})
const model = yield* catalog.model.get(providerID, modelID)
expect(model.endpoint).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
})
}),
)
it.effect("runs provider hooks after baseURL is normalized", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const plugin = yield* PluginV2.Service
const providerID = ProviderV2.ID.make("test")
const seen: unknown[] = []
yield* plugin.add({
id: PluginV2.ID.make("test"),
effect: Effect.succeed({
"provider.update": (evt) =>
Effect.sync(() => {
seen.push(evt.provider.endpoint.type)
if (evt.provider.endpoint.type === "aisdk") seen.push(evt.provider.endpoint.url)
seen.push(evt.provider.options.aisdk.provider.baseURL)
}),
}),
})
yield* catalog.provider.update(providerID, (provider) => {
provider.endpoint = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
}
provider.options.aisdk.provider.baseURL = "https://provider.example.com"
})
expect(seen).toEqual(["aisdk", "https://provider.example.com", undefined])
}),
)
it.effect("resolves provider and model option merges", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model")
yield* catalog.provider.update(providerID, (provider) => {
provider.options.headers.provider = "provider"
provider.options.headers.shared = "provider"
provider.options.body.provider = true
provider.options.aisdk.provider.provider = true
})
yield* catalog.model.update(providerID, modelID, (model) => {
model.options.headers.model = "model"
model.options.headers.shared = "model"
model.options.body.model = true
model.options.aisdk.provider.model = true
model.options.aisdk.request.request = true
})
const model = yield* catalog.model.get(providerID, modelID)
expect(model.options.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
expect(model.options.body).toEqual({ provider: true, model: true })
expect(model.options.aisdk.provider).toEqual({ provider: true, model: true })
expect(model.options.aisdk.request).toEqual({ request: true })
}),
)
it.effect("falls back to newest available model when no default is configured", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
yield* catalog.provider.update(providerID, (provider) => {
provider.enabled = { via: "custom", data: {} }
})
yield* catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => {
model.time.released = DateTime.makeUnsafe(1000)
})
yield* catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => {
model.time.released = DateTime.makeUnsafe(2000)
})
const model = yield* catalog.model.default()
expect(Option.getOrUndefined(model)?.id).toMatch("new")
}),
)
it.effect("small model prefers small keyword candidates before cost scoring", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
yield* catalog.provider.update(providerID, () => {})
yield* catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => {
model.capabilities.input = ["text"]
model.capabilities.output = ["text"]
model.cost = [{ input: 1, output: 1, cache: { read: 0, write: 0 } }]
model.time.released = DateTime.makeUnsafe(Date.now())
})
yield* catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => {
model.capabilities.input = ["text"]
model.capabilities.output = ["text"]
model.cost = [{ input: 10, output: 10, cache: { read: 0, write: 0 } }]
model.time.released = DateTime.makeUnsafe(Date.now())
})
const model = yield* catalog.model.small(providerID)
expect(Option.getOrUndefined(model)?.id).toMatch("expensive-mini")
}),
)
})
@@ -1,9 +0,0 @@
export function createFixtureProvider(options: Record<string, unknown>) {
const captured = Object.fromEntries(Object.entries(options))
return Object.assign((modelID: string) => ({ modelID, options: captured }), {
options: captured,
languageModel(modelID: string) {
return { modelID, options: captured }
},
})
}
@@ -1,67 +0,0 @@
import { describe, expect } from "bun:test"
import { createAlibaba } from "@ai-sdk/alibaba"
import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba"
import { it, model } from "./provider-helper"
describe("AlibabaPlugin", () => {
it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AlibabaPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("alibaba", "qwen"), package: "@ai-sdk/alibaba", options: { name: "alibaba" } },
{},
)
expect(result.sdk).toBeDefined()
}),
)
it.effect("ignores non-Alibaba SDK packages", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AlibabaPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("alibaba", "qwen"), package: "@ai-sdk/openai-compatible", options: { name: "alibaba" } },
{},
)
expect(result.sdk).toBeUndefined()
}),
)
it.effect("matches the old bundled Alibaba SDK provider naming", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AlibabaPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("custom-alibaba", "qwen"),
package: "@ai-sdk/alibaba",
options: { name: "custom-alibaba", apiKey: "test" },
},
{},
)
const expected = createAlibaba({ apiKey: "test", ...{ name: "custom-alibaba" } }).languageModel("qwen")
const actual = result.sdk?.languageModel("qwen")
expect(actual?.provider).toBe(expected.provider)
expect(actual?.modelId).toBe(expected.modelId)
}),
)
it.effect("uses the old default languageModel(apiID) behavior", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AlibabaPlugin)
const item = model("alibaba", "alias", { apiID: ModelV2.ID.make("qwen-plus") })
const result = yield* plugin.trigger("aisdk.sdk", { model: item, package: "@ai-sdk/alibaba", options: {} }, {})
const language = result.sdk?.languageModel(item.apiID)
expect(language?.modelId).toBe("qwen-plus")
expect(language?.provider).toBe("alibaba.chat")
}),
)
})
@@ -1,465 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock"
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID)
return (language as { config: { baseUrl: () => string } }).config.baseUrl()
}
function bedrockFetch(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID)
return (
language as { config: { fetch: (input: Parameters<typeof fetch>[0], init?: RequestInit) => Promise<Response> } }
).config.fetch
}
describe("AmazonBedrockPlugin", () => {
it.effect("moves endpoint option to endpoint URL", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("amazon-bedrock", {
options: {
headers: {},
body: {},
aisdk: { provider: { endpoint: "https://bedrock.example" }, request: {} },
},
}),
cancel: false,
},
)
expect(result.provider.endpoint).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://bedrock.example",
})
expect(result.provider.options.aisdk.provider.endpoint).toBeUndefined()
}),
)
it.effect("prefers endpoint over baseURL for SDK base URL", () =>
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
package: "@ai-sdk/amazon-bedrock",
options: {
name: "amazon-bedrock",
bearerToken: "token",
baseURL: "https://base.example",
endpoint: "https://endpoint.example",
region: "us-east-1",
},
},
{},
)
expect(bedrockBaseURL(result.sdk)).toBe("https://endpoint.example")
}),
),
)
it.effect("uses baseURL as SDK base URL", () =>
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
package: "@ai-sdk/amazon-bedrock",
options: {
name: "amazon-bedrock",
bearerToken: "token",
baseURL: "https://base.example",
region: "us-east-1",
},
},
{},
)
expect(bedrockBaseURL(result.sdk)).toBe("https://base.example")
}),
),
)
it.effect("creates SDK without explicit credential env so the default AWS chain can resolve credentials", () =>
withEnv(
{
AWS_ACCESS_KEY_ID: undefined,
AWS_BEARER_TOKEN_BEDROCK: undefined,
AWS_CONTAINER_CREDENTIALS_FULL_URI: undefined,
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: undefined,
AWS_PROFILE: undefined,
AWS_REGION: undefined,
AWS_WEB_IDENTITY_TOKEN_FILE: undefined,
},
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" },
},
{},
)
expect(result.sdk).toBeDefined()
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com")
}),
),
)
it.effect("uses config region over AWS_REGION for SDK base URL", () =>
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock", region: "eu-west-1" },
},
{},
)
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com")
}),
),
)
it.effect("uses AWS_REGION for SDK base URL when config region is absent", () =>
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" },
},
{},
)
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com")
}),
),
)
it.effect("defaults SDK region to us-east-1", () =>
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" },
},
{},
)
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com")
}),
),
)
it.effect("loads bearer token option into env and uses bearer auth", () =>
withEnv({ AWS_ACCESS_KEY_ID: undefined, AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const headers: Array<string | null> = []
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
package: "@ai-sdk/amazon-bedrock",
options: {
name: "amazon-bedrock",
bearerToken: "option-token",
fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
headers.push(new Headers(init?.headers).get("Authorization"))
return new Response("{}")
},
},
},
{},
)
yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" }))
expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("option-token")
expect(headers).toEqual(["Bearer option-token"])
}),
),
)
it.effect("prefers bearer token env over bearer token option", () =>
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "env-token" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const headers: Array<string | null> = []
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
package: "@ai-sdk/amazon-bedrock",
options: {
name: "amazon-bedrock",
bearerToken: "option-token",
fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
headers.push(new Headers(init?.headers).get("Authorization"))
return new Response("{}")
},
},
},
{},
)
yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" }))
expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("env-token")
expect(headers).toEqual(["Bearer env-token"])
}),
),
)
it.effect("uses SigV4 credential env when bearer token is absent", () =>
withEnv(
{
AWS_ACCESS_KEY_ID: "test-access-key",
AWS_BEARER_TOKEN_BEDROCK: undefined,
AWS_REGION: "us-east-1",
AWS_SECRET_ACCESS_KEY: "test-secret-key",
AWS_SESSION_TOKEN: "test-session-token",
},
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const headers: Array<string | null> = []
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
package: "@ai-sdk/amazon-bedrock",
options: {
name: "amazon-bedrock",
fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
headers.push(new Headers(init?.headers).get("Authorization"))
return new Response("{}")
},
},
},
{},
)
yield* Effect.promise(() =>
bedrockFetch(result.sdk)("https://bedrock-runtime.us-east-1.amazonaws.com/model/test/invoke", {
body: "{}",
method: "POST",
}),
)
expect(headers[0]?.startsWith("AWS4-HMAC-SHA256 ")).toBe(true)
}),
),
)
it.effect("applies legacy cross-region inference prefixes", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AmazonBedrockPlugin)
yield* plugin.trigger(
"aisdk.language",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
},
{},
)
yield* plugin.trigger(
"aisdk.language",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" },
},
{},
)
yield* plugin.trigger(
"aisdk.language",
{
model: model("amazon-bedrock", "global.anthropic.claude-sonnet-4-5"),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" },
},
{},
)
yield* plugin.trigger(
"aisdk.language",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "ap-northeast-1" },
},
{},
)
yield* plugin.trigger(
"aisdk.language",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "ap-southeast-2" },
},
{},
)
expect(calls).toEqual([
"languageModel:us.anthropic.claude-sonnet-4-5",
"languageModel:eu.anthropic.claude-sonnet-4-5",
"languageModel:global.anthropic.claude-sonnet-4-5",
"languageModel:jp.anthropic.claude-sonnet-4-5",
"languageModel:au.anthropic.claude-sonnet-4-5",
])
}),
)
it.effect("uses AWS_REGION for language prefixes when region option is absent", () =>
withEnv({ AWS_REGION: "eu-west-1" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AmazonBedrockPlugin)
yield* plugin.trigger(
"aisdk.language",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
},
{},
)
expect(calls).toEqual(["languageModel:eu.anthropic.claude-sonnet-4-5"])
}),
),
)
it.effect("applies the full legacy cross-region prefix matrix", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
const cases = [
{ region: "us-east-1", modelID: "amazon.nova-micro-v1:0", expected: "us.amazon.nova-micro-v1:0" },
{ region: "us-east-1", modelID: "amazon.nova-lite-v1:0", expected: "us.amazon.nova-lite-v1:0" },
{ region: "us-east-1", modelID: "amazon.nova-pro-v1:0", expected: "us.amazon.nova-pro-v1:0" },
{ region: "us-east-1", modelID: "amazon.nova-premier-v1:0", expected: "us.amazon.nova-premier-v1:0" },
{ region: "us-east-1", modelID: "amazon.nova-2-lite-v1:0", expected: "us.amazon.nova-2-lite-v1:0" },
{ region: "us-east-1", modelID: "anthropic.claude-sonnet-4-5", expected: "us.anthropic.claude-sonnet-4-5" },
{ region: "us-east-1", modelID: "deepseek.r1-v1:0", expected: "us.deepseek.r1-v1:0" },
{ region: "us-gov-west-1", modelID: "anthropic.claude-sonnet-4-5", expected: "anthropic.claude-sonnet-4-5" },
{ region: "us-east-1", modelID: "cohere.command-r-plus-v1:0", expected: "cohere.command-r-plus-v1:0" },
{ region: "eu-west-1", modelID: "anthropic.claude-sonnet-4-5", expected: "eu.anthropic.claude-sonnet-4-5" },
{ region: "eu-west-2", modelID: "amazon.nova-lite-v1:0", expected: "eu.amazon.nova-lite-v1:0" },
{ region: "eu-west-3", modelID: "amazon.nova-micro-v1:0", expected: "eu.amazon.nova-micro-v1:0" },
{
region: "eu-north-1",
modelID: "meta.llama3-70b-instruct-v1:0",
expected: "eu.meta.llama3-70b-instruct-v1:0",
},
{ region: "eu-central-1", modelID: "mistral.pixtral-large-v1:0", expected: "eu.mistral.pixtral-large-v1:0" },
{ region: "eu-south-1", modelID: "anthropic.claude-sonnet-4-5", expected: "eu.anthropic.claude-sonnet-4-5" },
{ region: "eu-south-2", modelID: "anthropic.claude-sonnet-4-5", expected: "eu.anthropic.claude-sonnet-4-5" },
{ region: "eu-central-2", modelID: "anthropic.claude-sonnet-4-5", expected: "anthropic.claude-sonnet-4-5" },
{ region: "eu-west-1", modelID: "cohere.command-r-plus-v1:0", expected: "cohere.command-r-plus-v1:0" },
{
region: "ap-southeast-2",
modelID: "anthropic.claude-sonnet-4-5",
expected: "au.anthropic.claude-sonnet-4-5",
},
{
region: "ap-southeast-4",
modelID: "anthropic.claude-haiku-v1:0",
expected: "au.anthropic.claude-haiku-v1:0",
},
{ region: "ap-southeast-2", modelID: "anthropic.claude-opus-4", expected: "apac.anthropic.claude-opus-4" },
{
region: "ap-northeast-1",
modelID: "anthropic.claude-sonnet-4-5",
expected: "jp.anthropic.claude-sonnet-4-5",
},
{ region: "ap-northeast-1", modelID: "amazon.nova-pro-v1:0", expected: "jp.amazon.nova-pro-v1:0" },
{ region: "ap-south-1", modelID: "anthropic.claude-sonnet-4-5", expected: "apac.anthropic.claude-sonnet-4-5" },
{ region: "ap-south-1", modelID: "amazon.nova-lite-v1:0", expected: "apac.amazon.nova-lite-v1:0" },
{ region: "ca-central-1", modelID: "anthropic.claude-sonnet-4-5", expected: "anthropic.claude-sonnet-4-5" },
{
region: "us-east-1",
modelID: "global.anthropic.claude-sonnet-4-5",
expected: "global.anthropic.claude-sonnet-4-5",
},
{ region: "us-east-1", modelID: "us.anthropic.claude-sonnet-4-5", expected: "us.anthropic.claude-sonnet-4-5" },
{ region: "eu-west-1", modelID: "eu.anthropic.claude-sonnet-4-5", expected: "eu.anthropic.claude-sonnet-4-5" },
{
region: "ap-northeast-1",
modelID: "jp.anthropic.claude-sonnet-4-5",
expected: "jp.anthropic.claude-sonnet-4-5",
},
{
region: "ap-south-1",
modelID: "apac.anthropic.claude-sonnet-4-5",
expected: "apac.anthropic.claude-sonnet-4-5",
},
{
region: "ap-southeast-2",
modelID: "au.anthropic.claude-sonnet-4-5",
expected: "au.anthropic.claude-sonnet-4-5",
},
]
yield* plugin.add(AmazonBedrockPlugin)
for (const item of cases) {
yield* plugin.trigger(
"aisdk.language",
{
model: model("amazon-bedrock", item.modelID),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: item.region },
},
{},
)
}
expect(calls).toEqual(cases.map((item) => `languageModel:${item.expected}`))
}),
)
it.effect("ignores non-Bedrock providers for language selection", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
model: model("openai", "anthropic.claude-sonnet-4-5"),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" },
},
{},
)
expect(calls).toEqual([])
expect(result.language).toBeUndefined()
}),
)
})
@@ -1,91 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic"
import { it, model, provider } from "./provider-helper"
describe("AnthropicPlugin", () => {
it.effect("applies legacy beta headers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AnthropicPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("anthropic", {
options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } },
}),
cancel: false,
},
)
expect(result.provider.options.headers["anthropic-beta"]).toBe(
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
)
expect(result.provider.options.headers.Existing).toBe("1")
}),
)
it.effect("ignores non-Anthropic providers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AnthropicPlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("openai"), cancel: false })
expect(result.provider.options.headers["anthropic-beta"]).toBeUndefined()
}),
)
it.effect("creates Anthropic SDKs with the model provider ID as the SDK name", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const providers: string[] = []
yield* plugin.add(AnthropicPlugin)
yield* plugin.add({
id: PluginV2.ID.make("anthropic-sdk-inspector"),
effect: Effect.succeed({
"aisdk.sdk": (evt) =>
Effect.sync(() => {
providers.push(evt.sdk.languageModel("claude-sonnet-4-5").provider)
}),
}),
})
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("custom-anthropic", "claude-sonnet-4-5"),
package: "@ai-sdk/anthropic",
options: { name: "custom-anthropic", apiKey: "test" },
},
{},
)
expect(providers).toEqual(["custom-anthropic"])
}),
)
it.effect("uses the Anthropic provider ID as the SDK name for the bundled Anthropic provider", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const providers: string[] = []
yield* plugin.add(AnthropicPlugin)
yield* plugin.add({
id: PluginV2.ID.make("anthropic-sdk-inspector"),
effect: Effect.succeed({
"aisdk.sdk": (evt) =>
Effect.sync(() => {
providers.push(evt.sdk.languageModel("claude-sonnet-4-5").provider)
}),
}),
})
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("anthropic", "claude-sonnet-4-5"),
package: "@ai-sdk/anthropic",
options: { name: "anthropic", apiKey: "test" },
},
{},
)
expect(providers).toEqual(["anthropic"])
}),
)
})
@@ -1,127 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure"
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
describe("AzureCognitiveServicesPlugin", () => {
it.effect("maps the resource env var to the Azure SDK baseURL", () =>
withEnv({ AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: "cognitive" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AzureCognitiveServicesPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{ provider: provider("azure-cognitive-services"), cancel: false },
)
expect(result.provider.endpoint).toEqual({
type: "aisdk",
package: "test-provider",
})
expect(result.provider.options.aisdk.provider.baseURL).toBe(
"https://cognitive.cognitiveservices.azure.com/openai",
)
expect(result.provider.options.aisdk.provider.resourceName).toBeUndefined()
}),
),
)
it.effect("leaves baseURL unset without resource env and ignores other providers", () =>
withEnv({ AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AzureCognitiveServicesPlugin)
const azure = yield* plugin.trigger(
"provider.update",
{},
{ provider: provider("azure-cognitive-services"), cancel: false },
)
const other = yield* plugin.trigger("provider.update", {}, { provider: provider("openai"), cancel: false })
expect(azure.provider.options.aisdk.provider.baseURL).toBeUndefined()
expect(azure.provider.endpoint).toEqual({ type: "aisdk", package: "test-provider" })
expect(other.provider.options.aisdk.provider.baseURL).toBeUndefined()
expect(other.provider.endpoint).toEqual({ type: "aisdk", package: "test-provider" })
}),
),
)
it.effect("selects chat only for completion URLs", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AzureCognitiveServicesPlugin)
yield* plugin.trigger(
"aisdk.language",
{
model: model("azure-cognitive-services", "deployment"),
sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true },
},
{},
)
expect(calls).toEqual(["chat:deployment"])
}),
)
it.effect("uses the legacy Azure selector order and provider guard", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AzureCognitiveServicesPlugin)
yield* plugin.trigger(
"aisdk.language",
{ model: model("azure-cognitive-services", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
{},
)
const ignored = yield* plugin.trigger(
"aisdk.language",
{ model: model("openai", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
{},
)
expect(calls).toEqual(["responses:deployment"])
expect(ignored.language).toBeUndefined()
}),
)
it.effect("falls back from responses to messages, chat, then languageModel", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
const sdk = fakeSelectorSdk(calls)
yield* plugin.add(AzureCognitiveServicesPlugin)
yield* plugin.trigger(
"aisdk.language",
{
model: model("azure-cognitive-services", "messages-deployment"),
sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel },
options: {},
},
{},
)
yield* plugin.trigger(
"aisdk.language",
{
model: model("azure-cognitive-services", "chat-deployment"),
sdk: { chat: sdk.chat, languageModel: sdk.languageModel },
options: {},
},
{},
)
yield* plugin.trigger(
"aisdk.language",
{
model: model("azure-cognitive-services", "language-deployment"),
sdk: { languageModel: sdk.languageModel },
options: {},
},
{},
)
expect(calls).toEqual([
"messages:messages-deployment",
"chat:chat-deployment",
"languageModel:language-deployment",
])
}),
)
})
@@ -1,245 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { AuthV2 } from "@opencode-ai/core/auth"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AuthPlugin } from "@opencode-ai/core/plugin/auth"
import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
import { testEffect } from "../lib/effect"
import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper"
const itWithAuth = testEffect(Layer.mergeAll(PluginV2.defaultLayer, AuthV2.defaultLayer, npmLayer))
describe("AzurePlugin", () => {
it.effect("resolves resourceName from env", () =>
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AzurePlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("azure"), cancel: false })
expect(result.provider.options.aisdk.provider.resourceName).toBe("from-env")
}),
),
)
it.effect("keeps explicit resourceName over env and ignores other providers", () =>
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AzurePlugin)
const azure = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("azure", {
options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "from-config" }, request: {} } },
}),
cancel: false,
},
)
const other = yield* plugin.trigger("provider.update", {}, { provider: provider("openai"), cancel: false })
expect(azure.provider.options.aisdk.provider.resourceName).toBe("from-config")
expect(other.provider.options.aisdk.provider.resourceName).toBeUndefined()
}),
),
)
itWithAuth.effect("prefers auth resourceName over env", () =>
withEnv(
{
AZURE_RESOURCE_NAME: "from-env",
},
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const auth = yield* AuthV2.Service
yield* auth.create({
serviceID: AuthV2.ServiceID.make("azure"),
credential: new AuthV2.ApiKeyCredential({
type: "api",
key: "key",
metadata: { resourceName: "from-auth" },
}),
active: true,
})
yield* plugin.add({
...AuthPlugin,
effect: AuthPlugin.effect.pipe(Effect.provideService(AuthV2.Service, auth)),
})
yield* plugin.add(AzurePlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("azure"), cancel: false })
expect(result.provider.options.aisdk.provider.resourceName).toBe("from-auth")
}),
),
)
it.effect("falls back to env when configured resourceName is blank", () =>
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AzurePlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("azure", {
options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "" }, request: {} } },
}),
cancel: false,
},
)
expect(result.provider.options.aisdk.provider.resourceName).toBe("from-env")
}),
),
)
it.effect("falls back to env when configured resourceName is whitespace", () =>
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AzurePlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("azure", {
options: { headers: {}, body: {}, aisdk: { provider: { resourceName: " " }, request: {} } },
}),
cancel: false,
},
)
expect(result.provider.options.aisdk.provider.resourceName).toBe("from-env")
}),
),
)
it.effect("allows configured baseURL without resourceName", () =>
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AzurePlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("azure", "deployment"),
package: "@ai-sdk/azure",
options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
},
{},
)
expect(result.sdk).toBeDefined()
}),
),
)
it.effect("rejects missing resourceName when baseURL is not configured", () =>
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AzurePlugin)
const exit = yield* plugin
.trigger(
"aisdk.sdk",
{ model: model("azure", "deployment"), package: "@ai-sdk/azure", options: { name: "azure" } },
{},
)
.pipe(Effect.exit)
expect(exit._tag).toBe("Failure")
}),
),
)
it.effect("selects chat only for completion URLs", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AzurePlugin)
yield* plugin.trigger(
"aisdk.language",
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
{},
)
expect(calls).toEqual(["chat:deployment"])
}),
)
it.effect("selects chat from per-call useCompletionUrls", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AzurePlugin)
yield* plugin.trigger(
"aisdk.language",
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
{},
)
expect(calls).toEqual(["chat:deployment"])
}),
)
it.effect("ignores model useCompletionUrls when per-call option is unset", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AzurePlugin)
yield* plugin.trigger(
"aisdk.language",
{
model: model("azure", "deployment", {
options: { headers: {}, body: {}, aisdk: { provider: {}, request: { useCompletionUrls: true } } },
}),
sdk: fakeSelectorSdk(calls),
options: {},
},
{},
)
expect(calls).toEqual(["responses:deployment"])
}),
)
it.effect("uses the legacy Azure selector order and provider guard", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AzurePlugin)
yield* plugin.trigger(
"aisdk.language",
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
{},
)
const ignored = yield* plugin.trigger(
"aisdk.language",
{ model: model("openai", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
{},
)
expect(calls).toEqual(["responses:deployment"])
expect(ignored.language).toBeUndefined()
}),
)
it.effect("falls back through the legacy Azure selector order", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
const make = (method: string) => (id: string) => {
calls.push(`${method}:${id}`)
return { modelId: id, provider: method, specificationVersion: "v3" }
}
yield* plugin.add(AzurePlugin)
yield* plugin.trigger(
"aisdk.language",
{
model: model("azure", "messages-deployment"),
sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
options: {},
},
{},
)
yield* plugin.trigger(
"aisdk.language",
{ model: model("azure", "language-deployment"), sdk: { languageModel: make("languageModel") }, options: {} },
{},
)
expect(calls).toEqual(["messages:messages-deployment", "languageModel:language-deployment"])
}),
)
})
@@ -1,102 +0,0 @@
import { describe, expect, mock } from "bun:test"
import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras"
import { it, model, provider } from "./provider-helper"
const cerebrasOptions: Record<string, unknown>[] = []
void mock.module("@ai-sdk/cerebras", () => ({
createCerebras: (options: Record<string, unknown>) => {
const snapshot = { ...options }
cerebrasOptions.push(snapshot)
return {
languageModel: (modelID: string) => ({ modelID, provider: snapshot.name, specificationVersion: "v3" }),
}
},
}))
describe("CerebrasPlugin", () => {
it.effect("applies the legacy integration header", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CerebrasPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("cerebras", {
options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } },
}),
cancel: false,
},
)
expect(result.provider.options.headers).toEqual({ Existing: "1", "X-Cerebras-3rd-Party-Integration": "opencode" })
}),
)
it.effect("ignores non-Cerebras providers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CerebrasPlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("groq"), cancel: false })
expect(result.provider.options.headers).toEqual({})
}),
)
it.effect("creates a bundled Cerebras SDK with the model provider ID as the SDK name", () =>
Effect.gen(function* () {
cerebrasOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(CerebrasPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("custom-cerebras", "llama-4-scout-17b-16e-instruct"),
package: "@ai-sdk/cerebras",
options: { name: "custom-cerebras", apiKey: "test" },
},
{},
)
expect(cerebrasOptions).toEqual([{ name: "custom-cerebras", apiKey: "test" }])
expect(result.sdk.languageModel("llama-4-scout-17b-16e-instruct").provider).toBe("custom-cerebras")
}),
)
it.effect("preserves an explicit bundled Cerebras SDK name option", () =>
Effect.gen(function* () {
cerebrasOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(CerebrasPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("custom-cerebras", "llama-4-scout-17b-16e-instruct"),
package: "@ai-sdk/cerebras",
options: { name: "configured-cerebras", apiKey: "test" },
},
{},
)
expect(cerebrasOptions).toEqual([{ name: "configured-cerebras", apiKey: "test" }])
}),
)
it.effect("ignores non-Cerebras SDK packages", () =>
Effect.gen(function* () {
cerebrasOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(CerebrasPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("custom-cerebras", "llama-4-scout-17b-16e-instruct"),
package: "@ai-sdk/groq",
options: { name: "custom-cerebras", apiKey: "test" },
},
{},
)
expect(cerebrasOptions).toEqual([])
expect(result.sdk).toBeUndefined()
}),
)
})
@@ -1,384 +0,0 @@
import { describe, expect, mock } from "bun:test"
import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { CloudflareAIGatewayPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-ai-gateway"
import { it, model, withEnv } from "./provider-helper"
const aiGatewayCalls: Record<string, unknown>[] = []
const unifiedCalls: string[] = []
const gatewayModelCalls: unknown[] = []
function captureAiGatewayOptions(options: Record<string, unknown>) {
const nested =
options.options && typeof options.options === "object" ? (options.options as Record<string, unknown>) : undefined
return {
...options,
...(nested
? {
options: {
...nested,
headers:
nested.headers && typeof nested.headers === "object"
? { ...(nested.headers as Record<string, unknown>) }
: nested.headers,
},
}
: {}),
}
}
function resetCalls() {
aiGatewayCalls.length = 0
unifiedCalls.length = 0
gatewayModelCalls.length = 0
}
function cloudflareEnv(overrides: Record<string, string | undefined> = {}) {
return {
CLOUDFLARE_ACCOUNT_ID: "env-account",
CLOUDFLARE_GATEWAY_ID: "env-gateway",
CLOUDFLARE_API_TOKEN: "env-token",
CF_AIG_TOKEN: undefined,
...overrides,
}
}
mock.module("ai-gateway-provider", () => ({
createAiGateway(options: Record<string, unknown>) {
aiGatewayCalls.push(captureAiGatewayOptions(options))
return (input: unknown) => {
gatewayModelCalls.push(input)
return {
modelId: input,
provider: "cloudflare-ai-gateway",
specificationVersion: "v3",
}
}
},
}))
mock.module("ai-gateway-provider/providers/unified", () => ({
createUnified() {
return (modelID: string) => {
unifiedCalls.push(modelID)
return { unifiedModelID: modelID }
}
},
}))
describe("CloudflareAIGatewayPlugin", () => {
it.effect("requires account, gateway, and token before creating the unified SDK", () =>
withEnv(
{
CLOUDFLARE_ACCOUNT_ID: "acct",
CLOUDFLARE_GATEWAY_ID: "gateway",
CLOUDFLARE_API_TOKEN: "token",
CF_AIG_TOKEN: undefined,
},
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
},
{},
)
expect(result.sdk.languageModel("openai/gpt-5")).toBeDefined()
}),
),
)
it.effect("passes legacy metadata, cache, log, and User-Agent values under the AI Gateway options key", () =>
withEnv(cloudflareEnv(), () =>
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
package: "ai-gateway-provider",
options: {
name: "cloudflare-ai-gateway",
metadata: { invoked_by: "test", project: "opencode" },
cacheTtl: 300,
cacheKey: "cache-key",
skipCache: true,
collectLog: false,
},
},
{},
)
expect(aiGatewayCalls).toHaveLength(1)
expect(aiGatewayCalls[0]).toEqual({
accountId: "env-account",
gateway: "env-gateway",
apiKey: "env-token",
options: {
metadata: { invoked_by: "test", project: "opencode" },
cacheTtl: 300,
cacheKey: "cache-key",
skipCache: true,
collectLog: false,
headers: {
"User-Agent": expect.stringContaining("opencode/"),
},
},
})
}),
),
)
it.effect("parses legacy cf-aig-metadata header when metadata option is absent", () =>
withEnv(cloudflareEnv(), () =>
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
package: "ai-gateway-provider",
options: {
name: "cloudflare-ai-gateway",
headers: {
"cf-aig-metadata": JSON.stringify({ invoked_by: "header", project: "opencode" }),
},
},
},
{},
)
expect(aiGatewayCalls[0]?.options).toMatchObject({
metadata: { invoked_by: "header", project: "opencode" },
})
}),
),
)
it.effect("prefers Cloudflare env values over auth/config-derived options", () =>
withEnv(cloudflareEnv(), () =>
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
package: "ai-gateway-provider",
options: {
name: "cloudflare-ai-gateway",
accountId: "auth-account",
gateway: "auth-gateway",
apiKey: "auth-token",
},
},
{},
)
expect(aiGatewayCalls[0]).toMatchObject({
accountId: "env-account",
gateway: "env-gateway",
apiKey: "env-token",
})
}),
),
)
it.effect("accepts gatewayId metadata copied from auth into provider options", () =>
withEnv(
cloudflareEnv({
CLOUDFLARE_ACCOUNT_ID: undefined,
CLOUDFLARE_GATEWAY_ID: undefined,
CLOUDFLARE_API_TOKEN: undefined,
}),
() =>
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
package: "ai-gateway-provider",
options: {
name: "cloudflare-ai-gateway",
accountId: "auth-account",
gatewayId: "auth-gateway",
apiKey: "auth-token",
},
},
{},
)
expect(aiGatewayCalls[0]).toMatchObject({
accountId: "auth-account",
gateway: "auth-gateway",
apiKey: "auth-token",
})
}),
),
)
it.effect("falls back to CF_AIG_TOKEN when CLOUDFLARE_API_TOKEN is unset", () =>
withEnv(cloudflareEnv({ CLOUDFLARE_API_TOKEN: undefined, CF_AIG_TOKEN: "cf-aig-token" }), () =>
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
},
{},
)
expect(aiGatewayCalls[0]).toMatchObject({ apiKey: "cf-aig-token" })
}),
),
)
it.effect("does not create an SDK when account and gateway IDs are missing", () =>
withEnv(cloudflareEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_GATEWAY_ID: undefined }), () =>
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
},
{},
)
expect(result.sdk).toBeUndefined()
expect(aiGatewayCalls).toHaveLength(0)
}),
),
)
it.effect("does not create an SDK when the token is missing", () =>
withEnv(cloudflareEnv({ CLOUDFLARE_API_TOKEN: undefined, CF_AIG_TOKEN: undefined }), () =>
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
},
{},
)
expect(result.sdk).toBeUndefined()
expect(aiGatewayCalls).toHaveLength(0)
}),
),
)
it.effect("does not replace a configured baseURL with the Cloudflare AI Gateway SDK", () =>
withEnv(
cloudflareEnv({
CLOUDFLARE_ACCOUNT_ID: undefined,
CLOUDFLARE_GATEWAY_ID: undefined,
CLOUDFLARE_API_TOKEN: undefined,
}),
() =>
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" },
},
{},
)
expect(result.sdk).toBeUndefined()
expect(aiGatewayCalls).toHaveLength(0)
}),
),
)
it.effect("maps provider/model IDs through the unified Cloudflare provider unchanged", () =>
withEnv(cloudflareEnv(), () =>
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-ai-gateway", "anthropic/claude-sonnet-4-5"),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
},
{},
)
expect(result.sdk.languageModel("anthropic/claude-sonnet-4-5")).toEqual({
modelId: { unifiedModelID: "anthropic/claude-sonnet-4-5" },
provider: "cloudflare-ai-gateway",
specificationVersion: "v3",
})
expect(unifiedCalls).toEqual(["anthropic/claude-sonnet-4-5"])
expect(gatewayModelCalls).toEqual([{ unifiedModelID: "anthropic/claude-sonnet-4-5" }])
}),
),
)
it.effect("ignores non Cloudflare AI Gateway packages", () =>
withEnv(cloudflareEnv(), () =>
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-ai-gateway" },
},
{},
)
expect(result.sdk).toBeUndefined()
expect(aiGatewayCalls).toHaveLength(0)
}),
),
)
})
@@ -1,267 +0,0 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { AuthV2 } from "@opencode-ai/core/auth"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AuthPlugin } from "@opencode-ai/core/plugin/auth"
import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
import { testEffect } from "../lib/effect"
import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper"
const itWithAuth = testEffect(Layer.mergeAll(PluginV2.defaultLayer, AuthV2.defaultLayer, npmLayer))
function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") {
return (sdk as { languageModel: (id: string) => { config: CloudflareConfig; provider: string } }).languageModel(
modelID,
)
}
type CloudflareConfig = {
url: (input: { path: string; modelId: string }) => string
headers: () => Record<string, string> | Promise<Record<string, string>>
}
function cloudflareURL(sdk: unknown, modelID = "@cf/model") {
return cloudflareLanguage(sdk, modelID).config.url({ path: "/chat/completions", modelId: modelID })
}
function cloudflareHeaders(sdk: unknown, modelID = "@cf/model") {
return cloudflareLanguage(sdk, modelID).config.headers()
}
describe("CloudflareWorkersAIPlugin", () => {
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* PluginV2.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const updated = yield* plugin.trigger(
"provider.update",
{},
{ provider: provider("cloudflare-workers-ai"), cancel: false },
)
const sdk = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-workers-ai", "@cf/model", { endpoint: updated.provider.endpoint }),
package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
},
{},
)
expect(updated.provider.endpoint).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1",
})
expect(sdk.sdk).toBeDefined()
}),
),
)
it.effect("preserves a configured endpoint URL instead of deriving one from account ID", () =>
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("cloudflare-workers-ai", {
endpoint: { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" },
}),
cancel: false,
},
)
expect(result.provider.endpoint).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://proxy.example/v1",
})
}),
),
)
it.effect("allows a configured baseURL without account ID", () =>
withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-workers-ai", "@cf/model", {
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://proxy.example/v1" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" },
},
{},
)
expect(cloudflareURL(result.sdk)).toBe("https://proxy.example/v1/chat/completions")
}),
),
)
itWithAuth.effect("falls back to auth account metadata when account env is absent", () =>
withEnv(
{
CLOUDFLARE_ACCOUNT_ID: undefined,
CLOUDFLARE_API_KEY: undefined,
},
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const auth = yield* AuthV2.Service
yield* auth.create({
serviceID: AuthV2.ServiceID.make("cloudflare-workers-ai"),
credential: new AuthV2.ApiKeyCredential({
type: "api",
key: "auth-key",
metadata: { accountId: "auth-acct" },
}),
active: true,
})
yield* plugin.add({
...AuthPlugin,
effect: AuthPlugin.effect.pipe(Effect.provideService(AuthV2.Service, auth)),
})
yield* plugin.add(CloudflareWorkersAIPlugin)
const updated = yield* plugin.trigger(
"provider.update",
{},
{ provider: provider("cloudflare-workers-ai"), cancel: false },
)
expect(updated.provider.endpoint).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://api.cloudflare.com/client/v4/accounts/auth-acct/ai/v1",
})
}),
),
)
it.effect("uses env account ID over configured account ID", () =>
withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("cloudflare-workers-ai", {
options: { headers: {}, body: {}, aisdk: { provider: { accountId: "configured-acct" }, request: {} } },
}),
cancel: false,
},
)
expect(result.provider.endpoint).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://api.cloudflare.com/client/v4/accounts/env-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* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-workers-ai", "@cf/model", {
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://proxy.example/v1" },
}),
package: "@ai-sdk/openai-compatible",
options: {
name: "cloudflare-workers-ai",
apiKey: "auth-key",
baseURL: "https://proxy.example/v1",
headers: { custom: "header" },
},
},
{},
)
const headers = yield* Effect.promise(() => Promise.resolve(cloudflareHeaders(result.sdk)))
expect(headers.authorization).toBe("Bearer env-key")
expect(headers.custom).toBe("header")
expect(headers["user-agent"]).toMatch(/^opencode\/.* cloudflare-workers-ai \(.+\) ai-sdk\/openai-compatible\//)
}),
),
)
it.effect("expands account ID vars in endpoint URLs", () =>
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-workers-ai", "@cf/model", {
endpoint: {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
},
}),
package: "@ai-sdk/openai-compatible",
options: {
name: "cloudflare-workers-ai",
baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
},
},
{},
)
expect(cloudflareURL(result.sdk)).toBe(
"https://api.cloudflare.com/client/v4/accounts/acct/ai/v1/chat/completions",
)
}),
),
)
it.effect("selects languageModel with the API model ID", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
model: model("cloudflare-workers-ai", "alias", { apiID: ModelV2.ID.make("@cf/api-model") }),
sdk: fakeSelectorSdk(calls),
options: {},
},
{},
)
expect(result.language).toBeDefined()
expect(calls).toEqual(["languageModel:@cf/api-model"])
}),
)
it.effect("does not create an SDK for non OpenAI-compatible packages", () =>
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-workers-ai", "@cf/model", {
endpoint: { type: "aisdk", package: "@ai-sdk/anthropic", url: "https://proxy.example/v1" },
}),
package: "@ai-sdk/anthropic",
options: { name: "cloudflare-workers-ai" },
},
{},
)
expect(result.sdk).toBeUndefined()
}),
),
)
})
@@ -1,86 +0,0 @@
import { describe, expect, mock } from "bun:test"
import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere"
import { fakeSelectorSdk, it, model } from "./provider-helper"
const cohereOptions: Record<string, any>[] = []
void mock.module("@ai-sdk/cohere", () => ({
createCohere: (options: Record<string, any>) => {
cohereOptions.push({ ...options })
return {
languageModel: (modelID: string) => ({
modelID,
provider: `${options.name ?? "cohere"}.chat`,
specificationVersion: "v3",
}),
}
},
}))
describe("CoherePlugin", () => {
it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CoherePlugin)
const ignored = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("cohere", "command"), package: "@ai-sdk/openai-compatible", options: { name: "cohere" } },
{},
)
expect(ignored.sdk).toBeUndefined()
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("cohere", "command"), package: "@ai-sdk/cohere", options: { name: "cohere" } },
{},
)
expect(result.sdk).toBeDefined()
}),
)
it.effect("uses the model provider ID as the bundled SDK name", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CoherePlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("custom-cohere", "command-r-plus"),
package: "@ai-sdk/cohere",
options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" },
},
{},
)
expect(cohereOptions.at(-1)).toEqual({
name: "custom-cohere",
apiKey: "test",
baseURL: "https://cohere.example",
})
expect(result.sdk?.languageModel("command-r-plus").provider).toBe("custom-cohere.chat")
}),
)
it.effect("leaves language selection to the default languageModel fallback", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
const sdk = fakeSelectorSdk(calls)
yield* plugin.add(CoherePlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{ model: model("cohere", "alias", { apiID: ModelV2.ID.make("command-r-plus") }), sdk, options: {} },
{},
)
expect(result.language).toBeUndefined()
expect(calls).toEqual([])
expect(result.language ?? sdk.languageModel("command-r-plus")).toBeDefined()
expect(calls).toEqual(["languageModel:command-r-plus"])
}),
)
})
@@ -1,129 +0,0 @@
import { describe, expect, mock } from "bun:test"
import { Effect, Layer } from "effect"
import { AISDK } from "@opencode-ai/core/aisdk"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra"
import { testEffect } from "../lib/effect"
import { it, model } from "./provider-helper"
const itAISDK = testEffect(Layer.provideMerge(AISDK.layer, PluginV2.defaultLayer))
const deepinfraOptions: Record<string, any>[] = []
const deepinfraLanguageModels: string[] = []
void mock.module("@ai-sdk/deepinfra", () => ({
createDeepInfra: (options: Record<string, any>) => {
const captured = { ...options }
deepinfraOptions.push(captured)
return {
languageModel: (modelID: string) => {
deepinfraLanguageModels.push(modelID)
return { modelID, provider: `${captured.name ?? "deepinfra"}.chat`, specificationVersion: "v3" }
},
}
},
}))
function resetDeepInfraMock() {
deepinfraOptions.length = 0
deepinfraLanguageModels.length = 0
}
describe("DeepInfraPlugin", () => {
it.effect("creates a DeepInfra SDK for @ai-sdk/deepinfra", () =>
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
yield* plugin.add(DeepInfraPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("deepinfra", "model"), package: "@ai-sdk/deepinfra", options: { name: "deepinfra" } },
{},
)
expect(result.sdk).toBeDefined()
}),
)
it.effect("passes the model provider ID as the bundled DeepInfra SDK name", () =>
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
yield* plugin.add(DeepInfraPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("custom-deepinfra", "model"),
package: "@ai-sdk/deepinfra",
options: { name: "custom-deepinfra", apiKey: "test" },
},
{},
)
expect(result.sdk.languageModel("model").provider).toBe("custom-deepinfra.chat")
expect(deepinfraOptions).toEqual([{ name: "custom-deepinfra", apiKey: "test" }])
}),
)
it.effect("uses the canonical provider ID as the bundled DeepInfra SDK name", () =>
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
yield* plugin.add(DeepInfraPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("deepinfra", "model"),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra", apiKey: "test" },
},
{},
)
expect(result.sdk.languageModel("model").provider).toBe("deepinfra.chat")
expect(deepinfraOptions).toEqual([{ name: "deepinfra", apiKey: "test" }])
}),
)
it.effect("matches only the exact bundled DeepInfra package", () =>
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
yield* plugin.add(DeepInfraPlugin)
const packages = [
"unmatched-package",
"@ai-sdk/deepinfra-compatible",
"file:///tmp/@ai-sdk/deepinfra-provider.js",
]
yield* Effect.forEach(packages, (item) =>
Effect.gen(function* () {
const ignored = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("deepinfra", "model"), package: item, options: { name: "deepinfra" } },
{},
)
expect(ignored.sdk).toBeUndefined()
}),
)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("deepinfra", "model"), package: "@ai-sdk/deepinfra", options: { name: "deepinfra" } },
{},
)
expect(result.sdk).toBeDefined()
expect(deepinfraOptions).toEqual([{ name: "deepinfra" }])
}),
)
itAISDK.effect("uses the default languageModel selection for DeepInfra models", () =>
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* plugin.add(DeepInfraPlugin)
const language = yield* aisdk.language(
model("deepinfra", "meta-llama/Llama-3.3-70B-Instruct", {
endpoint: { type: "aisdk", package: "@ai-sdk/deepinfra" },
}),
)
expect(language.provider).toBe("deepinfra.chat")
expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"])
}),
)
})
@@ -1,172 +0,0 @@
import { Npm } from "@opencode-ai/core/npm"
import { describe, expect } from "bun:test"
import { Cause, Effect, Layer, Option } from "effect"
import fs from "fs/promises"
import os from "os"
import path from "path"
import { fileURLToPath } from "url"
import { AISDK } from "@opencode-ai/core/aisdk"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { DynamicProviderPlugin } from "@opencode-ai/core/plugin/provider/dynamic"
import { testEffect } from "../lib/effect"
import { fixtureProvider, it, model, npmLayer } from "./provider-helper"
const fixtureProviderPath = fileURLToPath(fixtureProvider)
const itWithAISDK = testEffect(AISDK.layer.pipe(Layer.provideMerge(PluginV2.defaultLayer)))
function npmEntrypointLayer(entrypoint: Option.Option<string>) {
return Layer.succeed(
Npm.Service,
Npm.Service.of({
add: () => Effect.succeed({ directory: "", entrypoint }),
install: () => Effect.void,
which: () => Effect.succeed(Option.none<string>()),
}),
)
}
function dynamicPlugin(layer = npmLayer) {
return { id: DynamicProviderPlugin.id, effect: DynamicProviderPlugin.effect.pipe(Effect.provide(layer)) }
}
function tempEntrypoint(source: string) {
return Effect.acquireRelease(
Effect.promise(async () => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-provider-dynamic-"))
const entrypoint = path.join(directory, "provider.mjs")
await Bun.write(entrypoint, source)
return { directory, entrypoint }
}),
(tmp) => Effect.promise(() => fs.rm(tmp.directory, { recursive: true, force: true })),
)
}
describe("DynamicProviderPlugin", () => {
it.effect("creates an SDK from a provider factory export", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(dynamicPlugin())
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("custom", "test-model"),
package: fixtureProvider,
options: { name: "custom", marker: "dynamic" },
},
{},
)
expect(result.sdk.options).toEqual({ marker: "dynamic", name: "custom" })
expect(result.sdk.languageModel("x")).toEqual({ modelID: "x", options: { marker: "dynamic", name: "custom" } })
}),
)
it.effect("does not override an SDK already supplied by an earlier plugin", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const sdk = { marker: "existing" }
yield* plugin.add(dynamicPlugin())
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("custom", "test-model"),
package: fixtureProvider,
options: { name: "custom", marker: "dynamic" },
},
{ sdk },
)
expect(result.sdk).toBe(sdk)
}),
)
it.effect("injects the provider ID as the SDK factory name", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(dynamicPlugin())
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("custom-provider", "test-model"),
package: fixtureProvider,
options: { name: "custom-provider", marker: "dynamic" },
},
{},
)
expect(result.sdk.options).toEqual({ marker: "dynamic", name: "custom-provider" })
}),
)
it.effect("loads npm packages through their resolved import entrypoint", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.some(fixtureProviderPath))))
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("npm-provider", "test-model"),
package: "fixture-provider",
options: { name: "npm-provider", marker: "npm" },
},
{},
)
expect(result.sdk.languageModel("x")).toEqual({ modelID: "x", options: { marker: "npm", name: "npm-provider" } })
}),
)
itWithAISDK.effect("wraps missing npm entrypoint failures as AISDK init errors", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.none<string>())))
const exit = yield* aisdk
.language(model("missing-entrypoint", "alias", { endpoint: { type: "aisdk", package: "fixture-provider" } }))
.pipe(Effect.exit)
expect(exit._tag).toBe("Failure")
if (exit._tag === "Failure") expect(Cause.prettyErrors(exit.cause).join("\n")).toContain("AISDK.InitError")
}),
)
itWithAISDK.effect("wraps dynamic import failures as AISDK init errors", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* plugin.add(dynamicPlugin())
const exit = yield* aisdk
.language(
model("bad-import", "alias", { endpoint: { type: "aisdk", package: "file:///missing/provider-factory.js" } }),
)
.pipe(Effect.exit)
expect(exit._tag).toBe("Failure")
if (exit._tag === "Failure") expect(Cause.prettyErrors(exit.cause).join("\n")).toContain("AISDK.InitError")
}),
)
itWithAISDK.live("wraps missing provider factory exports as AISDK init errors", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const tmp = yield* tempEntrypoint("export const notAProviderFactory = true\n")
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.some(tmp.entrypoint))))
const exit = yield* aisdk
.language(model("missing-factory", "alias", { endpoint: { type: "aisdk", package: "fixture-provider" } }))
.pipe(Effect.exit)
expect(exit._tag).toBe("Failure")
if (exit._tag === "Failure") expect(Cause.prettyErrors(exit.cause).join("\n")).toContain("AISDK.InitError")
}),
)
itWithAISDK.effect("uses the model apiID for the default language model", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* plugin.add(dynamicPlugin())
const language = yield* aisdk.language(
model("custom", "alias", {
apiID: ModelV2.ID.make("test-model-api"),
endpoint: { type: "aisdk", package: fixtureProvider },
}),
)
expect(language).toMatchObject({ modelID: "test-model-api", options: { name: "custom" } })
}),
)
})

Some files were not shown because too many files have changed in this diff Show More