workers-ai-provider
Version:
Workers AI Provider for the vercel AI SDK
130 lines (108 loc) • 3.68 kB
text/typescript
import type { LanguageModelV3, SharedV3Warning } from "@ai-sdk/provider";
import type { AISearchChatSettings } from "./aisearch-chat-settings";
import { convertToWorkersAIChatMessages } from "./convert-to-workersai-chat-messages";
import { mapWorkersAIUsage } from "./map-workersai-usage";
import { getMappedStream, prependStreamStart } from "./streaming";
import { processToolCalls } from "./utils";
import type { TextGenerationModels } from "./workersai-models";
type AISearchChatConfig = {
provider: string;
binding: AutoRAG;
gateway?: GatewayOptions;
};
export class AISearchChatLanguageModel implements LanguageModelV3 {
readonly specificationVersion = "v3";
readonly defaultObjectGenerationMode = "json";
readonly supportedUrls: Record<string, RegExp[]> | PromiseLike<Record<string, RegExp[]>> = {};
readonly modelId: TextGenerationModels;
readonly settings: AISearchChatSettings;
private readonly config: AISearchChatConfig;
constructor(
modelId: TextGenerationModels,
settings: AISearchChatSettings,
config: AISearchChatConfig,
) {
this.modelId = modelId;
this.settings = settings;
this.config = config;
}
get provider(): string {
return this.config.provider;
}
private getWarnings({
tools,
frequencyPenalty,
presencePenalty,
responseFormat,
}: Parameters<LanguageModelV3["doGenerate"]>[0]): SharedV3Warning[] {
const warnings: SharedV3Warning[] = [];
if (tools != null && tools.length > 0) {
console.warn(
"[workers-ai-provider] Tools are not supported by AI Search. They will be ignored.",
);
warnings.push({ feature: "tools", type: "unsupported" });
}
if (frequencyPenalty != null) {
warnings.push({ feature: "frequencyPenalty", type: "unsupported" });
}
if (presencePenalty != null) {
warnings.push({ feature: "presencePenalty", type: "unsupported" });
}
if (responseFormat?.type === "json") {
warnings.push({ feature: "responseFormat", type: "unsupported" });
}
return warnings;
}
/**
* Build the search query from messages.
* Flattens the conversation into a single string for aiSearch.
*/
private buildQuery(prompt: Parameters<LanguageModelV3["doGenerate"]>[0]["prompt"]): string {
const { messages } = convertToWorkersAIChatMessages(prompt);
return messages.map(({ content, role }) => `${role}: ${content}`).join("\n\n");
}
async doGenerate(
options: Parameters<LanguageModelV3["doGenerate"]>[0],
): Promise<Awaited<ReturnType<LanguageModelV3["doGenerate"]>>> {
const warnings = this.getWarnings(options);
const query = this.buildQuery(options.prompt);
const output = await this.config.binding.aiSearch({ query });
return {
finishReason: { unified: "stop", raw: "stop" },
content: [
...output.data.map(({ file_id, filename, score }) => ({
type: "source" as const,
sourceType: "url" as const,
id: file_id,
url: filename,
providerMetadata: {
attributes: { score },
},
})),
{
type: "text" as const,
text: output.response,
},
...processToolCalls(output as unknown as Record<string, unknown>),
],
usage: mapWorkersAIUsage(output as unknown as Record<string, unknown>),
warnings,
};
}
async doStream(
options: Parameters<LanguageModelV3["doStream"]>[0],
): Promise<Awaited<ReturnType<LanguageModelV3["doStream"]>>> {
const warnings = this.getWarnings(options);
const query = this.buildQuery(options.prompt);
const response = await this.config.binding.aiSearch({
query,
stream: true,
});
return {
stream: prependStreamStart(
getMappedStream(response as unknown as Response | ReadableStream<Uint8Array>),
warnings,
),
};
}
}