agents
Version:
A home for your AI agents
290 lines (289 loc) • 8.79 kB
JavaScript
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