UNPKG

agents

Version:

A home for your AI agents

290 lines (289 loc) • 8.79 kB
import { t as createAI$1 } from "../../ai-sdk-Kon40i0q.js"; import { Model, Provider } from "@opencode/plugin"; //#region src/models/opencode/language-v3.ts /** * OpenCode loads AI SDK models through the `LanguageModelV3` contract; * `agents/models/ai-sdk` implements `LanguageModelV4`. The two differ only in * how files are carried (V4 wraps file data in a tagged union and folds the * image variants into one `file` part) and in the parts V4 added (`custom`, * `reasoning-file`), which V3 has no slot for and are dropped. Text, * reasoning, tool calls, usage and finish reasons are the same shape. */ function asLanguageModelV3(model) { return { specificationVersion: "v3", provider: model.provider, modelId: model.modelId, supportedUrls: model.supportedUrls, async doGenerate(options) { const result = await model.doGenerate(callOptions(options)); return { ...result, warnings: result.warnings.map(warning), content: result.content.flatMap((part) => { const converted = content(part); return converted === void 0 ? [] : [converted]; }) }; }, async doStream(options) { const result = await model.doStream(callOptions(options)); return { ...result, stream: result.stream.pipeThrough(new TransformStream({ transform(part, controller) { const converted = streamPart(part); if (converted !== void 0) controller.enqueue(converted); } })) }; } }; } function callOptions(options) { return { ...options, prompt: options.prompt.map(message) }; } function message(input) { switch (input.role) { case "system": return input; case "user": return { ...input, content: input.content.map((part) => part.type === "file" ? filePart(part) : part) }; case "assistant": return { ...input, content: input.content.map((part) => { if (part.type === "file") return filePart(part); if (part.type === "tool-result") return { ...part, output: toolOutput(part.output) }; return part; }) }; case "tool": return { ...input, content: input.content.map((part) => part.type === "tool-result" ? { ...part, output: toolOutput(part.output) } : part) }; } } function filePart(part) { return { ...part, data: fileData(part.data) }; } function fileData(data) { return data instanceof URL ? { type: "url", url: data } : { type: "data", data }; } function toolOutput(output) { if (output.type !== "content") return output; return { ...output, value: output.value.map((item) => { switch (item.type) { case "text": return item; case "file-data": case "image-data": return { type: "file", data: { type: "data", data: item.data }, mediaType: item.mediaType, ..."filename" in item && item.filename !== void 0 ? { filename: item.filename } : {}, providerOptions: item.providerOptions }; case "file-url": case "image-url": return { type: "file", data: { type: "url", url: new URL(item.url) }, mediaType: item.type === "image-url" ? "image/*" : "application/octet-stream", providerOptions: item.providerOptions }; case "file-id": case "image-file-id": return { type: "file", data: { type: "reference", reference: typeof item.fileId === "string" ? { id: item.fileId } : item.fileId }, mediaType: item.type === "image-file-id" ? "image/*" : "application/octet-stream", providerOptions: item.providerOptions }; case "custom": return { type: "custom", providerOptions: item.providerOptions }; } }) }; } function content(part) { switch (part.type) { case "custom": case "reasoning-file": return; case "file": return generatedFile(part); default: return part; } } function streamPart(part) { switch (part.type) { case "custom": case "reasoning-file": return; case "file": return generatedFile(part); case "stream-start": return { ...part, warnings: part.warnings.map(warning) }; default: return part; } } /** V4 added `deprecated` warnings; V3 reports them as `other`. */ function warning(input) { return input.type === "deprecated" ? { type: "other", message: `${input.setting}: ${input.message}` } : input; } function generatedFile(part) { if (part.data.type !== "data") return void 0; return { type: "file", mediaType: part.mediaType, data: part.data.data, ...part.providerMetadata === void 0 ? {} : { providerMetadata: part.providerMetadata } }; } //#endregion //#region src/models/opencode/index.ts /** * `agents/models/opencode` — Cloudflare's models as an OpenCode provider, with * the same shape as `agents/models/ai-sdk` and `agents/models/pi-ai`. * * ```ts * import { createAI } from "agents/models/opencode"; * import { OpenCodeWorkerd } from "@opencode/sdk/workerd"; * * const ai = createAI({ binding: env.AI }); * * const model = ai("@cf/moonshotai/kimi-k2.7-code"); // { providerID, id } * ai("anthropic/claude-opus-4.8"); // an AI Gateway catalog model * await OpenCodeWorkerd.create({ storage, plugins: [ai.plugin] }); * ``` * * `ai.plugin` is an OpenCode plugin. It adds a `cloudflare` provider whose * models are the ids you have called `ai(id)` with — Workers AI ids, AI * Gateway catalog ids and dynamic routes — and answers OpenCode's AI SDK hook * with `agents/models/ai-sdk`'s models, so requests go the way that * provider sends them: through the `AI` binding, or over HTTP with an account * id and API token. Nothing is patched globally. Catalog streams through the * binding resume after a dropped connection, as they do in * `agents/models/ai-sdk`. * * @experimental The surface may change in any release. * * @module */ /** The OpenCode provider id `createAI`'s plugin registers. */ const CLOUDFLARE_PROVIDER_ID = "cloudflare"; /** * The AI SDK package OpenCode is told the provider uses. OpenCode builds an * AI SDK model in two hook steps: `sdk` (load the package) and `language` * (make the model). Its `sdk` step can only load packages it bundles; any * other is handed to an npm installer that cannot run in workerd, and an * external plugin's `sdk` hook runs after that installer. So the provider * names `@ai-sdk/vercel`, which OpenCode bundles and loads in-process, and * this plugin's `language` hook replaces the model it would make. The * Vercel client is built but never called: its base URL does not resolve. */ const CARRIER_PACKAGE = "aisdk:@ai-sdk/vercel"; const UNREACHABLE_BASE_URL = "https://workers-ai.binding.invalid/v1"; const DEFAULT_LIMIT = { context: 128e3, output: 8192 }; /** * Create Cloudflare's model provider for OpenCode. * * @param settings - The `AI` binding, or an account id and API token, and * gateway defaults, as for `agents/models/ai-sdk`. * @returns The provider: call it with model ids; register `plugin`. */ function createAI(settings) { const aiSdk = createAI$1(settings); const models = /* @__PURE__ */ new Map(); /** One reload per OpenCode instance the plugin is set up in. */ const reloads = /* @__PURE__ */ new Set(); const providerID = Provider.ID.make(CLOUDFLARE_PROVIDER_ID); const info = (id, options) => { return { ...Model.Info.default(providerID, Model.ID.make(id)), package: CARRIER_PACKAGE, limit: options?.limit ?? DEFAULT_LIMIT }; }; const plugin = { id: "agents.models.opencode", async setup(ctx) { const transform = await ctx.provider.transform((editor) => { editor.add({ info: { id: providerID, name: "Cloudflare", activation: "enabled", package: CARRIER_PACKAGE, settings: { baseURL: UNREACHABLE_BASE_URL, apiKey: "binding" } }, models: [...models].map(([id, options]) => info(id, options)) }); }); const hook = await ctx.aisdk.hook("language", (event) => { const id = event.model.modelID ?? event.model.id; event.language = asLanguageModelV3(aiSdk.languageModel(id, models.get(id))); }, { providerID }); const reload = () => ctx.provider.reload(); reloads.add(reload); return async () => { reloads.delete(reload); await hook.dispose(); await transform.dispose(); }; } }; const model = (modelId, options) => { aiSdk.languageModel(modelId, options); const changed = !models.has(modelId) || JSON.stringify(models.get(modelId)?.limit) !== JSON.stringify(options?.limit); models.set(modelId, options); if (changed) for (const reload of reloads) reload().catch((error) => { console.warn("agents/models/opencode: provider reload failed", error); }); return { providerID: CLOUDFLARE_PROVIDER_ID, id: modelId }; }; return Object.assign(model, { model, plugin }); } //#endregion export { CLOUDFLARE_PROVIDER_ID, createAI }; //# sourceMappingURL=index.js.map