ai-sdk-ollama
Version:
Vercel AI SDK Provider for Ollama using official ollama-js library
642 lines (635 loc) • 20.3 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/provider.browser.ts
import { NoSuchModelError } from "@ai-sdk/provider";
import { Ollama } from "ollama/browser";
// src/utils/convert-to-ollama-messages.ts
function convertToOllamaChatMessages(prompt) {
const messages = [];
for (const message of prompt) {
switch (message.role) {
case "system": {
messages.push({
role: "system",
content: message.content
});
break;
}
case "user": {
if (typeof message.content === "string") {
messages.push({
role: "user",
content: message.content
});
} else {
const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
const imageParts = message.content.filter(
(part) => part.type === "file"
).filter((part) => {
return part.mediaType?.startsWith("image/") || false;
}).map((part) => {
const imageData = part.data;
if (imageData instanceof URL) {
if (imageData.protocol === "data:") {
const base64Match = imageData.href.match(
/data:[^;]+;base64,(.+)/
);
if (base64Match) {
return base64Match[1];
}
return imageData.href;
}
return imageData.href;
} else if (typeof imageData === "string") {
if (imageData.startsWith("data:")) {
const base64Match = imageData.match(/data:[^;]+;base64,(.+)/);
if (base64Match) {
return base64Match[1];
}
}
return imageData;
} else if (imageData instanceof Uint8Array) {
return Buffer.from(imageData).toString("base64");
} else {
console.warn(
`Unsupported image data type: ${typeof imageData}`
);
return null;
}
}).filter((img) => img !== null);
messages.push({
role: "user",
content: textParts || "",
// Ensure content is never undefined
images: imageParts.length > 0 ? imageParts : void 0
});
}
break;
}
case "assistant": {
let content = "";
if (typeof message.content === "string") {
content = message.content;
} else {
const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
const reasoningParts = message.content.filter((part) => part.type === "reasoning").map((part) => part.text).join("\n");
content = [textParts, reasoningParts].filter(Boolean).join("\n");
const toolCalls = message.content.filter(
(part) => part.type === "tool-call"
);
if (toolCalls.length > 0) {
const toolCallText = toolCalls.map((tc) => `[Tool Call: ${tc.toolName}]`).join("\n");
if (toolCallText) {
content = content ? `${content}
${toolCallText}` : toolCallText;
}
}
}
messages.push({
role: "assistant",
content: content || ""
// Ensure content is never undefined
});
break;
}
case "tool": {
if (typeof message.content === "string") {
messages.push({
role: "user",
// Ollama doesn't have native tool role, so we use user
content: `[Tool Result]: ${message.content}`
});
} else {
const toolResultParts = message.content.filter((part) => part.type === "tool-result").map((part) => {
if (part.output.type === "text") {
return part.output.value;
} else if (part.output.type === "json") {
return JSON.stringify(part.output.value);
}
return String(part.output.value);
}).join("\n");
messages.push({
role: "user",
content: `[Tool Result]: ${toolResultParts || ""}`
});
}
break;
}
default: {
const role = message.role;
throw new Error(
`Unsupported message role: ${role}. Supported roles are: system, user, assistant, tool`
);
}
}
}
return messages;
}
// src/utils/map-ollama-finish-reason.ts
function mapOllamaFinishReason(reason) {
if (!reason) return "unknown";
switch (reason) {
case "stop": {
return "stop";
}
case "length": {
return "length";
}
default: {
return "unknown";
}
}
}
// src/utils/ollama-error.ts
var OllamaError = class _OllamaError extends Error {
constructor({
message,
cause,
data
}) {
super(message);
__publicField(this, "cause");
__publicField(this, "data");
this.name = "OllamaError";
this.cause = cause;
this.data = data;
}
static isOllamaError(error) {
return error instanceof _OllamaError;
}
};
// src/models/chat-language-model.ts
var OllamaChatLanguageModel = class {
constructor(modelId, settings, config) {
this.modelId = modelId;
this.settings = settings;
this.config = config;
__publicField(this, "specificationVersion", "v2");
__publicField(this, "defaultObjectGenerationMode", "json");
__publicField(this, "supportsImages", true);
// ✅ Ollama supports images (URLs, files, base64)
__publicField(this, "supportsVideoURLs", false);
// ❌ Not supported by Ollama API
__publicField(this, "supportsAudioURLs", false);
// ❌ Not supported by Ollama API
__publicField(this, "supportsVideoFile", false);
// ❌ Not supported by Ollama API
__publicField(this, "supportsAudioFile", false);
// ❌ Not supported by Ollama API
__publicField(this, "supportsImageFile", true);
// ✅ Already correct
__publicField(this, "supportedUrls", {
// Support common image URL patterns
image: [
/^https?:\/\/.*\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?.*)?$/i,
/^data:image\/[^;]+;base64,/i
// Data URLs
]
});
}
get provider() {
return this.config.provider;
}
get supportsStructuredOutputs() {
return this.settings.structuredOutputs ?? false;
}
/**
* Check if structured outputs should be enabled based on the call options
* This is used internally to auto-detect when structured outputs are needed
*/
shouldEnableStructuredOutputs(options) {
if (options.responseFormat?.type === "json" && options.responseFormat.schema) {
if (this.settings.structuredOutputs === false) {
console.warn(
"Ollama: structuredOutputs was set to false but auto-enabled for object generation. This ensures generateObject and streamObject work correctly."
);
}
return true;
}
if (this.settings.structuredOutputs !== void 0) {
return this.settings.structuredOutputs;
}
return false;
}
getCallOptions(options) {
const {
prompt,
temperature,
maxOutputTokens,
topP,
topK,
frequencyPenalty,
presencePenalty,
stopSequences,
seed,
responseFormat,
tools
} = options;
const warnings = [];
const needsStructuredOutputs = this.shouldEnableStructuredOutputs(options);
if (responseFormat?.type === "json" && responseFormat.schema && !needsStructuredOutputs) {
throw new Error(
"JSON schema is only supported when structuredOutputs is enabled"
);
}
const ollamaTools = tools ? tools.map((tool) => {
if (tool.type === "function") {
let jsonSchema;
if (tool.inputSchema && typeof tool.inputSchema === "object") {
if ("parse" in tool.inputSchema && typeof tool.inputSchema.parse === "function") {
console.warn(
`Tool ${tool.name} is using a Zod schema directly. Schema conversion may not work properly due to Zod version mismatch.`
);
jsonSchema = {
type: "object",
properties: {},
additionalProperties: false
};
} else if ("properties" in tool.inputSchema || "type" in tool.inputSchema) {
jsonSchema = tool.inputSchema;
} else {
jsonSchema = {
type: "object",
properties: {},
additionalProperties: false
};
}
} else {
jsonSchema = {
type: "object",
properties: {},
additionalProperties: false
};
}
return {
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: jsonSchema
}
};
}
throw new Error(
`Provider-defined tools are not supported by Ollama. Use function tools instead.`
);
}) : void 0;
const ollamaOptions = {
// Start with AI SDK parameters mapped to Ollama names
...temperature !== void 0 && { temperature },
...maxOutputTokens !== void 0 && { num_predict: maxOutputTokens },
...topP !== void 0 && { top_p: topP },
...topK !== void 0 && { top_k: topK },
...frequencyPenalty !== void 0 && {
frequency_penalty: frequencyPenalty
},
...presencePenalty !== void 0 && {
presence_penalty: presencePenalty
},
...stopSequences !== void 0 && { stop: stopSequences },
...seed !== void 0 && { seed },
// Ollama model options override AI SDK parameters
...this.settings.options
};
for (const key of Object.keys(ollamaOptions)) {
if (ollamaOptions[key] === void 0) {
delete ollamaOptions[key];
}
}
let format;
if (responseFormat?.type === "json") {
format = responseFormat.schema && needsStructuredOutputs ? responseFormat.schema : "json";
}
const messages = convertToOllamaChatMessages(prompt);
return {
messages,
options: ollamaOptions,
format,
tools: ollamaTools,
warnings
};
}
async doGenerate(options) {
const {
messages,
options: ollamaOptions,
format,
tools,
warnings
} = this.getCallOptions(options);
try {
const response = await this.config.client.chat({
model: this.modelId,
messages,
options: ollamaOptions,
format,
tools,
stream: false
});
const text = response.message.content;
const toolCalls = response.message.tool_calls;
const thinking = response.message.thinking;
const content = [];
if (thinking && this.settings.reasoning) {
content.push({ type: "reasoning", text: thinking });
}
if (text) {
content.push({ type: "text", text });
}
if (toolCalls && toolCalls.length > 0) {
for (const toolCall of toolCalls) {
const toolInput = toolCall.function.arguments || {};
content.push({
type: "tool-call",
toolCallId: crypto.randomUUID(),
// Ollama doesn't provide IDs
toolName: toolCall.function.name,
input: JSON.stringify(toolInput)
});
}
}
return {
content,
finishReason: mapOllamaFinishReason(
response.done_reason
),
usage: {
inputTokens: response.prompt_eval_count || 0,
outputTokens: response.eval_count || 0,
totalTokens: (response.prompt_eval_count || 0) + (response.eval_count || 0)
},
providerMetadata: {
ollama: {
model: response.model,
created_at: response.created_at ? new Date(response.created_at).toISOString() : void 0,
total_duration: response.total_duration,
load_duration: response.load_duration,
eval_duration: response.eval_duration
}
},
request: {
body: JSON.stringify({
model: this.modelId,
messages,
options: ollamaOptions,
format,
tools
})
},
response: {
timestamp: /* @__PURE__ */ new Date(),
modelId: this.modelId
},
warnings
};
} catch (error) {
throw new OllamaError({
message: error instanceof Error ? error.message : String(error),
cause: error
});
}
}
async doStream(options) {
const {
messages,
options: ollamaOptions,
format,
tools,
warnings
} = this.getCallOptions(options);
try {
const stream = await this.config.client.chat({
model: this.modelId,
messages,
options: ollamaOptions,
format,
tools,
stream: true
});
let usage = {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0
};
let finishReason = "unknown";
const reasoningEnabled = this.settings.reasoning;
const transformStream = new TransformStream({
async transform(chunk, controller) {
if (!chunk || typeof chunk !== "object") {
return;
}
if (chunk.done) {
if (chunk.message && typeof chunk.message.content === "string" && chunk.message.content.length > 0) {
controller.enqueue({
type: "text-delta",
id: crypto.randomUUID(),
delta: chunk.message.content
});
}
usage = {
inputTokens: chunk.prompt_eval_count || 0,
outputTokens: chunk.eval_count || 0,
totalTokens: (chunk.prompt_eval_count || 0) + (chunk.eval_count || 0)
};
finishReason = mapOllamaFinishReason(
chunk.done_reason
);
controller.enqueue({
type: "finish",
finishReason,
usage
});
} else {
if (chunk.message.thinking && reasoningEnabled) {
controller.enqueue({
type: "reasoning-start",
id: crypto.randomUUID()
});
controller.enqueue({
type: "reasoning-delta",
id: crypto.randomUUID(),
delta: chunk.message.thinking
});
controller.enqueue({
type: "reasoning-end",
id: crypto.randomUUID()
});
}
if (chunk.message.tool_calls && chunk.message.tool_calls.length > 0) {
for (const toolCall of chunk.message.tool_calls) {
const toolInput = toolCall.function.arguments || {};
controller.enqueue({
type: "tool-call",
toolCallId: crypto.randomUUID(),
// Ollama doesn't provide IDs
toolName: toolCall.function.name,
input: JSON.stringify(toolInput)
});
}
}
if (chunk.message.content && typeof chunk.message.content === "string" && chunk.message.content.length > 0) {
controller.enqueue({
type: "text-delta",
id: crypto.randomUUID(),
// Generate unique ID for each text chunk
delta: chunk.message.content
});
}
}
}
});
const readableStream = new ReadableStream({
async start(controller) {
try {
for await (const chunk of stream) {
if (chunk && typeof chunk === "object") {
controller.enqueue(chunk);
}
}
controller.close();
} catch (error) {
controller.error(error);
}
}
});
return {
stream: readableStream.pipeThrough(transformStream),
rawCall: {
rawPrompt: messages,
rawSettings: {
model: this.modelId,
options: ollamaOptions,
format,
tools
}
},
warnings: warnings.length > 0 ? warnings : void 0
};
} catch (error) {
throw new OllamaError({
message: error instanceof Error ? error.message : String(error),
cause: error
});
}
}
};
// src/models/embedding-model.ts
var OllamaEmbeddingModel = class {
constructor(modelId, settings, config) {
this.settings = settings;
this.config = config;
__publicField(this, "specificationVersion", "v2");
__publicField(this, "modelId");
__publicField(this, "maxEmbeddingsPerCall", 2048);
__publicField(this, "supportsParallelCalls", true);
this.modelId = modelId;
}
get provider() {
return this.config.provider;
}
async doEmbed(params) {
const { values, abortSignal } = params;
if (values.length > this.maxEmbeddingsPerCall) {
throw new OllamaError({
message: `Too many values to embed. Maximum: ${this.maxEmbeddingsPerCall}, Received: ${values.length}`
});
}
if (values.length === 0) {
return { embeddings: [] };
}
try {
const embeddings = [];
for (const value of values) {
if (value === void 0 || value === null) {
continue;
}
const response = await this.config.client.embed({
model: this.modelId,
input: value,
options: this.settings.options
});
if (!response.embeddings) {
throw new OllamaError({
message: `No embeddings field in response`
});
}
if (response.embeddings.length === 0) {
throw new OllamaError({
message: `Empty embeddings array returned`
});
}
embeddings.push(response.embeddings[0]);
if (abortSignal?.aborted) {
throw new Error("Embedding generation aborted");
}
}
if (embeddings.length === 0) {
throw new OllamaError({
message: `No valid values provided for embedding (all were undefined/null)`
});
}
return {
embeddings
};
} catch (error) {
if (error instanceof OllamaError) {
throw error;
}
throw new OllamaError({
message: error instanceof Error ? error.message : String(error),
cause: error
});
}
}
};
// src/provider.browser.ts
function createOllama(options = {}) {
const client = new Ollama({
host: options.baseURL,
fetch: options.fetch,
headers: options.headers
});
const createChatModel = (modelId, settings = {}) => {
return new OllamaChatLanguageModel(modelId, settings, {
client,
provider: "ollama"
});
};
const createEmbeddingModel = (modelId, settings = {}) => {
return new OllamaEmbeddingModel(modelId, settings, {
client,
provider: "ollama"
});
};
const provider = function(modelId, settings) {
if (new.target) {
throw new Error(
"The Ollama provider cannot be called with the new keyword."
);
}
return createChatModel(modelId, settings);
};
provider.chat = createChatModel;
provider.languageModel = createChatModel;
provider.embedding = createEmbeddingModel;
provider.textEmbedding = createEmbeddingModel;
provider.textEmbeddingModel = createEmbeddingModel;
provider.imageModel = (modelId) => {
throw new NoSuchModelError({
modelId,
modelType: "imageModel",
message: "Image generation is not supported by Ollama"
});
};
return provider;
}
var ollama = createOllama();
export {
OllamaChatLanguageModel,
OllamaEmbeddingModel,
OllamaError,
createOllama,
ollama
};
//# sourceMappingURL=index.browser.js.map