mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 17:19:49 -04:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb3e9fb1a1 | |||
| bc89008514 | |||
| 6bcec1b159 | |||
| b3ec867da3 | |||
| 6efb07e805 | |||
| 4980355300 |
@@ -12,7 +12,6 @@ type GenericModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly provider?: string
|
||||
readonly baseURL: string
|
||||
readonly queryParams?: Readonly<Record<string, string>>
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
@@ -20,8 +19,6 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL: string
|
||||
readonly provider?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
readonly queryParams?: Readonly<Record<string, string>>
|
||||
}
|
||||
|
||||
export type FamilyModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
@@ -34,11 +31,11 @@ export const routes = [OpenAICompatibleChat.route]
|
||||
|
||||
export const configure = (input: GenericModelOptions) => {
|
||||
const provider = input.provider ?? "openai-compatible"
|
||||
const { provider: _, baseURL, apiKey: _apiKey, auth: _auth, queryParams, ...rest } = input
|
||||
const { provider: _, baseURL, apiKey: _apiKey, auth: _auth, ...rest } = input
|
||||
const route = OpenAICompatibleChat.route.with({
|
||||
...rest,
|
||||
provider,
|
||||
endpoint: { baseURL, query: queryParams },
|
||||
endpoint: { baseURL },
|
||||
auth: AuthOptions.bearer(input, []),
|
||||
})
|
||||
return {
|
||||
@@ -78,8 +75,6 @@ export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsIn
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
limits: settings.limits,
|
||||
provider: settings.provider,
|
||||
providerOptions: settings.providerOptions,
|
||||
queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams },
|
||||
}).model(modelID)
|
||||
|
||||
export const baseten = define(profiles.baseten)
|
||||
|
||||
@@ -64,24 +64,6 @@ describe("provider package entrypoints", () => {
|
||||
expect(xai.route.defaults.providerOptions).toMatchObject({ xai: { reasoningEffort: "high", store: false } })
|
||||
})
|
||||
|
||||
test("maps OpenAI-compatible package settings onto the executable model", async () => {
|
||||
const OpenAICompatible = await import("@opencode-ai/ai/providers/openai-compatible")
|
||||
const selected = OpenAICompatible.model("custom-model", {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://provider.example.test/v1",
|
||||
provider: "example",
|
||||
queryParams: { version: "preview" },
|
||||
providerOptions: { openai: { reasoningEffort: "high" } },
|
||||
})
|
||||
|
||||
expect(String(selected.provider)).toBe("example")
|
||||
expect(selected.route.endpoint).toMatchObject({
|
||||
baseURL: "https://provider.example.test/v1",
|
||||
query: { version: "preview" },
|
||||
})
|
||||
expect(selected.route.defaults.providerOptions).toEqual({ openai: { reasoningEffort: "high" } })
|
||||
})
|
||||
|
||||
test("maps package settings onto the executable model", () => {
|
||||
const selected = model("gpt-5", {
|
||||
apiKey: "fixture",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"fix-node-pty": "bun run script/fix-node-pty.ts",
|
||||
"benchmark:location": "bun run script/benchmark-location.ts",
|
||||
"test": "bun test --only-failures",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
"typecheck": "tsgo -b && tsgo --noEmit -p tsconfig.tests.json"
|
||||
},
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode"
|
||||
|
||||
@@ -132,14 +132,16 @@ function renderMigration(name: string, sql: string) {
|
||||
return `import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: ${JSON.stringify(name)},
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
${renderStatements(sql)}
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
`
|
||||
}
|
||||
|
||||
@@ -147,13 +149,15 @@ function renderSchema(sql: string) {
|
||||
return `import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export default {
|
||||
const schema: Omit<DatabaseMigration.Migration, "id"> = {
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
${renderStatements(sql)}
|
||||
})
|
||||
},
|
||||
} satisfies Omit<DatabaseMigration.Migration, "id">
|
||||
}
|
||||
|
||||
export default schema
|
||||
`
|
||||
}
|
||||
|
||||
@@ -191,10 +195,10 @@ async function formatTypescript(input: string) {
|
||||
function renderRegistry(names: string[]) {
|
||||
return `import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export const migrations = (
|
||||
export const migrations: DatabaseMigration.Migration[] = (
|
||||
await Promise.all([
|
||||
${names.map((name) => ` import("./migration/${name}"),`).join("\n")}
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
).map((module) => module.default)
|
||||
`
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface Mapping {
|
||||
|
||||
export interface MapInput {
|
||||
readonly packageName: string | undefined
|
||||
readonly providerID: string
|
||||
readonly settings: Readonly<Record<string, unknown>>
|
||||
readonly modelID: string
|
||||
}
|
||||
@@ -52,8 +51,6 @@ export function map(input: MapInput): Mapping | undefined {
|
||||
...mapGoogleOptions(input.settings),
|
||||
},
|
||||
}
|
||||
case "@ai-sdk/openai-compatible":
|
||||
return mapOpenAICompatible(input, baseSettings)
|
||||
case "@openrouter/ai-sdk-provider":
|
||||
return mapOpenRouter(input.settings, baseSettings)
|
||||
case "@ai-sdk/xai":
|
||||
@@ -66,33 +63,6 @@ export function map(input: MapInput): Mapping | undefined {
|
||||
},
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function mapOpenAICompatible(
|
||||
input: MapInput,
|
||||
baseSettings: Readonly<Record<string, unknown>>,
|
||||
): Mapping | undefined {
|
||||
const accountId =
|
||||
input.providerID === "cloudflare-workers-ai" && typeof input.settings.accountId === "string"
|
||||
? input.settings.accountId
|
||||
: undefined
|
||||
const baseURL =
|
||||
typeof baseSettings.baseURL === "string" && accountId
|
||||
? baseSettings.baseURL.replaceAll("${CLOUDFLARE_ACCOUNT_ID}", encodeURIComponent(accountId))
|
||||
: baseSettings.baseURL
|
||||
if (typeof baseURL !== "string") return undefined
|
||||
return {
|
||||
package: "@opencode-ai/ai/providers/openai-compatible",
|
||||
settings: {
|
||||
baseURL,
|
||||
...mapAPIKey(input.settings),
|
||||
provider: input.providerID,
|
||||
...(isStringRecord(input.settings.queryParams) ? { queryParams: input.settings.queryParams } : {}),
|
||||
...mapOpenAIOptions(input.settings),
|
||||
},
|
||||
...(isStringRecord(input.settings.headers) ? { headers: input.settings.headers } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function mapBedrockMantle(input: MapInput, baseSettings: Readonly<Record<string, unknown>>): Mapping | undefined {
|
||||
|
||||
@@ -233,13 +233,13 @@ export const layer = (options?: Options) => Layer.effect(
|
||||
|
||||
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
|
||||
return [
|
||||
...(yield* loadWellknown().pipe(Effect.orDie)),
|
||||
...claude,
|
||||
...agents,
|
||||
...(supplementary[0] ?? []),
|
||||
...explicit,
|
||||
...direct,
|
||||
...supplementary.slice(1).flat(),
|
||||
...(yield* loadWellknown().pipe(Effect.orDie)),
|
||||
...content,
|
||||
]
|
||||
})
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export const migrations = (
|
||||
export const migrations: DatabaseMigration.Migration[] = (
|
||||
await Promise.all([
|
||||
import("./migration/20260127222353_familiar_lady_ursula"),
|
||||
import("./migration/20260211171708_add_project_commands"),
|
||||
@@ -43,4 +43,4 @@ export const migrations = (
|
||||
import("./migration/20260804233008_loose_psylocke"),
|
||||
import("./migration/20260805200742_import_legacy_credentials"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
).map((module) => module.default)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260127222353_familiar_lady_ursula",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -104,4 +104,6 @@ export default {
|
||||
yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260211171708_add_project_commands",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`project\` ADD \`commands\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260213144116_wakeful_the_professor",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -20,4 +20,6 @@ export default {
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260225215848_workspace",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -16,4 +16,6 @@ export default {
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260227213759_add_session_workspace_id",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -9,4 +9,6 @@ export default {
|
||||
yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260228203230_blue_harpoon",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -27,4 +27,6 @@ export default {
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260303231226_add_workspace_fields",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -12,4 +12,6 @@ export default {
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` DROP COLUMN \`config\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260309230000_move_org_to_state",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -12,4 +12,6 @@ export default {
|
||||
yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260312043431_session_message_cursor",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -13,4 +13,6 @@ export default {
|
||||
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260323234822_events",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -23,4 +23,6 @@ export default {
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260410174513_workspace-name",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -26,4 +26,6 @@ export default {
|
||||
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260413175956_chief_energizer",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -21,4 +21,6 @@ export default {
|
||||
yield* tx.run(`CREATE INDEX \`session_entry_time_created_idx\` ON \`session_entry\` (\`time_created\`);`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260423070820_add_icon_url_override",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -11,4 +11,6 @@ export default {
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260427172553_slow_nightmare",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -27,4 +27,6 @@ export default {
|
||||
yield* tx.run(`DROP TABLE \`session_entry\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260428004200_add_session_path",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`path\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260501142318_next_venus",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -9,4 +9,6 @@ export default {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`model\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260504145000_add_sync_owner",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`event_sequence\` ADD \`owner_id\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260507164347_add_workspace_time",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`time_used\` integer NOT NULL DEFAULT 0;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260510033149_session_usage",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -53,4 +53,6 @@ export default {
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260511000411_data_migration_state",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -13,4 +13,6 @@ export default {
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260511173437_session-metadata",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -13,4 +13,6 @@ export default {
|
||||
yield* tx.run(`ALTER TABLE \`session\` ADD \`metadata\` text;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260601010001_normalize_storage_paths",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -19,4 +19,6 @@ export default {
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260601202201_amazing_prowler",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`DROP TABLE \`permission\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260602002951_lowly_union_jack",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -21,4 +21,6 @@ export default {
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260602182828_add_project_directories",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -17,4 +17,6 @@ export default {
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
+4
-2
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260603001617_session_message_projection_indexes",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -16,4 +16,6 @@ export default {
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
+4
-2
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260603040000_session_message_projection_order",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -16,4 +16,6 @@ export default {
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260603141458_session_input_inbox",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -22,4 +22,6 @@ export default {
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260603160727_jittery_ezekiel_stane",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -17,4 +17,6 @@ export default {
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260604172448_event_sourced_session_input",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -44,4 +44,6 @@ export default {
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260605003541_add_session_context_snapshot",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -18,4 +18,6 @@ export default {
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260605042240_add_context_epoch_agent",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`ALTER TABLE \`session_context_epoch\` ADD \`agent\` text DEFAULT 'build' NOT NULL;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260611035744_credential",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -22,4 +22,6 @@ export default {
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260611192811_lush_chimera",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -22,4 +22,6 @@ export default {
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260612174303_project_dir_strategy",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -26,4 +26,6 @@ export default {
|
||||
yield* tx.run(`PRAGMA foreign_keys=ON;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
+4
-2
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260622142730_simplify_session_context_epoch",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -10,4 +10,6 @@ export default {
|
||||
yield* tx.run(`ALTER TABLE \`session_context_epoch\` DROP COLUMN \`revision\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260622170816_reset_v2_session_state",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -12,4 +12,6 @@ export default {
|
||||
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260622202450_simplify_session_input",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -14,4 +14,6 @@ export default {
|
||||
yield* tx.run(`DELETE FROM \`workspace\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260804233008_loose_psylocke",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
@@ -135,4 +135,6 @@ export default {
|
||||
yield* tx.run(`DROP TABLE \`session_input\`;`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
@@ -30,12 +30,14 @@ const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
|
||||
const decodeValue = Schema.decodeUnknownOption(LegacyValue)
|
||||
const wellKnownSourcesKey = "wellknown:sources"
|
||||
|
||||
export default {
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260805200742_import_legacy_credentials",
|
||||
up(tx) {
|
||||
return importLegacyCredentials(tx, path.join(Global.Path.data, "auth.json"))
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
}
|
||||
|
||||
export default migration
|
||||
|
||||
export function importLegacyCredentials(tx: Parameters<DatabaseMigration.Migration["up"]>[0], filepath: string) {
|
||||
return Effect.gen(function* () {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "./migration"
|
||||
|
||||
export default {
|
||||
const schema: Omit<DatabaseMigration.Migration, "id"> = {
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
@@ -248,4 +248,6 @@ export default {
|
||||
)
|
||||
})
|
||||
},
|
||||
} satisfies Omit<DatabaseMigration.Migration, "id">
|
||||
}
|
||||
|
||||
export default schema
|
||||
|
||||
@@ -82,10 +82,10 @@ export class DuplicateKeyError extends Schema.TaggedErrorClass<DuplicateKeyError
|
||||
}
|
||||
}
|
||||
|
||||
export const empty: Instructions = []
|
||||
export const empty: ReadonlyArray<Source> = []
|
||||
|
||||
/** Closes a typed definition into one `Source`, so differently typed sources compose. */
|
||||
export function make<A>(source: Source.Definition<A>): Instructions {
|
||||
export function make<A>(source: Source.Definition<A>): ReadonlyArray<Source> {
|
||||
const decode = Schema.decodeUnknownOption(source.codec)
|
||||
const encode = Schema.encodeSync(source.codec)
|
||||
const initial = (value: A) => requireText(source.key, "initial", source.render.initial(value))
|
||||
@@ -121,7 +121,7 @@ export function make<A>(source: Source.Definition<A>): Instructions {
|
||||
]
|
||||
}
|
||||
|
||||
export function combine(values: ReadonlyArray<Instructions>): Instructions {
|
||||
export function combine(values: ReadonlyArray<ReadonlyArray<Source>>): ReadonlyArray<Source> {
|
||||
const sources = values.flat()
|
||||
const keys = new Set<Key>()
|
||||
for (const source of sources) {
|
||||
@@ -131,7 +131,7 @@ export function combine(values: ReadonlyArray<Instructions>): Instructions {
|
||||
return sources
|
||||
}
|
||||
|
||||
export function read(value: Instructions): Effect.Effect<ReadResult> {
|
||||
export function read(value: ReadonlyArray<Source>): Effect.Effect<ReadResult> {
|
||||
return Effect.forEach(
|
||||
value,
|
||||
(source) => source.read.pipe(Effect.map((observed) => ({ key: source.key, value: observed }))),
|
||||
@@ -158,7 +158,7 @@ export function diff(observed: ReadResult, previous?: Values): Effect.Effect<Adm
|
||||
return Effect.succeed({ delta, blobs })
|
||||
}
|
||||
|
||||
export function renderInitial(value: Instructions, values: Readonly<Record<string, Schema.Json>>) {
|
||||
export function renderInitial(value: ReadonlyArray<Source>, values: Readonly<Record<string, Schema.Json>>) {
|
||||
return render(
|
||||
value.flatMap((source) => {
|
||||
if (!Object.hasOwn(values, source.key)) return []
|
||||
@@ -169,7 +169,7 @@ export function renderInitial(value: Instructions, values: Readonly<Record<strin
|
||||
}
|
||||
|
||||
export function renderUpdate(
|
||||
value: Instructions,
|
||||
value: ReadonlyArray<Source>,
|
||||
previous: Readonly<Record<string, Schema.Json>>,
|
||||
delta: Readonly<Record<string, Option.Option<Schema.Json>>>,
|
||||
) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { LanguageModel } from "@opencode-ai/ai"
|
||||
// ast-grep-ignore: no-star-import
|
||||
import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages"
|
||||
// ast-grep-ignore: no-star-import
|
||||
import * as OpenAICompatibleChat from "@opencode-ai/ai/protocols/openai-compatible-chat"
|
||||
// ast-grep-ignore: no-star-import
|
||||
import * as OpenAIResponses from "@opencode-ai/ai/protocols/openai-responses"
|
||||
import { Auth, type AnyRoute } from "@opencode-ai/ai/route"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
@@ -144,7 +146,6 @@ export const fromCatalogModel = (
|
||||
if (draft.settings?.apiKey === "") delete draft.settings.apiKey
|
||||
if (credential?.type === "key" && credential.metadata !== undefined)
|
||||
draft.body = Provider.mergeOverlay(draft.body, credential.metadata)
|
||||
if (draft.providerID === "cloudflare-workers-ai" && draft.body) delete draft.body.accountId
|
||||
})
|
||||
const packageName = Provider.packageName(resolved.package)
|
||||
const key = apiKey(resolved, credential)
|
||||
@@ -163,11 +164,21 @@ export const fromCatalogModel = (
|
||||
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
|
||||
)
|
||||
}
|
||||
if (
|
||||
Provider.isAISDK(resolved.package) &&
|
||||
packageName === "@ai-sdk/openai-compatible" &&
|
||||
typeof resolved.settings?.baseURL === "string"
|
||||
) {
|
||||
return Effect.succeed(
|
||||
withDefaults(resolved, OpenAICompatibleChat.route)
|
||||
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
|
||||
)
|
||||
}
|
||||
const configured = { ...resolved.settings, ...credential?.metadata }
|
||||
const mapping = Provider.isAISDK(resolved.package)
|
||||
? AISDKNative.map({
|
||||
packageName,
|
||||
providerID: resolved.providerID,
|
||||
settings: configured,
|
||||
modelID: resolved.modelID ?? resolved.id,
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ export * as PluginPromise from "./promise"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||
import type { Context, Plugin } from "@opencode-ai/plugin/promise/plugin"
|
||||
import type { SessionHooks, SessionHttp, SessionHttpMiddleware } from "@opencode-ai/plugin/promise/session"
|
||||
import type { Info } from "@opencode-ai/plugin/promise/tool"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Integration } from "@opencode-ai/schema/integration"
|
||||
@@ -57,6 +58,62 @@ export function fromPromise(plugin: Plugin) {
|
||||
}),
|
||||
)
|
||||
|
||||
function sessionHook<Name extends keyof SessionHooks>(
|
||||
name: Name,
|
||||
callback: (event: SessionHooks[Name]) => Promise<void> | void,
|
||||
): Promise<Registration>
|
||||
function sessionHook(
|
||||
...registration: {
|
||||
[Name in keyof SessionHooks]: [
|
||||
name: Name,
|
||||
callback: (event: SessionHooks[Name]) => Promise<void> | void,
|
||||
]
|
||||
}[keyof SessionHooks]
|
||||
) {
|
||||
if (registration[0] !== "http")
|
||||
return register(
|
||||
host.session.hook(registration[0], (event) =>
|
||||
Effect.promise(() => Promise.resolve(registration[1](event))),
|
||||
),
|
||||
)
|
||||
return register(
|
||||
host.session.hook("http", (event) => {
|
||||
const middlewares: SessionHttpMiddleware[] = []
|
||||
const output: SessionHttp = {
|
||||
...event,
|
||||
use: (item) => {
|
||||
middlewares.push(item)
|
||||
},
|
||||
}
|
||||
return Effect.promise(() => Promise.resolve(registration[1](output))).pipe(
|
||||
Effect.flatMap(() =>
|
||||
Effect.forEach(
|
||||
middlewares,
|
||||
(item) =>
|
||||
event.use((input, next) =>
|
||||
Effect.tryPromise({
|
||||
try: (signal) => {
|
||||
const inputSignal = AbortSignal.any([signal, input.signal])
|
||||
return Promise.resolve(
|
||||
item(new Request(input, { signal: inputSignal }), (request) => {
|
||||
const requestSignal = AbortSignal.any([signal, request.signal])
|
||||
return Effect.runPromiseWith(
|
||||
context,
|
||||
)(next(new Request(request, { signal: requestSignal })), { signal: requestSignal })
|
||||
}),
|
||||
)
|
||||
},
|
||||
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
|
||||
}),
|
||||
),
|
||||
{ discard: true },
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const context2: Context = {
|
||||
app: host.app,
|
||||
options: host.options,
|
||||
@@ -265,8 +322,7 @@ export function fromPromise(plugin: Plugin) {
|
||||
),
|
||||
},
|
||||
session: {
|
||||
hook: (name, callback) =>
|
||||
register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
hook: sessionHook,
|
||||
create: (input) =>
|
||||
run(
|
||||
host.session.create(
|
||||
|
||||
@@ -14,20 +14,38 @@ export const CloudflareWorkersAIPlugin = define({
|
||||
if (!item) return
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
if (!Provider.isAISDK(provider.package)) return
|
||||
if (typeof provider.settings?.baseURL === "string") return
|
||||
const accountId = resolveAccountId(provider.settings ?? {})
|
||||
if (accountId)
|
||||
provider.settings = {
|
||||
...provider.settings,
|
||||
baseURL:
|
||||
typeof provider.settings?.baseURL === "string"
|
||||
? provider.settings.baseURL.replaceAll("${CLOUDFLARE_ACCOUNT_ID}", encodeURIComponent(accountId))
|
||||
: workersEndpoint(accountId),
|
||||
}
|
||||
provider.headers = Provider.mergeHeaders(provider.headers, {
|
||||
"User-Agent": `${App.useragent(ctx.app)} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
})
|
||||
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
|
||||
})
|
||||
})
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
if (evt.package !== "@ai-sdk/openai-compatible") return
|
||||
|
||||
const accountId = resolveAccountId(evt.options)
|
||||
if (!hasWorkersEndpoint(evt.model) && !accountId) return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
|
||||
evt.sdk = mod.createOpenAICompatible(
|
||||
sdkOptions(
|
||||
{
|
||||
...evt.options,
|
||||
baseURL: evt.options.baseURL ?? (accountId ? workersEndpoint(accountId) : undefined),
|
||||
},
|
||||
ctx.app,
|
||||
) as any,
|
||||
)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -36,7 +54,32 @@ function resolveAccountId(options: Record<string, unknown>) {
|
||||
}
|
||||
|
||||
function workersEndpoint(accountId: string) {
|
||||
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1`
|
||||
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
|
||||
}
|
||||
|
||||
function hasWorkersEndpoint(model: {
|
||||
readonly package?: string
|
||||
readonly settings?: Readonly<Record<string, unknown>>
|
||||
}) {
|
||||
return Provider.isAISDK(model.package) && typeof model.settings?.baseURL === "string"
|
||||
}
|
||||
|
||||
function sdkOptions(options: Record<string, any>, app: App.Info) {
|
||||
return {
|
||||
...options,
|
||||
baseURL: expandAccountId(options.baseURL),
|
||||
apiKey: process.env.CLOUDFLARE_API_KEY ?? options.apiKey,
|
||||
headers: {
|
||||
"User-Agent": `${App.useragent(app)} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
...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) {
|
||||
|
||||
@@ -221,18 +221,18 @@ export const OpenAIPlugin = define({
|
||||
}
|
||||
draft.cost = []
|
||||
// Match Codex CLI so context consumption and subscription usage stay consistent between clients.
|
||||
draft.limit = { ...draft.limit, context: 400_000, input: 272_000 }
|
||||
draft.limit = { ...draft.limit, context: 272_000, input: 272_000 }
|
||||
})
|
||||
}
|
||||
})
|
||||
yield* ctx.session.hook("http.request", (evt) =>
|
||||
Effect.sync(() => {
|
||||
if (!chatgpt || evt.model.providerID !== Provider.ID.openai) return
|
||||
const url = new URL(evt.request.url)
|
||||
evt.request.headers.set("originator", "opencode")
|
||||
evt.request.headers.set("session-id", evt.sessionID)
|
||||
if (url.origin !== "https://api.openai.com") return
|
||||
evt.request = new Request(`${codexBaseURL}${url.pathname.replace(/^\/v1/, "")}${url.search}`, evt.request)
|
||||
yield* ctx.session.hook("http", (evt) =>
|
||||
evt.use((request, next) => {
|
||||
if (!chatgpt || evt.model.providerID !== Provider.ID.openai) return next(request)
|
||||
const url = new URL(request.url)
|
||||
request.headers.set("originator", "opencode")
|
||||
request.headers.set("session-id", evt.sessionID)
|
||||
if (url.origin !== "https://api.openai.com") return next(request)
|
||||
return next(new Request(`${codexBaseURL}${url.pathname.replace(/^\/v1/, "")}${url.search}`, request))
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ export * as SessionModelRequest from "./model-request"
|
||||
|
||||
import { LLM, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai"
|
||||
import type { StreamOptions } from "@opencode-ai/ai/route"
|
||||
import type { SessionHttpHandler, SessionHttpMiddleware } from "@opencode-ai/plugin/effect/session"
|
||||
import type { Content } from "@opencode-ai/schema/tool"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Cause, Config, Context, Effect, Layer, Result, Stream } from "effect"
|
||||
@@ -229,31 +230,44 @@ export const layer = Layer.effect(
|
||||
const options: StreamOptions = {
|
||||
http: (request, handler) =>
|
||||
Effect.gen(function* () {
|
||||
const before = yield* hooks.trigger("session", "http.request", {
|
||||
let latest = request
|
||||
const origins = new WeakMap<Response, HttpClientRequest.HttpClientRequest>()
|
||||
const middlewares: SessionHttpMiddleware[] = []
|
||||
const web = yield* HttpClientRequest.toWeb(request)
|
||||
yield* hooks.trigger("session", "http", {
|
||||
sessionID: session.id,
|
||||
agent: agent.id,
|
||||
model: resolved.ref,
|
||||
request: yield* HttpClientRequest.toWeb(request),
|
||||
use: (item) =>
|
||||
Effect.sync(() => {
|
||||
middlewares.push(item)
|
||||
}),
|
||||
})
|
||||
let sent = HttpClientRequest.fromWeb(before.request)
|
||||
if (before.request.body)
|
||||
sent = HttpClientRequest.bodyUint8Array(
|
||||
sent,
|
||||
new Uint8Array(yield* Effect.promise(() => before.request.clone().arrayBuffer())),
|
||||
before.request.headers.get("content-type") ?? undefined,
|
||||
)
|
||||
const response = yield* handler(sent)
|
||||
const after = yield* hooks.trigger("session", "http.response", {
|
||||
sessionID: session.id,
|
||||
agent: agent.id,
|
||||
model: resolved.ref,
|
||||
request: before.request,
|
||||
response: new Response(
|
||||
[204, 205, 304].includes(response.status) ? null : yield* Stream.toReadableStreamEffect(response.stream),
|
||||
{ status: response.status, headers: response.headers },
|
||||
),
|
||||
})
|
||||
return HttpClientResponse.fromWeb(sent, after.response)
|
||||
const send = (input: Request) =>
|
||||
Effect.gen(function* () {
|
||||
let sent = HttpClientRequest.fromWeb(input)
|
||||
if (input.body)
|
||||
sent = HttpClientRequest.bodyUint8Array(
|
||||
sent,
|
||||
new Uint8Array(yield* Effect.promise(() => input.clone().arrayBuffer())),
|
||||
input.headers.get("content-type") ?? undefined,
|
||||
)
|
||||
latest = sent
|
||||
const response = yield* handler(sent)
|
||||
const body = [204, 205, 304].includes(response.status)
|
||||
? null
|
||||
: yield* Stream.toReadableStreamEffect(response.stream)
|
||||
const output = new Response(body, { status: response.status, headers: response.headers })
|
||||
origins.set(output, sent)
|
||||
return output
|
||||
})
|
||||
const dispatch = middlewares.reduce<SessionHttpHandler>(
|
||||
(next, item) => (input: Request) => item(input, next),
|
||||
send,
|
||||
)
|
||||
const response = yield* dispatch(web)
|
||||
const origin = origins.get(response) ?? latest
|
||||
return HttpClientResponse.fromWeb(origin, response)
|
||||
}).pipe(Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause))))),
|
||||
}
|
||||
if (promptCacheSnapshots) {
|
||||
|
||||
@@ -13,54 +13,13 @@ export abstract class NamedError extends Error {
|
||||
static create<Name extends string, Fields extends Schema.Struct.Fields>(
|
||||
name: Name,
|
||||
fields: Fields,
|
||||
): ReturnType<typeof NamedError.createSchemaClass<Name, Schema.Struct<Fields>>>
|
||||
): ReturnType<typeof createSchemaClass<Name, Schema.Struct<Fields>>>
|
||||
static create<Name extends string, DataSchema extends Schema.Top>(
|
||||
name: Name,
|
||||
data: DataSchema,
|
||||
): ReturnType<typeof NamedError.createSchemaClass<Name, DataSchema>>
|
||||
): ReturnType<typeof createSchemaClass<Name, DataSchema>>
|
||||
static create<Name extends string>(name: Name, data: Schema.Top | Schema.Struct.Fields) {
|
||||
return NamedError.createSchemaClass(name, Schema.isSchema(data) ? data : Schema.Struct(data))
|
||||
}
|
||||
|
||||
private static createSchemaClass<Name extends string, DataSchema extends Schema.Top>(name: Name, data: DataSchema) {
|
||||
const schema = Schema.Struct({
|
||||
name: Schema.Literal(name),
|
||||
data,
|
||||
}).annotate({ identifier: name })
|
||||
type Data = Schema.Schema.Type<DataSchema>
|
||||
|
||||
const result = class extends NamedError {
|
||||
public static readonly Schema = schema
|
||||
public static readonly EffectSchema = schema
|
||||
public static readonly tag = name
|
||||
|
||||
public override readonly name = name
|
||||
|
||||
constructor(
|
||||
public readonly data: Data,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(name, options)
|
||||
this.name = name
|
||||
}
|
||||
|
||||
static isInstance(input: unknown): input is InstanceType<typeof result> {
|
||||
return NamedError.hasName(input, name)
|
||||
}
|
||||
|
||||
schema() {
|
||||
return schema
|
||||
}
|
||||
|
||||
toObject() {
|
||||
return {
|
||||
name: name,
|
||||
data: this.data,
|
||||
}
|
||||
}
|
||||
}
|
||||
Object.defineProperty(result, "name", { value: name })
|
||||
return result
|
||||
return createSchemaClass(name, Schema.isSchema(data) ? data : Schema.Struct(data), this)
|
||||
}
|
||||
|
||||
public static readonly Unknown = NamedError.create("UnknownError", {
|
||||
@@ -68,3 +27,48 @@ export abstract class NamedError extends Error {
|
||||
ref: Schema.optional(Schema.String),
|
||||
})
|
||||
}
|
||||
|
||||
function createSchemaClass<Name extends string, DataSchema extends Schema.Top>(
|
||||
name: Name,
|
||||
data: DataSchema,
|
||||
base: typeof NamedError = NamedError,
|
||||
) {
|
||||
const schema = Schema.Struct({
|
||||
name: Schema.Literal(name),
|
||||
data,
|
||||
}).annotate({ identifier: name })
|
||||
type Data = Schema.Schema.Type<DataSchema>
|
||||
|
||||
const result = class extends base {
|
||||
public static readonly Schema = schema
|
||||
public static readonly EffectSchema = schema
|
||||
public static readonly tag = name
|
||||
|
||||
public override readonly name = name
|
||||
|
||||
constructor(
|
||||
public readonly data: Data,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(name, options)
|
||||
this.name = name
|
||||
}
|
||||
|
||||
static isInstance(input: unknown): input is InstanceType<typeof result> {
|
||||
return base.hasName(input, name)
|
||||
}
|
||||
|
||||
schema() {
|
||||
return schema
|
||||
}
|
||||
|
||||
toObject() {
|
||||
return {
|
||||
name: name,
|
||||
data: this.data,
|
||||
}
|
||||
}
|
||||
}
|
||||
Object.defineProperty(result, "name", { value: name })
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { AISDKNative } from "@opencode-ai/core/aisdk-native"
|
||||
|
||||
const map = (
|
||||
packageName: string,
|
||||
settings: Readonly<Record<string, unknown>>,
|
||||
modelID = "test-model",
|
||||
providerID = "test-provider",
|
||||
) => AISDKNative.map({ packageName, providerID, settings, modelID })
|
||||
const map = (packageName: string, settings: Readonly<Record<string, unknown>>, modelID = "test-model") =>
|
||||
AISDKNative.map({ packageName, settings, modelID })
|
||||
|
||||
describe("AISDKNative", () => {
|
||||
test("maps both models.dev Bedrock packages to native providers", () => {
|
||||
@@ -45,52 +41,6 @@ describe("AISDKNative", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("maps Cloudflare Workers AI to the generic OpenAI-compatible provider", () => {
|
||||
expect(
|
||||
map(
|
||||
"@ai-sdk/openai-compatible",
|
||||
{
|
||||
accountId: "account/id",
|
||||
apiKey: "secret",
|
||||
baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||
headers: { "x-custom": "value" },
|
||||
queryParams: { version: "preview" },
|
||||
reasoningEffort: "high",
|
||||
},
|
||||
"@cf/model",
|
||||
"cloudflare-workers-ai",
|
||||
),
|
||||
).toEqual({
|
||||
package: "@opencode-ai/ai/providers/openai-compatible",
|
||||
settings: {
|
||||
apiKey: "secret",
|
||||
baseURL: "https://api.cloudflare.com/client/v4/accounts/account%2Fid/ai/v1",
|
||||
provider: "cloudflare-workers-ai",
|
||||
queryParams: { version: "preview" },
|
||||
providerOptions: { openai: { reasoningEffort: "high" } },
|
||||
},
|
||||
headers: { "x-custom": "value" },
|
||||
})
|
||||
})
|
||||
|
||||
test("maps generic OpenAI-compatible providers to the native package", () => {
|
||||
expect(
|
||||
map("@ai-sdk/openai-compatible", {
|
||||
apiKey: "secret",
|
||||
baseURL: "https://provider.example/v1",
|
||||
reasoningEffort: "high",
|
||||
}),
|
||||
).toEqual({
|
||||
package: "@opencode-ai/ai/providers/openai-compatible",
|
||||
settings: {
|
||||
apiKey: "secret",
|
||||
baseURL: "https://provider.example/v1",
|
||||
provider: "test-provider",
|
||||
providerOptions: { openai: { reasoningEffort: "high" } },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("maps Bedrock provider and request options", () => {
|
||||
expect(
|
||||
map(
|
||||
|
||||
@@ -131,45 +131,6 @@ describe("ModelResolver", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("routes Cloudflare Workers AI through the generic OpenAI-compatible provider", () =>
|
||||
Effect.gen(function* () {
|
||||
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||
model(Provider.aisdk("@ai-sdk/openai-compatible"), {
|
||||
providerID: Provider.ID.make("cloudflare-workers-ai"),
|
||||
modelID: "@cf/meta/llama-3.1-8b-instruct",
|
||||
settings: {
|
||||
baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||
queryParams: { version: "preview" },
|
||||
reasoningEffort: "high",
|
||||
},
|
||||
}),
|
||||
Credential.Key.make({ type: "key", key: "secret", metadata: { accountId: "account/id" } }),
|
||||
{ loadAISDK: () => Effect.die("AI SDK loader should not be called") },
|
||||
)
|
||||
const headers = yield* resolved.route.auth.apply({
|
||||
request: LLM.request({ model: resolved, prompt: "Hello" }),
|
||||
method: "POST",
|
||||
url: "https://example.com",
|
||||
body: "{}",
|
||||
headers: Headers.empty,
|
||||
})
|
||||
|
||||
expect(resolved.route.id).toBe("openai-compatible-chat")
|
||||
expect(String(resolved.provider)).toBe("cloudflare-workers-ai")
|
||||
expect(resolved.route.endpoint.baseURL).toBe("https://api.cloudflare.com/client/v4/accounts/account%2Fid/ai/v1")
|
||||
expect(resolved.route.endpoint.query).toEqual({ version: "preview" })
|
||||
expect(resolved.route.defaults.providerOptions).toEqual({ openai: { reasoningEffort: "high" } })
|
||||
expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true } })
|
||||
const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" }))
|
||||
expect(prepared.body).toMatchObject({
|
||||
reasoning_effort: "high",
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
expect(prepared.body).not.toHaveProperty("accountId")
|
||||
expect(headers.authorization).toBe("Bearer secret")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = model(Provider.aisdk("@ai-sdk/openai"), {
|
||||
@@ -190,9 +151,6 @@ describe("ModelResolver", () => {
|
||||
http: { body: { custom_extension: { enabled: true } } },
|
||||
},
|
||||
})
|
||||
const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" }))
|
||||
expect(prepared.body.max_output_tokens).toBeUndefined()
|
||||
expect(JSON.stringify(prepared.body)).not.toContain("max_output_tokens")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Message, SystemPart } from "@opencode-ai/ai"
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import { DateTime, Deferred, Effect, Fiber, Schema } from "effect"
|
||||
import { Agent } from "@opencode-ai/core/agent"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Model } from "@opencode-ai/core/model"
|
||||
@@ -15,7 +15,7 @@ import { SessionPending } from "@opencode-ai/core/session/pending"
|
||||
import { Tool } from "@opencode-ai/core/tool"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import { define } from "@opencode-ai/plugin/promise/plugin"
|
||||
import type { SessionHooks } from "@opencode-ai/plugin/effect/session"
|
||||
import type { SessionHooks, SessionHttpHandler } from "@opencode-ai/plugin/effect/session"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
import { host as testHost } from "./host"
|
||||
@@ -223,45 +223,102 @@ describe("fromPromise", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("adapts promise session HTTP request and response hooks", () =>
|
||||
it.effect("adapts promise session HTTP hooks", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
const bodies: string[] = []
|
||||
yield* PluginPromise.fromPromise(
|
||||
define({
|
||||
id: "promise-session-http",
|
||||
setup: async (ctx) => {
|
||||
await ctx.session.hook("http", (event) => {
|
||||
event.use(async (request, next) => {
|
||||
request.headers.set("x-hook", "promise")
|
||||
await next(request)
|
||||
const response = await next(request)
|
||||
return new Response(`${await response.text()}-response`)
|
||||
})
|
||||
})
|
||||
await ctx.session.hook("http", (event) => {
|
||||
event.use(async (request, next) => {
|
||||
const response = await next(request)
|
||||
return new Response(`${await response.text()}-outer`)
|
||||
})
|
||||
})
|
||||
},
|
||||
}),
|
||||
).effect(host)
|
||||
const middlewares: Parameters<PluginHooks.Domains["session"]["http"]["use"]>[0][] = []
|
||||
const event: PluginHooks.Domains["session"]["http"] = {
|
||||
sessionID: Session.ID.make("ses_promise_session_http"),
|
||||
agent: Agent.ID.make("build"),
|
||||
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
|
||||
use: (item) =>
|
||||
Effect.sync(() => {
|
||||
middlewares.push(item)
|
||||
}),
|
||||
}
|
||||
|
||||
yield* hooks.trigger("session", "http", event)
|
||||
const request = middlewares.reduce<SessionHttpHandler>(
|
||||
(next, item) => (input: Request) => item(input, next),
|
||||
(input: Request) =>
|
||||
Effect.promise(() => input.text()).pipe(
|
||||
Effect.tap((body) => Effect.sync(() => bodies.push(body))),
|
||||
Effect.as(new Response(input.headers.get("x-hook") ?? "missing")),
|
||||
),
|
||||
)
|
||||
const response = yield* request(new Request("https://provider.test", { method: "POST", body: "payload" }))
|
||||
|
||||
expect(bodies).toEqual(["payload", "payload"])
|
||||
expect(yield* Effect.promise(() => response.text())).toBe("promise-response-outer")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("interrupts the Effect request through a promise session HTTP hook", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* PluginPromise.fromPromise(
|
||||
define({
|
||||
id: "promise-session-http",
|
||||
id: "promise-session-http-interrupt",
|
||||
setup: async (ctx) => {
|
||||
await ctx.session.hook("http.request", (event) => {
|
||||
event.request = new Request("https://provider.test/changed", event.request)
|
||||
event.request.headers.set("x-hook", "promise")
|
||||
})
|
||||
await ctx.session.hook("http.response", async (event) => {
|
||||
event.response = new Response(`${await event.response.text()}-response`, {
|
||||
status: event.response.status,
|
||||
})
|
||||
await ctx.session.hook("http", (event) => {
|
||||
event.use((request, next) => next(request))
|
||||
})
|
||||
},
|
||||
}),
|
||||
).effect(host)
|
||||
const context = {
|
||||
sessionID: Session.ID.make("ses_promise_session_http"),
|
||||
const started = yield* Deferred.make<void>()
|
||||
const interrupted = yield* Deferred.make<void>()
|
||||
const middlewares: Parameters<PluginHooks.Domains["session"]["http"]["use"]>[0][] = []
|
||||
const event: PluginHooks.Domains["session"]["http"] = {
|
||||
sessionID: Session.ID.make("ses_promise_session_http_interrupt"),
|
||||
agent: Agent.ID.make("build"),
|
||||
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
|
||||
use: (item) =>
|
||||
Effect.sync(() => {
|
||||
middlewares.push(item)
|
||||
}),
|
||||
}
|
||||
|
||||
const request = yield* hooks.trigger("session", "http.request", {
|
||||
...context,
|
||||
request: new Request("https://provider.test", { method: "POST", body: "payload" }),
|
||||
})
|
||||
const response = yield* hooks.trigger("session", "http.response", {
|
||||
...context,
|
||||
request: request.request,
|
||||
response: new Response(request.request.headers.get("x-hook") ?? "missing"),
|
||||
})
|
||||
yield* hooks.trigger("session", "http", event)
|
||||
const request = middlewares.reduce<SessionHttpHandler>(
|
||||
(next, item) => (input: Request) => item(input, next),
|
||||
() =>
|
||||
Deferred.succeed(started, undefined).pipe(
|
||||
Effect.andThen(Effect.never),
|
||||
Effect.onInterrupt(() => Deferred.succeed(interrupted, undefined)),
|
||||
),
|
||||
)
|
||||
const fiber = yield* request(new Request("https://provider.test")).pipe(Effect.forkChild)
|
||||
yield* Deferred.await(started)
|
||||
yield* Fiber.interrupt(fiber)
|
||||
|
||||
expect(request.request.url).toBe("https://provider.test/changed")
|
||||
expect(yield* Effect.promise(() => response.response.text())).toBe("promise-response")
|
||||
expect(yield* Deferred.isDone(interrupted)).toBeTrue()
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Model } from "@opencode-ai/core/model"
|
||||
import { Plugin } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
@@ -12,7 +15,9 @@ const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
yield* CloudflareWorkersAIPlugin.effect(yield* PluginHost.make(plugin))
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* CloudflareWorkersAIPlugin.effect(host)
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
@@ -20,96 +25,243 @@ function required<T>(value: T | undefined): T {
|
||||
return value
|
||||
}
|
||||
|
||||
function withEnv<A, E, R>(value: string | undefined, effect: () => Effect.Effect<A, E, R>) {
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = process.env.CLOUDFLARE_ACCOUNT_ID
|
||||
if (value === undefined) delete process.env.CLOUDFLARE_ACCOUNT_ID
|
||||
else process.env.CLOUDFLARE_ACCOUNT_ID = value
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
return previous
|
||||
}),
|
||||
effect,
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
if (previous === undefined) delete process.env.CLOUDFLARE_ACCOUNT_ID
|
||||
else process.env.CLOUDFLARE_ACCOUNT_ID = previous
|
||||
}),
|
||||
Effect.sync(() =>
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const providerID = Provider.ID.make("cloudflare-workers-ai")
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
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("resolves the account environment variable into the native endpoint", () =>
|
||||
withEnv("account/id", () =>
|
||||
it.effect("maps account ID to endpoint URL and creates an OpenAI-compatible SDK", () =>
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((draft) =>
|
||||
draft.provider.update(providerID, (provider) => {
|
||||
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||
provider.package = Provider.aisdk("test-provider")
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
const provider = required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai")))
|
||||
const sdk = yield* aisdk.runSDK({
|
||||
model: Model.Info.make({
|
||||
...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")),
|
||||
modelID: Model.ID.make("@cf/model"),
|
||||
package: provider.package,
|
||||
settings: provider.settings,
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
|
||||
})
|
||||
expect(provider).toMatchObject({
|
||||
package: "aisdk:test-provider",
|
||||
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1" },
|
||||
})
|
||||
expect(sdk.sdk).toBeDefined()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(required(yield* catalog.provider.get(providerID))).toMatchObject({
|
||||
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/account%2Fid/ai/v1" },
|
||||
headers: { "User-Agent": expect.stringContaining("cloudflare-workers-ai") },
|
||||
it.effect("preserves a configured endpoint URL instead of deriving one from account ID", () =>
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||
provider.package = Provider.aisdk("test-provider")
|
||||
provider.settings = { ...provider.settings, baseURL: "https://proxy.example/v1" }
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai")))).toMatchObject({
|
||||
package: "aisdk:test-provider",
|
||||
settings: { baseURL: "https://proxy.example/v1" },
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("resolves an account ID from provider settings", () =>
|
||||
withEnv(undefined, () =>
|
||||
it.effect("allows a configured baseURL without account ID", () =>
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* aisdk.runSDK({
|
||||
model: Model.Info.make({
|
||||
...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")),
|
||||
modelID: Model.ID.make("@cf/model"),
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
settings: { baseURL: "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")
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("uses env account ID over configured account ID", () =>
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((draft) =>
|
||||
draft.provider.update(providerID, (provider) => {
|
||||
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.settings = { accountId: "configured/account" }
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||
provider.package = Provider.aisdk("test-provider")
|
||||
provider.settings = { ...provider.settings, accountId: "configured-acct" }
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai")))).toMatchObject({
|
||||
package: "aisdk:test-provider",
|
||||
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1" },
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(required(yield* catalog.provider.get(providerID)).settings?.baseURL).toBe(
|
||||
"https://api.cloudflare.com/client/v4/accounts/configured%2Faccount/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* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* aisdk.runSDK({
|
||||
model: Model.Info.make({
|
||||
...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")),
|
||||
modelID: Model.ID.make("@cf/model"),
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
settings: { baseURL: "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* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* aisdk.runSDK({
|
||||
model: Model.Info.make({
|
||||
...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")),
|
||||
modelID: Model.ID.make("@cf/model"),
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
settings: { baseURL: "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("expands account placeholders and preserves configured endpoints", () =>
|
||||
withEnv("env-account", () =>
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((draft) =>
|
||||
draft.provider.update(providerID, (provider) => {
|
||||
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.settings = {
|
||||
baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||
}
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(providerID)).settings?.baseURL).toBe(
|
||||
"https://api.cloudflare.com/client/v4/accounts/env-account/ai/v1",
|
||||
)
|
||||
}),
|
||||
),
|
||||
it.effect("selects languageModel with the API model ID", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
const result = yield* aisdk.runLanguage({
|
||||
model: Model.Info.make({
|
||||
...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("alias")),
|
||||
modelID: Model.ID.make("@cf/api-model"),
|
||||
package: "aisdk:test-provider",
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
})
|
||||
expect(result.language).toBeDefined()
|
||||
expect(calls).toEqual(["languageModel:@cf/api-model"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves a custom endpoint without an account ID", () =>
|
||||
withEnv(undefined, () =>
|
||||
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 catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((draft) =>
|
||||
draft.provider.update(providerID, (provider) => {
|
||||
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.settings = { baseURL: "https://proxy.example/v1" }
|
||||
}),
|
||||
)
|
||||
const plugin = yield* Plugin.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(providerID)).settings?.baseURL).toBe("https://proxy.example/v1")
|
||||
const result = yield* aisdk.runSDK({
|
||||
model: Model.Info.make({
|
||||
...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")),
|
||||
modelID: Model.ID.make("@cf/model"),
|
||||
package: "aisdk:@ai-sdk/anthropic",
|
||||
settings: { baseURL: "https://proxy.example/v1" },
|
||||
}),
|
||||
package: "@ai-sdk/anthropic",
|
||||
options: { name: "cloudflare-workers-ai" },
|
||||
})
|
||||
expect(result.sdk).toBeUndefined()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
|
||||
import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import type { SessionHttpHandler } from "@opencode-ai/plugin/effect/session"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
@@ -30,13 +31,26 @@ function required<T>(value: T | undefined): T {
|
||||
}
|
||||
|
||||
const http = Effect.fn(function* (providerID: Provider.ID, url: string) {
|
||||
const event = yield* (yield* PluginHooks.Service).trigger("session", "http.request", {
|
||||
const middlewares: Parameters<PluginHooks.Domains["session"]["http"]["use"]>[0][] = []
|
||||
yield* (yield* PluginHooks.Service).trigger("session", "http", {
|
||||
sessionID: Session.ID.make("ses_test"),
|
||||
agent: Agent.ID.make("build"),
|
||||
model: Model.Ref.make({ providerID, id: Model.ID.make("gpt-5.5") }),
|
||||
request: new Request(url, { method: "POST", body: "{}" }),
|
||||
use: (item) =>
|
||||
Effect.sync(() => {
|
||||
middlewares.push(item)
|
||||
}),
|
||||
})
|
||||
return { url: event.request.url, headers: Object.fromEntries(event.request.headers.entries()) }
|
||||
const request = middlewares.reduce<SessionHttpHandler>(
|
||||
(next, item) => (input: Request) => item(input, next),
|
||||
(input: Request) => {
|
||||
const headers = new Headers(input.headers)
|
||||
headers.set("x-seen-url", input.url)
|
||||
return Effect.succeed(new Response(null, { headers }))
|
||||
},
|
||||
)
|
||||
const response = yield* request(new Request(url, { method: "POST", body: "{}" }))
|
||||
return { url: response.headers.get("x-seen-url"), headers: Object.fromEntries(response.headers.entries()) }
|
||||
})
|
||||
|
||||
describe("OpenAIPlugin", () => {
|
||||
@@ -126,7 +140,7 @@ describe("OpenAIPlugin", () => {
|
||||
const eligible = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
|
||||
expect(eligible.package).toBe("@opencode-ai/ai/providers/openai")
|
||||
expect(eligible.cost).toEqual([])
|
||||
expect(eligible.limit).toEqual({ context: 400_000, input: 272_000, output: 128_000 })
|
||||
expect(eligible.limit).toEqual({ context: 272_000, input: 272_000, output: 128_000 })
|
||||
expect(eligible.enabled).toBe(true)
|
||||
expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5-pro"))).enabled).toBe(
|
||||
false,
|
||||
@@ -135,14 +149,14 @@ describe("OpenAIPlugin", () => {
|
||||
false,
|
||||
)
|
||||
expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.4"))).limit).toEqual({
|
||||
context: 400_000,
|
||||
context: 272_000,
|
||||
input: 272_000,
|
||||
output: 64_000,
|
||||
})
|
||||
expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.6"))).enabled).toBe(false)
|
||||
const gpt56 = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.6-sol")))
|
||||
expect(gpt56.enabled).toBe(true)
|
||||
expect(gpt56.limit).toEqual({ context: 400_000, input: 272_000, output: 128_000 })
|
||||
expect(gpt56.limit).toEqual({ context: 272_000, input: 272_000, output: 128_000 })
|
||||
expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-4.1"))).enabled).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -255,7 +255,6 @@ describe("SessionRunnerLLM recorded", () => {
|
||||
describe("SessionModelRequest HTTP bridge", () => {
|
||||
const bodies: Uint8Array[] = []
|
||||
const methods: string[] = []
|
||||
const headers: Array<string | undefined> = []
|
||||
const response = [
|
||||
'data: {"id":"chatcmpl_test","object":"chat.completion.chunk","created":0,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello!"},"finish_reason":null}]}',
|
||||
'data: {"id":"chatcmpl_test","object":"chat.completion.chunk","created":0,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}',
|
||||
@@ -269,7 +268,6 @@ describe("SessionModelRequest HTTP bridge", () => {
|
||||
if (request.body._tag !== "Uint8Array") throw new Error(`Unexpected request body: ${request.body._tag}`)
|
||||
methods.push(request.method)
|
||||
bodies.push(request.body.body.slice())
|
||||
headers.push(request.headers["x-hook"])
|
||||
return HttpClientResponse.fromWeb(
|
||||
request,
|
||||
new Response(response, { headers: { "content-type": "text/event-stream" } }),
|
||||
@@ -277,16 +275,14 @@ describe("SessionModelRequest HTTP bridge", () => {
|
||||
}),
|
||||
),
|
||||
)
|
||||
const httpIt = testEffect(
|
||||
const retryIt = testEffect(
|
||||
testLayer(LLMClient.layer.pipe(Layer.provide(RequestExecutor.layer.pipe(Layer.provide(transport))))),
|
||||
)
|
||||
|
||||
httpIt.effect("runs Effect HTTP request and response hooks around one provider request", () =>
|
||||
retryIt.effect("lets an Effect plugin send the same POST Request twice", () =>
|
||||
Effect.gen(function* () {
|
||||
bodies.length = 0
|
||||
methods.length = 0
|
||||
headers.length = 0
|
||||
const seen: string[] = []
|
||||
const agents = yield* Agent.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
const hooks = yield* PluginHooks.Service
|
||||
@@ -301,20 +297,13 @@ describe("SessionModelRequest HTTP bridge", () => {
|
||||
catalog: catalogHost(catalog),
|
||||
session: { hook: (name, callback) => hooks.register("session", name, callback) },
|
||||
})
|
||||
yield* pluginHost.session.hook("http.request", (event) =>
|
||||
Effect.sync(() => {
|
||||
seen.push("request")
|
||||
event.request.headers.set("x-hook", "effect")
|
||||
}),
|
||||
)
|
||||
yield* pluginHost.session.hook("http.response", (event) =>
|
||||
Effect.gen(function* () {
|
||||
seen.push(`response:${event.response.status}:${event.request.headers.get("x-hook")}`)
|
||||
event.response = new Response(
|
||||
(yield* Effect.promise(() => event.response.text())).replace("Hello!", "Hooked!"),
|
||||
event.response,
|
||||
)
|
||||
}),
|
||||
yield* pluginHost.session.hook("http", (event) =>
|
||||
event.use((request, next) =>
|
||||
Effect.gen(function* () {
|
||||
yield* next(request).pipe(Effect.flatMap((response) => Effect.promise(() => response.text())))
|
||||
return yield* next(request)
|
||||
}),
|
||||
),
|
||||
)
|
||||
yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), { discard: true })
|
||||
const { db } = yield* Database.Service
|
||||
@@ -342,15 +331,10 @@ describe("SessionModelRequest HTTP bridge", () => {
|
||||
|
||||
yield* session.resume(retrySessionID)
|
||||
|
||||
expect(methods).toEqual(["POST"])
|
||||
expect(headers).toEqual(["effect"])
|
||||
expect(seen).toEqual(["request", "response:200:effect"])
|
||||
expect(bodies).toHaveLength(1)
|
||||
expect(methods).toEqual(["POST", "POST"])
|
||||
expect(bodies).toHaveLength(2)
|
||||
expect(bodies[0]?.byteLength).toBeGreaterThan(0)
|
||||
expect((yield* session.context(retrySessionID))[1]).toMatchObject({
|
||||
type: "assistant",
|
||||
content: [{ type: "text", text: "Hooked!" }],
|
||||
})
|
||||
expect(bodies[1]).toEqual(bodies[0])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noUncheckedIndexedAccess": false
|
||||
}
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"incremental": true,
|
||||
"noEmit": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"outDir": "node_modules/.ts-dist/source",
|
||||
"rootDir": "src",
|
||||
"tsBuildInfoFile": "node_modules/.ts-dist/source.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"tsBuildInfoFile": "node_modules/.ts-dist/tests.tsbuildinfo"
|
||||
},
|
||||
"include": ["drizzle.config.ts", "script", "test"],
|
||||
"references": [{ "path": "./tsconfig.json" }]
|
||||
}
|
||||
@@ -110,12 +110,12 @@ export type SQLiteEffectDelete<
|
||||
export type AnySQLiteEffectDelete = SQLiteEffectDeleteBase<any, any, any, any, any, any>
|
||||
|
||||
export interface SQLiteEffectDeleteBase<
|
||||
TTable extends SQLiteTable,
|
||||
TRunResult,
|
||||
TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
out TTable extends SQLiteTable,
|
||||
out TRunResult,
|
||||
out TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
out TDynamic extends boolean = false,
|
||||
_TExcludedMethods extends string = never,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
out TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> extends RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">,
|
||||
SQLWrapper,
|
||||
Effect.Effect<
|
||||
@@ -137,12 +137,12 @@ export interface SQLiteEffectDeleteBase<
|
||||
}
|
||||
|
||||
export class SQLiteEffectDeleteBase<
|
||||
TTable extends SQLiteTable,
|
||||
TRunResult,
|
||||
TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
out TTable extends SQLiteTable,
|
||||
out TRunResult,
|
||||
out TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
out TDynamic extends boolean = false,
|
||||
_TExcludedMethods extends string = never,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
out TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
>
|
||||
implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper
|
||||
{
|
||||
|
||||
@@ -126,9 +126,9 @@ export type SQLiteEffectInsert<
|
||||
export type AnySQLiteEffectInsert = SQLiteEffectInsertBase<any, any, any, any, any, any>
|
||||
|
||||
export class SQLiteEffectInsertBuilder<
|
||||
TTable extends SQLiteTable,
|
||||
TRunResult,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
in out TTable extends SQLiteTable,
|
||||
out TRunResult,
|
||||
out TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> {
|
||||
static readonly [entityKind]: string = "SQLiteEffectInsertBuilder"
|
||||
|
||||
@@ -194,12 +194,12 @@ export class SQLiteEffectInsertBuilder<
|
||||
}
|
||||
|
||||
export interface SQLiteEffectInsertBase<
|
||||
TTable extends SQLiteTable,
|
||||
TRunResult,
|
||||
TReturning = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
in out TTable extends SQLiteTable,
|
||||
out TRunResult,
|
||||
out TReturning = undefined,
|
||||
out TDynamic extends boolean = false,
|
||||
_TExcludedMethods extends string = never,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
out TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> extends SQLWrapper,
|
||||
RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">,
|
||||
Effect.Effect<
|
||||
@@ -221,12 +221,12 @@ export interface SQLiteEffectInsertBase<
|
||||
}
|
||||
|
||||
export class SQLiteEffectInsertBase<
|
||||
TTable extends SQLiteTable,
|
||||
TRunResult,
|
||||
TReturning = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
in out TTable extends SQLiteTable,
|
||||
out TRunResult,
|
||||
out TReturning = undefined,
|
||||
out TDynamic extends boolean = false,
|
||||
_TExcludedMethods extends string = never,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
out TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
>
|
||||
implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper
|
||||
{
|
||||
|
||||
@@ -19,9 +19,9 @@ import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
|
||||
import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
|
||||
|
||||
export class SQLiteEffectRelationalQueryBuilder<
|
||||
TSchema extends TablesRelationalConfig,
|
||||
out TSchema extends TablesRelationalConfig,
|
||||
TFields extends TableRelationalConfig,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
out TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> {
|
||||
static readonly [entityKind]: string = "SQLiteEffectRelationalQueryBuilderV2"
|
||||
|
||||
|
||||
@@ -152,18 +152,18 @@ export interface SQLiteEffectSelectHKT<TEffectHKT extends QueryEffectHKTBase = Q
|
||||
}
|
||||
|
||||
export interface SQLiteEffectSelectBase<
|
||||
TTableName extends string | undefined,
|
||||
TRunResult,
|
||||
TSelection extends ColumnsSelection,
|
||||
TSelectMode extends SelectMode = "single",
|
||||
TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string
|
||||
out TTableName extends string | undefined,
|
||||
out TRunResult,
|
||||
out TSelection extends ColumnsSelection,
|
||||
out TSelectMode extends SelectMode = "single",
|
||||
out TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string
|
||||
? Record<TTableName, "not-null">
|
||||
: {},
|
||||
TDynamic extends boolean = false,
|
||||
out TDynamic extends boolean = false,
|
||||
TExcludedMethods extends string = never,
|
||||
TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
|
||||
TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
out TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
|
||||
out TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
|
||||
out TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> extends SQLiteSelectQueryBuilderBase<
|
||||
SQLiteEffectSelectHKT<TEffectHKT>,
|
||||
TTableName,
|
||||
@@ -180,18 +180,18 @@ export interface SQLiteEffectSelectBase<
|
||||
Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]> {}
|
||||
|
||||
export class SQLiteEffectSelectBase<
|
||||
TTableName extends string | undefined,
|
||||
TRunResult,
|
||||
TSelection extends ColumnsSelection,
|
||||
TSelectMode extends SelectMode = "single",
|
||||
TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string
|
||||
out TTableName extends string | undefined,
|
||||
out TRunResult,
|
||||
out TSelection extends ColumnsSelection,
|
||||
out TSelectMode extends SelectMode = "single",
|
||||
out TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string
|
||||
? Record<TTableName, "not-null">
|
||||
: {},
|
||||
TDynamic extends boolean = false,
|
||||
out TDynamic extends boolean = false,
|
||||
TExcludedMethods extends string = never,
|
||||
TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
|
||||
TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
out TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
|
||||
out TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
|
||||
out TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
>
|
||||
extends SQLiteSelectQueryBuilderBase<
|
||||
SQLiteEffectSelectHKT<TEffectHKT>,
|
||||
|
||||
@@ -158,9 +158,9 @@ export type SQLiteEffectUpdateJoinFn<T extends AnySQLiteEffectUpdate> = <
|
||||
) => T
|
||||
|
||||
export class SQLiteEffectUpdateBuilder<
|
||||
TTable extends SQLiteTable,
|
||||
TRunResult,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
in out TTable extends SQLiteTable,
|
||||
out TRunResult,
|
||||
out TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> {
|
||||
static readonly [entityKind]: string = "SQLiteEffectUpdateBuilder"
|
||||
|
||||
@@ -193,13 +193,13 @@ export class SQLiteEffectUpdateBuilder<
|
||||
}
|
||||
|
||||
export interface SQLiteEffectUpdateBase<
|
||||
TTable extends SQLiteTable = SQLiteTable,
|
||||
TRunResult = unknown,
|
||||
TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
|
||||
TReturning = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
out TTable extends SQLiteTable = SQLiteTable,
|
||||
out TRunResult = unknown,
|
||||
out TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
|
||||
out TReturning = undefined,
|
||||
out TDynamic extends boolean = false,
|
||||
_TExcludedMethods extends string = never,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
out TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
> extends SQLWrapper,
|
||||
RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">,
|
||||
Effect.Effect<
|
||||
@@ -222,13 +222,13 @@ export interface SQLiteEffectUpdateBase<
|
||||
}
|
||||
|
||||
export class SQLiteEffectUpdateBase<
|
||||
TTable extends SQLiteTable = SQLiteTable,
|
||||
TRunResult = unknown,
|
||||
TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
|
||||
TReturning = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
out TTable extends SQLiteTable = SQLiteTable,
|
||||
out TRunResult = unknown,
|
||||
out TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
|
||||
out TReturning = undefined,
|
||||
out TDynamic extends boolean = false,
|
||||
_TExcludedMethods extends string = never,
|
||||
TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
out TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
|
||||
>
|
||||
implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Message, SystemPart } from "@opencode-ai/ai"
|
||||
import type { Agent } from "@opencode-ai/schema/agent"
|
||||
import type { Model } from "@opencode-ai/schema/model"
|
||||
import type { Session } from "@opencode-ai/schema/session"
|
||||
import type { JsonSchema } from "effect"
|
||||
import type { Effect, JsonSchema } from "effect"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface SessionContext {
|
||||
@@ -15,25 +15,23 @@ export interface SessionContext {
|
||||
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
||||
}
|
||||
|
||||
export interface SessionHttpRequest {
|
||||
export interface SessionHttp {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
request: Request
|
||||
readonly use: (middleware: SessionHttpMiddleware) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export interface SessionHttpResponse {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
readonly request: Request
|
||||
response: Response
|
||||
}
|
||||
export type SessionHttpHandler = (request: Request) => Effect.Effect<Response, Error>
|
||||
|
||||
export type SessionHttpMiddleware = (
|
||||
request: Request,
|
||||
next: SessionHttpHandler,
|
||||
) => Effect.Effect<Response, Error>
|
||||
|
||||
export interface SessionHooks {
|
||||
readonly context: SessionContext
|
||||
readonly "http.request": SessionHttpRequest
|
||||
readonly "http.response": SessionHttpResponse
|
||||
readonly http: SessionHttp
|
||||
}
|
||||
|
||||
export type SessionDomain = Pick<
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Session } from "@opencode-ai/schema/session"
|
||||
import type { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import type { Hooks, Transform } from "./registration.js"
|
||||
|
||||
interface ToolDraft {
|
||||
export interface ToolDraft {
|
||||
add<
|
||||
Input extends Tool.ValueSchema<any>,
|
||||
Output extends Tool.ValueSchema<any> | undefined,
|
||||
|
||||
@@ -15,25 +15,23 @@ export interface SessionContext {
|
||||
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
||||
}
|
||||
|
||||
export interface SessionHttpRequest {
|
||||
export interface SessionHttp {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
request: Request
|
||||
readonly use: (middleware: SessionHttpMiddleware) => void
|
||||
}
|
||||
|
||||
export interface SessionHttpResponse {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
readonly request: Request
|
||||
response: Response
|
||||
}
|
||||
export type SessionHttpHandler = (request: Request) => Promise<Response>
|
||||
|
||||
export type SessionHttpMiddleware = (
|
||||
request: Request,
|
||||
next: SessionHttpHandler,
|
||||
) => Promise<Response> | Response
|
||||
|
||||
export interface SessionHooks {
|
||||
readonly context: SessionContext
|
||||
readonly "http.request": SessionHttpRequest
|
||||
readonly "http.response": SessionHttpResponse
|
||||
readonly http: SessionHttp
|
||||
}
|
||||
|
||||
export type SessionDomain = Pick<
|
||||
|
||||
+8
-16
@@ -246,27 +246,19 @@ Runtime hooks intercept live operations:
|
||||
| `ctx.aisdk.hook("sdk", callback)` | `sdk`, after inspecting `model`, `package`, and `options` |
|
||||
| `ctx.aisdk.hook("language", callback)` | `language`, after inspecting `model`, `sdk`, and `options` |
|
||||
| `ctx.session.hook("context", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch |
|
||||
| `ctx.session.hook("http.request", callback)` | `request`, immediately before provider dispatch |
|
||||
| `ctx.session.hook("http.response", callback)` | `response`, immediately after the provider responds |
|
||||
| `ctx.session.hook("http", callback)` | `use`, registering request and response handling |
|
||||
| `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes |
|
||||
| `ctx.tool.hook("execute.after", callback)` | Terminal `result` on success or `error` on failure |
|
||||
|
||||
HTTP hooks can modify requests and responses. They apply to native models; AI
|
||||
SDK models do not currently pass through these hooks. Request and response
|
||||
bodies are one-shot streams. Use `clone()` when you intentionally need a
|
||||
separate reader, but be aware that its slower branch may buffer data. To inspect
|
||||
or modify chunks while preserving streaming, replace the body with one piped
|
||||
through a `TransformStream`.
|
||||
HTTP hooks can modify requests, inspect responses, retry, or return a
|
||||
response without calling the provider. It applies to native models; AI SDK
|
||||
models do not currently pass through this hook.
|
||||
|
||||
```ts
|
||||
await ctx.session.hook("http.request", (event) => {
|
||||
event.request.headers.set("x-session-id", event.sessionID)
|
||||
})
|
||||
|
||||
await ctx.session.hook("http.response", (event) => {
|
||||
event.response = new Response(event.response.body, {
|
||||
status: event.response.status,
|
||||
headers: { ...Object.fromEntries(event.response.headers), "x-plugin": "enabled" },
|
||||
await ctx.session.hook("http", (event) => {
|
||||
event.use((request, next) => {
|
||||
request.headers.set("x-session-id", event.sessionID)
|
||||
return next(request)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user