Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c911076
feat(js/plugins/ollama): migrate ollama plugin to v2 plugin API
HassanBahati Sep 11, 2025
767750e
feat(js/plugins/ollama): migrate ollama plugin to v2 plugin API
HassanBahati Sep 11, 2025
c815682
feat(js/plugins/ollama): migrate ollama plugin to v2 plugin API
HassanBahati Sep 11, 2025
f4a8f56
feat(js/plugins/ollama): migrate ollama plugin to v2 plugin API
HassanBahati Sep 11, 2025
2de0a58
chore(js/plugins/ollama): format
HassanBahati Sep 11, 2025
f385bd9
chore(js/plugin/ollama): clean up
HassanBahati Sep 16, 2025
5f423cf
chore(js/plugins/ollama): migrate embeddings
HassanBahati Sep 18, 2025
4ce2782
chore(js/plugins/ollama): update types
HassanBahati Sep 18, 2025
f515fad
test(js/plugins/ollama): update tests
HassanBahati Sep 18, 2025
29d8c84
feat(js/plugins/ollama): migrate ollama plugin to v2 plugins API
HassanBahati Sep 18, 2025
7015e4e
chore(js/plugins/ollama): format
HassanBahati Sep 18, 2025
9f1d182
tests(js/plugins/ollama): clean up
HassanBahati Sep 18, 2025
825ac3c
chore(js/plugins/ollama): clean up
HassanBahati Sep 18, 2025
3546697
chore(js/plugins/ollama): add back mock tool call response
HassanBahati Sep 18, 2025
ec3d0c9
test(js/plugin/ollama): fix tests
HassanBahati Sep 18, 2025
90d0245
chore(plugins/ollama): minor tweaks
CorieW Sep 29, 2025
d08379f
chore(js/plugins/ollama): input => request
HassanBahati Sep 30, 2025
e32fb5a
chore(js/plugins/ollama): minor tweaks
HassanBahati Sep 30, 2025
09da5be
refactor(js/plugins/ollama): extract constants to own module, fix som…
cabljac Sep 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions js/plugins/ollama/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { ModelInfo } from 'genkit/model';

export const ANY_JSON_SCHEMA: Record<string, any> = {
$schema: 'http://json-schema.org/draft-07/schema#',
};

export const GENERIC_MODEL_INFO = {
supports: {
multiturn: true,
media: true,
tools: true,
toolChoice: true,
systemRole: true,
constrained: 'all',
},
} as ModelInfo;

export const DEFAULT_OLLAMA_SERVER_ADDRESS = 'http://localhost:11434';
28 changes: 18 additions & 10 deletions js/plugins/ollama/src/embeddings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Document, EmbedderAction, Genkit } from 'genkit';
import type { Document, EmbedderAction } from 'genkit';
import { embedder } from 'genkit/plugin';
import type { EmbedRequest, EmbedResponse } from 'ollama';
import { DEFAULT_OLLAMA_SERVER_ADDRESS } from './constants.js';
import type { DefineOllamaEmbeddingParams, RequestHeaders } from './types.js';

async function toOllamaEmbedRequest(
export async function toOllamaEmbedRequest(
modelName: string,
dimensions: number,
documents: Document[],
Expand Down Expand Up @@ -59,11 +61,13 @@ async function toOllamaEmbedRequest(
};
}

export function defineOllamaEmbedder(
ai: Genkit,
{ name, modelName, dimensions, options }: DefineOllamaEmbeddingParams
): EmbedderAction<any> {
return ai.defineEmbedder(
export function defineOllamaEmbedder({
name,
modelName,
dimensions,
options,
}: DefineOllamaEmbeddingParams): EmbedderAction<any> {
return embedder(
{
name: `ollama/${name}`,
info: {
Expand All @@ -75,13 +79,17 @@ export function defineOllamaEmbedder(
},
},
},
async (input, config) => {
const serverAddress = config?.serverAddress || options.serverAddress;
async (request, config) => {
console.log('request.options', request.options);
// request.options; might be the equivalent of config now

const serverAddress =
options.serverAddress || DEFAULT_OLLAMA_SERVER_ADDRESS;

const { url, requestPayload, headers } = await toOllamaEmbedRequest(
modelName,
dimensions,
input,
request.input,
serverAddress,
options.requestHeaders
);
Expand Down
200 changes: 90 additions & 110 deletions js/plugins/ollama/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
z,
type ActionMetadata,
type EmbedderReference,
type Genkit,
type ModelReference,
type ToolRequest,
type ToolRequestPart,
Expand All @@ -35,11 +34,19 @@ import {
type GenerateRequest,
type GenerateResponseData,
type MessageData,
type ModelInfo,
type ToolDefinition,
} from 'genkit/model';
import { genkitPlugin, type GenkitPlugin } from 'genkit/plugin';
import type { ActionType } from 'genkit/registry';
import {
genkitPluginV2,
model,
type GenkitPluginV2,
type ResolvableAction,
} from 'genkit/plugin';
import {
ANY_JSON_SCHEMA,
DEFAULT_OLLAMA_SERVER_ADDRESS,
GENERIC_MODEL_INFO,
} from './constants.js';
import { defineOllamaEmbedder } from './embeddings.js';
import type {
ApiType,
Expand All @@ -56,7 +63,7 @@ import type {
export type { OllamaPluginParams };

export type OllamaPlugin = {
(params?: OllamaPluginParams): GenkitPlugin;
(params?: OllamaPluginParams): GenkitPluginV2;

model(
name: string,
Expand All @@ -65,61 +72,6 @@ export type OllamaPlugin = {
embedder(name: string, config?: Record<string, any>): EmbedderReference;
};

const ANY_JSON_SCHEMA: Record<string, any> = {
$schema: 'http://json-schema.org/draft-07/schema#',
};

const GENERIC_MODEL_INFO = {
supports: {
multiturn: true,
media: true,
tools: true,
toolChoice: true,
systemRole: true,
constrained: 'all',
},
} as ModelInfo;

const DEFAULT_OLLAMA_SERVER_ADDRESS = 'http://localhost:11434';

async function initializer(
ai: Genkit,
serverAddress: string,
params?: OllamaPluginParams
) {
params?.models?.map((model) =>
defineOllamaModel(ai, model, serverAddress, params?.requestHeaders)
);
params?.embedders?.map((model) =>
defineOllamaEmbedder(ai, {
name: model.name,
modelName: model.name,
dimensions: model.dimensions,
options: params!,
})
);
}

function resolveAction(
ai: Genkit,
actionType: ActionType,
actionName: string,
serverAddress: string,
requestHeaders?: RequestHeaders
) {
// We can only dynamically resolve models, for embedders user must provide dimensions.
if (actionType === 'model') {
defineOllamaModel(
ai,
{
name: actionName,
},
serverAddress,
requestHeaders
);
}
}

async function listActions(
serverAddress: string,
requestHeaders?: RequestHeaders
Expand All @@ -131,39 +83,13 @@ async function listActions(
?.filter((m) => m.model && !m.model.includes('embed'))
.map((m) =>
modelActionMetadata({
name: `ollama/${m.model}`,
name: m.model,
info: GENERIC_MODEL_INFO,
})
) || []
);
}

function ollamaPlugin(params?: OllamaPluginParams): GenkitPlugin {
if (!params) {
params = {};
}
if (!params.serverAddress) {
params.serverAddress = DEFAULT_OLLAMA_SERVER_ADDRESS;
}
const serverAddress = params.serverAddress;
return genkitPlugin(
'ollama',
async (ai: Genkit) => {
await initializer(ai, serverAddress, params);
},
async (ai, actionType, actionName) => {
resolveAction(
ai,
actionType,
actionName,
serverAddress,
params?.requestHeaders
);
},
async () => await listActions(serverAddress, params?.requestHeaders)
);
}

async function listLocalModels(
serverAddress: string,
requestHeaders?: RequestHeaders
Expand Down Expand Up @@ -217,26 +143,25 @@ export const OllamaConfigSchema = GenerationCommonConfigSchema.extend({
.optional(),
});

function defineOllamaModel(
ai: Genkit,
model: ModelDefinition,
function createOllamaModel(
modelDef: ModelDefinition,
serverAddress: string,
requestHeaders?: RequestHeaders
) {
return ai.defineModel(
return model(
{
name: `ollama/${model.name}`,
label: `Ollama - ${model.name}`,
name: modelDef.name,
label: `Ollama - ${modelDef.name}`,
configSchema: OllamaConfigSchema,
supports: {
multiturn: !model.type || model.type === 'chat',
multiturn: !modelDef.type || modelDef.type === 'chat',
systemRole: true,
tools: model.supports?.tools,
tools: modelDef.supports?.tools,
},
},
async (input, streamingCallback) => {
async (request, opts) => {
const { topP, topK, stopSequences, maxOutputTokens, ...rest } =
input.config as any;
request.config as any;
const options: Record<string, any> = { ...rest };
if (topP !== undefined) {
options.top_p = topP;
Expand All @@ -250,29 +175,29 @@ function defineOllamaModel(
if (maxOutputTokens !== undefined) {
options.num_predict = maxOutputTokens;
}
const type = model.type ?? 'chat';
const request = toOllamaRequest(
model.name,
input,
const type = modelDef.type ?? 'chat';
const ollamaRequest = toOllamaRequest(
modelDef.name,
request,
options,
type,
!!streamingCallback
!!opts
);
logger.debug(request, `ollama request (${type})`);
logger.debug(ollamaRequest, `ollama request (${type})`);

const extraHeaders = await getHeaders(
serverAddress,
requestHeaders,
model,
input
modelDef,
request
);
let res;
try {
res = await fetch(
serverAddress + (type === 'chat' ? '/api/chat' : '/api/generate'),
{
method: 'POST',
body: JSON.stringify(request),
body: JSON.stringify(ollamaRequest),
headers: {
'Content-Type': 'application/json',
...extraHeaders,
Expand All @@ -297,16 +222,15 @@ function defineOllamaModel(

let message: MessageData;

if (streamingCallback) {
if (opts.sendChunk) {
const reader = res.body.getReader();
const textDecoder = new TextDecoder();
let textResponse = '';
for await (const chunk of readChunks(reader)) {
const chunkText = textDecoder.decode(chunk);
const json = JSON.parse(chunkText);
const message = parseMessage(json, type);
streamingCallback({
index: 0,
opts.sendChunk({
content: message.content,
});
textResponse += message.content[0].text;
Expand All @@ -329,13 +253,69 @@ function defineOllamaModel(

return {
message,
usage: getBasicUsageStats(input.messages, message),
usage: getBasicUsageStats(request.messages, message),
finishReason: 'stop',
} as GenerateResponseData;
}
);
}

function ollamaPlugin(params?: OllamaPluginParams): GenkitPluginV2 {
if (!params) {
params = {};
}
if (!params.serverAddress) {
params.serverAddress = DEFAULT_OLLAMA_SERVER_ADDRESS;
}
const serverAddress = params.serverAddress;

return genkitPluginV2({
name: 'ollama',
init() {
const actions: ResolvableAction[] = [];

if (params?.models) {
for (const model of params.models) {
actions.push(
createOllamaModel(model, serverAddress, params.requestHeaders)
);
}
}

if (params?.embedders && params.serverAddress) {
for (const embedder of params.embedders) {
actions.push(
defineOllamaEmbedder({
name: embedder.name,
modelName: embedder.name,
dimensions: embedder.dimensions,
options: { ...params, serverAddress },
})
);
}
}

return actions;
},
async resolve(actionType, actionName) {
// dynamically resolve models, for embedders user must provide dimensions.
if (actionType === 'model') {
return await createOllamaModel(
{
name: actionName,
},
serverAddress,
params?.requestHeaders
);
}
return undefined;
},
async list() {
return await listActions(serverAddress, params?.requestHeaders);
},
});
}

function parseMessage(response: any, type: ApiType): MessageData {
if (response.error) {
throw new Error(response.error);
Expand Down
Loading