Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 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
8e1187d
(js/plugins/ollama): ensure all embeddings tests are executed
HassanBahati Oct 6, 2025
e0fdacc
tests(js/plugins/ollama): add tests to cover cases when genkit isnt i…
HassanBahati Oct 6, 2025
2e0103d
tests(js/plugins/ollama): add live tests
HassanBahati Oct 6, 2025
58d934c
chore(js/plugins/ollama): format
HassanBahati Oct 6, 2025
7c39354
test(js/plugins/ollama): update model tests
cabljac Oct 6, 2025
4d330c0
fix(js/plguings/ollama): change to opts.streamingRequested and improv…
cabljac Oct 6, 2025
d05147b
refactor(js/plguings/ollama): improve model tests
cabljac Oct 7, 2025
d5ba77f
refactor(js/plguings/ollama): use namespace and keep plugin function …
cabljac Oct 7, 2025
61c52dc
fix(js/plguings/ollama): revert to using prefixes
cabljac Oct 7, 2025
6fec1d6
Update js/plugins/ollama/package.json
cabljac Oct 7, 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';
25 changes: 15 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,14 @@ export function defineOllamaEmbedder(
},
},
},
async (input, config) => {
const serverAddress = config?.serverAddress || options.serverAddress;
async (request, config) => {
const serverAddress =
options.serverAddress || DEFAULT_OLLAMA_SERVER_ADDRESS;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to double check this, make sure we're not breaking anything by removing config.serverAddress


const { url, requestPayload, headers } = await toOllamaEmbedRequest(
modelName,
dimensions,
input,
request.input,
serverAddress,
options.requestHeaders
);
Expand Down
190 changes: 85 additions & 105 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,59 +72,60 @@ 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#',
};
function ollamaPlugin(params?: OllamaPluginParams): GenkitPluginV2 {
if (!params) {
params = {};
}
Comment on lines +75 to +78
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function ollamaPlugin(params?: OllamaPluginParams): GenkitPluginV2 {
if (!params) {
params = {};
}
function ollamaPlugin(params?: OllamaPluginParams = {}): GenkitPluginV2 {

if (!params.serverAddress) {
params.serverAddress = DEFAULT_OLLAMA_SERVER_ADDRESS;
}
const serverAddress = params.serverAddress;
Comment on lines +79 to +82
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!params.serverAddress) {
params.serverAddress = DEFAULT_OLLAMA_SERVER_ADDRESS;
}
const serverAddress = params.serverAddress;
const serverAddress = params.serverAddress || DEFAULT_OLLAMA_SERVER_ADDRESS;


const GENERIC_MODEL_INFO = {
supports: {
multiturn: true,
media: true,
tools: true,
toolChoice: true,
systemRole: true,
constrained: 'all',
},
} as ModelInfo;
return genkitPluginV2({
name: 'ollama',
init() {
const actions: ResolvableAction[] = [];

const DEFAULT_OLLAMA_SERVER_ADDRESS = 'http://localhost:11434';
if (params?.models) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be extracted into a helper function?

function getModelActions(params: OllamaPluginParams, serverAddress: string): ResolvableAction[] {
  /** Extract variables **/
  const { models, requestHeaders } = params;

  /** If no models, return empty array; **/
  if (!models || !models.length) return [];
  
 /** Return Ollama models **/
  return models.map(m => createOllamaModel(m, serverAddress, requestHeaders));
}
init() {
  return [
    ...getModelActions(params, serverAddress),
    ...getEmbedderActions(params, serverAddress)
  ];
}

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

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!,
})
);
}
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 },
})
);
}
}

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
);
}
return actions;
},
async resolve(actionType, actionName) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this is called actionName and not name?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should factor these out into helpers (like in original)

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably should handle the case where someone tries to resolve an embedder?

},
Comment on lines +114 to +124
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why we removed the resolveAction function? The original resolveActionWe can return early if undefined. Also should we return undefined here.

Suggested change
if (actionType === 'model') {
return await createOllamaModel(
{
name: actionName,
},
serverAddress,
params?.requestHeaders
);
}
return undefined;
},
async resolve(actionType, actionName) {
/** If no model actions, return undefined **/
if (actionType !== 'model') return undefined;
return await createOllamaModel(
{
name: actionName,
},
serverAddress,
params?.requestHeaders
);
}

async list() {
return await listActions(serverAddress, params?.requestHeaders);
},
});
}

async function listActions(
Expand All @@ -131,39 +139,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 +199,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 +231,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?.streamingRequested
);
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 +278,15 @@ function defineOllamaModel(

let message: MessageData;

if (streamingCallback) {
if (opts.streamingRequested) {
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,7 +309,7 @@ function defineOllamaModel(

return {
message,
usage: getBasicUsageStats(input.messages, message),
usage: getBasicUsageStats(request.messages, message),
finishReason: 'stop',
} as GenerateResponseData;
}
Expand Down Expand Up @@ -500,7 +480,7 @@ function toGenkitToolRequest(tool_calls: OllamaToolCall[]): ToolRequestPart[] {
}));
}

function readChunks(reader) {
function readChunks(reader: ReadableStreamDefaultReader<Uint8Array>) {
return {
async *[Symbol.asyncIterator]() {
let readResult = await reader.read();
Expand Down
Loading