dify-ai-provider
Version:
Dify provider for Vercel AI SDK
385 lines (380 loc) • 11.8 kB
JavaScript
// src/dify-provider.ts
import { loadApiKey } from "@ai-sdk/provider-utils";
// src/dify-chat-language-model.ts
import {
APICallError
} from "@ai-sdk/provider";
import {
combineHeaders,
createEventSourceResponseHandler,
createJsonErrorResponseHandler,
createJsonResponseHandler,
generateId,
postJsonToApi
} from "@ai-sdk/provider-utils";
// src/dify-chat-schema.ts
import { z } from "zod";
var completionResponseSchema = z.object({
id: z.string(),
answer: z.string(),
task_id: z.string(),
conversation_id: z.string(),
message_id: z.string(),
metadata: z.object({
usage: z.object({
completion_tokens: z.number(),
prompt_tokens: z.number(),
total_tokens: z.number()
})
})
});
var errorResponseSchema = z.object({
code: z.string(),
message: z.string(),
status: z.number()
});
var difyStreamEventBase = z.object({
event: z.string(),
conversation_id: z.string().optional(),
message_id: z.string().optional(),
task_id: z.string().optional(),
created_at: z.number().optional()
}).passthrough();
var workflowStartedSchema = difyStreamEventBase.extend({
event: z.literal("workflow_started"),
workflow_run_id: z.string(),
data: z.object({
id: z.string(),
workflow_id: z.string(),
created_at: z.number()
}).passthrough()
});
var workflowFinishedSchema = difyStreamEventBase.extend({
event: z.literal("workflow_finished"),
workflow_run_id: z.string(),
data: z.object({
id: z.string(),
workflow_id: z.string(),
total_tokens: z.number().optional(),
created_at: z.number()
}).passthrough()
});
var nodeStartedSchema = difyStreamEventBase.extend({
event: z.literal("node_started"),
workflow_run_id: z.string(),
data: z.object({
id: z.string(),
node_id: z.string(),
node_type: z.string()
}).passthrough()
});
var nodeFinishedSchema = difyStreamEventBase.extend({
event: z.literal("node_finished"),
workflow_run_id: z.string(),
data: z.object({
id: z.string(),
node_id: z.string(),
node_type: z.string()
}).passthrough()
});
var messageSchema = difyStreamEventBase.extend({
event: z.literal("message"),
id: z.string().optional(),
answer: z.string(),
from_variable_selector: z.array(z.string()).optional()
});
var messageEndSchema = difyStreamEventBase.extend({
event: z.literal("message_end"),
id: z.string(),
metadata: z.object({
usage: z.object({
prompt_tokens: z.number(),
completion_tokens: z.number(),
total_tokens: z.number()
}).passthrough()
}).passthrough(),
files: z.array(z.unknown()).optional()
});
var ttsMessageSchema = difyStreamEventBase.extend({
event: z.literal("tts_message"),
audio: z.string()
});
var ttsMessageEndSchema = difyStreamEventBase.extend({
event: z.literal("tts_message_end"),
audio: z.string()
});
var agentMessageSchema = difyStreamEventBase.extend({
event: z.literal("agent_message"),
answer: z.string()
});
var agentThoughtSchema = difyStreamEventBase.extend({
event: z.literal("agent_thought"),
thought: z.string(),
observation: z.string(),
tool: z.string(),
tool_labels: z.record(z.string(), z.string()),
tool_input: z.string(),
message_files: z.array(z.unknown())
});
var difyStreamEventSchema = z.discriminatedUnion("event", [
workflowStartedSchema,
workflowFinishedSchema,
nodeStartedSchema,
nodeFinishedSchema,
messageSchema,
messageEndSchema,
ttsMessageSchema,
ttsMessageEndSchema,
agentThoughtSchema,
agentMessageSchema
]).or(difyStreamEventBase);
// src/dify-chat-language-model.ts
var difyFailedResponseHandler = createJsonErrorResponseHandler({
errorSchema: errorResponseSchema,
errorToMessage: (data) => {
console.log("Dify API error:", data);
return `Dify API error: ${data.message}`;
}
});
var DifyChatLanguageModel = class {
constructor(modelId, settings, config) {
this.settings = settings;
this.specificationVersion = "v1";
this.defaultObjectGenerationMode = void 0;
this.modelId = modelId;
this.config = config;
this.generateId = generateId;
this.chatMessagesEndpoint = `${this.config.baseURL}/chat-messages`;
if (!this.settings.responseMode) {
this.settings.responseMode = "streaming";
}
}
get provider() {
return this.config.provider;
}
async doGenerate(options) {
var _a, _b, _c, _d;
const { abortSignal } = options;
const requestBody = this.getRequestBody(options);
const { responseHeaders, value: data } = await postJsonToApi({
url: this.chatMessagesEndpoint,
headers: combineHeaders(this.config.headers(), options.headers),
body: requestBody,
abortSignal,
failedResponseHandler: difyFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(
completionResponseSchema
),
fetch: this.config.fetch
});
return {
text: data.answer,
toolCalls: [],
// Dify doesn't currently support tool calls
finishReason: "stop",
// Dify doesn't specify finish reason
usage: {
promptTokens: ((_b = (_a = data.metadata) == null ? void 0 : _a.usage) == null ? void 0 : _b.prompt_tokens) || 0,
completionTokens: ((_d = (_c = data.metadata) == null ? void 0 : _c.usage) == null ? void 0 : _d.completion_tokens) || 0
},
rawCall: this.createRawCall(options),
providerMetadata: {
difyWorkflowData: {
conversationId: data.conversation_id,
messageId: data.message_id
}
},
rawResponse: {
headers: responseHeaders,
body: data
},
request: { body: JSON.stringify(requestBody) },
response: {
id: data.id,
timestamp: /* @__PURE__ */ new Date()
}
};
}
async doStream(options) {
const { abortSignal } = options;
const requestBody = this.getRequestBody(options);
const body = { ...requestBody, response_mode: "streaming" };
const { responseHeaders, value: responseStream } = await postJsonToApi({
url: this.chatMessagesEndpoint,
headers: combineHeaders(this.config.headers(), options.headers),
body,
failedResponseHandler: difyFailedResponseHandler,
successfulResponseHandler: createEventSourceResponseHandler(
difyStreamEventSchema
),
abortSignal,
fetch: this.config.fetch
});
let conversationId;
let messageId;
let taskId;
return {
stream: responseStream.pipeThrough(
new TransformStream({
transform(chunk, controller) {
if (!chunk.success) {
controller.enqueue({ type: "error", error: chunk.error });
return;
}
const data = chunk.value;
if (data.conversation_id) conversationId = data.conversation_id;
if (data.message_id) messageId = data.message_id;
if (data.task_id) taskId = data.task_id;
switch (data.event) {
case "workflow_finished":
case "message_end": {
let totalTokens = 0;
if ("data" in data && data.data && typeof data.data === "object" && "total_tokens" in data.data && typeof data.data.total_tokens === "number") {
totalTokens = data.data.total_tokens;
}
controller.enqueue({
type: "finish",
finishReason: "stop",
providerMetadata: {
difyWorkflowData: {
conversationId,
messageId,
taskId
}
},
usage: {
promptTokens: 0,
completionTokens: totalTokens
}
});
break;
}
case "message":
case "agent_message": {
if ("answer" in data && typeof data.answer === "string") {
controller.enqueue({
type: "text-delta",
textDelta: data.answer
});
if ("id" in data && typeof data.id === "string") {
controller.enqueue({
type: "response-metadata",
id: data.id
});
}
}
break;
}
}
}
})
),
rawCall: this.createRawCall(options),
rawResponse: { headers: responseHeaders },
request: { body: JSON.stringify(body) }
};
}
/**
* Get the request body for the Dify API
*/
getRequestBody(options) {
var _a, _b, _c;
const messages = options.messages || options.prompt;
if (!messages || !messages.length) {
throw new APICallError({
message: "No messages provided",
url: this.chatMessagesEndpoint,
requestBodyValues: options
});
}
const latestMessage = messages[messages.length - 1];
if (latestMessage.role !== "user") {
throw new APICallError({
message: "The last message must be a user message",
url: this.chatMessagesEndpoint,
requestBodyValues: { latestMessageRole: latestMessage.role }
});
}
const hasAttachments = Array.isArray(latestMessage.content) && latestMessage.content.some((part) => {
return typeof part !== "string" && part.type === "image";
});
if (hasAttachments) {
throw new APICallError({
message: "Dify provider does not currently support image attachments",
url: this.chatMessagesEndpoint,
requestBodyValues: { hasAttachments: true }
});
}
let query = "";
if (typeof latestMessage.content === "string") {
query = latestMessage.content;
} else if (Array.isArray(latestMessage.content)) {
query = latestMessage.content.map((part) => {
if (typeof part === "string") {
return part;
} else if (part.type === "text") {
return part.text;
}
return "";
}).filter(Boolean).join(" ");
}
const conversationId = (_a = options.headers) == null ? void 0 : _a["chat-id"];
const userId = (_c = (_b = options.headers) == null ? void 0 : _b["user-id"]) != null ? _c : "you_should_pass_user-id";
const {
"chat-id": _,
"user-id": __,
...cleanHeaders
} = options.headers || {};
options.headers = cleanHeaders;
return {
inputs: this.settings.inputs || {},
query,
response_mode: this.settings.responseMode,
conversation_id: conversationId,
user: userId
};
}
/**
* Create the rawCall object for response
*/
createRawCall(options) {
return {
rawPrompt: options.messages || options.prompt,
rawSettings: { ...this.settings }
};
}
supportsUrl(url) {
return false;
}
};
// src/dify-provider.ts
function createDifyProvider(options = {}) {
const createChatModel = (modelId, settings = {}) => new DifyChatLanguageModel(modelId, settings, {
provider: "dify.chat",
baseURL: options.baseURL || "https://api.dify.ai/v1",
headers: () => ({
Authorization: `Bearer ${loadApiKey({
apiKey: settings.apiKey,
environmentVariableName: "DIFY_API_KEY",
description: "Dify API Key"
})}`,
"Content-Type": "application/json",
...options.headers
})
});
const provider = function(modelId, settings) {
if (new.target) {
throw new Error(
"The model factory function cannot be called with the new keyword."
);
}
return createChatModel(modelId, settings);
};
provider.chat = createChatModel;
return provider;
}
var difyProvider = createDifyProvider();
export {
createDifyProvider,
difyProvider
};