AIClient-2-API/src/converters/strategies/OllamaConverter.js
hex2077 2d317e0333 refactor(项目结构): 重构项目目录结构并优化代码组织
将常用工具函数移动到utils目录
重构提供商策略模式实现
新增docker-compose构建配置文件
优化UI配置选择器的样式和交互
重构代理工具和API管理模块
更新脚本路径和依赖引用
2026-01-10 18:19:06 +08:00

690 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Ollama转换器
* 处理Ollama协议与其他协议之间的转换
*/
import { v4 as uuidv4 } from 'uuid';
import { createHash } from 'crypto';
import { BaseConverter } from '../BaseConverter.js';
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
import {
OLLAMA_DEFAULT_CONTEXT_LENGTH,
OLLAMA_DEFAULT_MAX_OUTPUT_TOKENS,
OLLAMA_CLAUDE_DEFAULT_CONTEXT_LENGTH,
OLLAMA_CLAUDE_SONNET_45_CONTEXT_LENGTH,
OLLAMA_CLAUDE_SONNET_45_MAX_OUTPUT_TOKENS,
OLLAMA_CLAUDE_HAIKU_45_CONTEXT_LENGTH,
OLLAMA_CLAUDE_HAIKU_45_MAX_OUTPUT_TOKENS,
OLLAMA_CLAUDE_OPUS_41_CONTEXT_LENGTH,
OLLAMA_CLAUDE_OPUS_41_MAX_OUTPUT_TOKENS,
OLLAMA_CLAUDE_SONNET_40_CONTEXT_LENGTH,
OLLAMA_CLAUDE_SONNET_40_MAX_OUTPUT_TOKENS,
OLLAMA_CLAUDE_SONNET_37_CONTEXT_LENGTH,
OLLAMA_CLAUDE_SONNET_37_MAX_OUTPUT_TOKENS,
OLLAMA_CLAUDE_OPUS_40_CONTEXT_LENGTH,
OLLAMA_CLAUDE_OPUS_40_MAX_OUTPUT_TOKENS,
OLLAMA_CLAUDE_HAIKU_35_CONTEXT_LENGTH,
OLLAMA_CLAUDE_HAIKU_35_MAX_OUTPUT_TOKENS,
OLLAMA_CLAUDE_HAIKU_30_CONTEXT_LENGTH,
OLLAMA_CLAUDE_HAIKU_30_MAX_OUTPUT_TOKENS,
OLLAMA_CLAUDE_SONNET_35_CONTEXT_LENGTH,
OLLAMA_CLAUDE_SONNET_35_MAX_OUTPUT_TOKENS,
OLLAMA_CLAUDE_OPUS_30_CONTEXT_LENGTH,
OLLAMA_CLAUDE_OPUS_30_MAX_OUTPUT_TOKENS,
OLLAMA_GEMINI_25_PRO_CONTEXT_LENGTH,
OLLAMA_GEMINI_25_PRO_MAX_OUTPUT_TOKENS,
OLLAMA_GEMINI_25_FLASH_CONTEXT_LENGTH,
OLLAMA_GEMINI_25_FLASH_MAX_OUTPUT_TOKENS,
OLLAMA_GEMINI_25_IMAGE_CONTEXT_LENGTH,
OLLAMA_GEMINI_25_IMAGE_MAX_OUTPUT_TOKENS,
OLLAMA_GEMINI_25_LIVE_CONTEXT_LENGTH,
OLLAMA_GEMINI_25_LIVE_MAX_OUTPUT_TOKENS,
OLLAMA_GEMINI_25_TTS_CONTEXT_LENGTH,
OLLAMA_GEMINI_25_TTS_MAX_OUTPUT_TOKENS,
OLLAMA_GEMINI_20_FLASH_CONTEXT_LENGTH,
OLLAMA_GEMINI_20_FLASH_MAX_OUTPUT_TOKENS,
OLLAMA_GEMINI_20_IMAGE_CONTEXT_LENGTH,
OLLAMA_GEMINI_20_IMAGE_MAX_OUTPUT_TOKENS,
OLLAMA_GEMINI_15_PRO_CONTEXT_LENGTH,
OLLAMA_GEMINI_15_PRO_MAX_OUTPUT_TOKENS,
OLLAMA_GEMINI_15_FLASH_CONTEXT_LENGTH,
OLLAMA_GEMINI_15_FLASH_MAX_OUTPUT_TOKENS,
OLLAMA_GEMINI_DEFAULT_CONTEXT_LENGTH,
OLLAMA_GEMINI_DEFAULT_MAX_OUTPUT_TOKENS,
OLLAMA_GPT4_TURBO_CONTEXT_LENGTH,
OLLAMA_GPT4_TURBO_MAX_OUTPUT_TOKENS,
OLLAMA_GPT4_32K_CONTEXT_LENGTH,
OLLAMA_GPT4_32K_MAX_OUTPUT_TOKENS,
OLLAMA_GPT4_BASE_CONTEXT_LENGTH,
OLLAMA_GPT4_BASE_MAX_OUTPUT_TOKENS,
OLLAMA_GPT35_16K_CONTEXT_LENGTH,
OLLAMA_GPT35_16K_MAX_OUTPUT_TOKENS,
OLLAMA_GPT35_BASE_CONTEXT_LENGTH,
OLLAMA_GPT35_BASE_MAX_OUTPUT_TOKENS,
OLLAMA_QWEN_CODER_PLUS_CONTEXT_LENGTH,
OLLAMA_QWEN_CODER_PLUS_MAX_OUTPUT_TOKENS,
OLLAMA_QWEN_VL_PLUS_CONTEXT_LENGTH,
OLLAMA_QWEN_VL_PLUS_MAX_OUTPUT_TOKENS,
OLLAMA_QWEN_CODER_FLASH_CONTEXT_LENGTH,
OLLAMA_QWEN_CODER_FLASH_MAX_OUTPUT_TOKENS,
OLLAMA_QWEN_DEFAULT_CONTEXT_LENGTH,
OLLAMA_QWEN_DEFAULT_MAX_OUTPUT_TOKENS,
OLLAMA_DEFAULT_FILE_TYPE,
OLLAMA_DEFAULT_QUANTIZATION_VERSION,
OLLAMA_DEFAULT_ROPE_FREQ_BASE,
OLLAMA_DEFAULT_TEMPERATURE,
OLLAMA_DEFAULT_TOP_P,
OLLAMA_DEFAULT_QUANTIZATION_LEVEL,
OLLAMA_SHOW_QUANTIZATION_LEVEL
} from '../utils.js';
/**
* Ollama转换器类
* 实现Ollama协议到其他协议的转换
*/
export class OllamaConverter extends BaseConverter {
constructor() {
super('ollama');
}
/**
* 转换请求 - Ollama -> 其他协议
*/
convertRequest(data, targetProtocol) {
switch (targetProtocol) {
case MODEL_PROTOCOL_PREFIX.OPENAI:
case MODEL_PROTOCOL_PREFIX.CLAUDE:
case MODEL_PROTOCOL_PREFIX.GEMINI:
return this.toOpenAIRequest(data);
default:
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
}
}
/**
* 转换响应 - 其他协议 -> Ollama
*/
convertResponse(data, sourceProtocol, model) {
return this.toOllamaChatResponse(data, model);
}
/**
* 转换流式响应块 - 其他协议 -> Ollama
*/
convertStreamChunk(chunk, sourceProtocol, model, isDone = false) {
return this.toOllamaStreamChunk(chunk, model, isDone);
}
/**
* 转换模型列表 - 其他协议 -> Ollama
*/
convertModelList(data, sourceProtocol) {
return this.toOllamaTags(data, sourceProtocol);
}
// =========================================================================
// Ollama -> OpenAI 转换
// =========================================================================
/**
* Ollama请求 -> OpenAI请求
*/
toOpenAIRequest(ollamaRequest) {
const openaiRequest = {
model: ollamaRequest.model || 'default',
messages: [],
stream: ollamaRequest.stream !== undefined ? ollamaRequest.stream : false
};
// Map Ollama messages to OpenAI format
if (ollamaRequest.messages && Array.isArray(ollamaRequest.messages)) {
openaiRequest.messages = ollamaRequest.messages.map(msg => ({
role: msg.role || 'user',
content: msg.content || ''
}));
}
// Map Ollama options to OpenAI parameters
if (ollamaRequest.options) {
const opts = ollamaRequest.options;
if (opts.temperature !== undefined) openaiRequest.temperature = opts.temperature;
if (opts.top_p !== undefined) openaiRequest.top_p = opts.top_p;
if (opts.top_k !== undefined) openaiRequest.top_k = opts.top_k;
if (opts.num_predict !== undefined) openaiRequest.max_tokens = opts.num_predict;
if (opts.stop !== undefined) openaiRequest.stop = opts.stop;
}
// Handle system prompt
if (ollamaRequest.system) {
openaiRequest.messages.unshift({
role: 'system',
content: ollamaRequest.system
});
}
// Handle template/prompt for generate endpoint
if (ollamaRequest.prompt) {
openaiRequest.messages = [{
role: 'user',
content: ollamaRequest.prompt
}];
// Add system prompt if provided
if (ollamaRequest.system) {
openaiRequest.messages.unshift({
role: 'system',
content: ollamaRequest.system
});
}
}
return openaiRequest;
}
// =========================================================================
// OpenAI/Claude/Gemini -> Ollama 转换
// =========================================================================
/**
* OpenAI/Claude/Gemini响应 -> Ollama chat响应
*/
toOllamaChatResponse(response, model) {
const ollamaResponse = {
model: model || response.model || 'unknown',
created_at: new Date().toISOString(),
done: true
};
// Handle OpenAI format (choices array)
if (response.choices && response.choices.length > 0) {
const choice = response.choices[0];
ollamaResponse.message = {
role: choice.message?.role || 'assistant',
content: choice.message?.content || ''
};
// Map finish reason
if (choice.finish_reason) {
ollamaResponse.done_reason = choice.finish_reason === 'stop' ? 'stop' : choice.finish_reason;
}
}
// Handle Claude format (content array)
else if (response.content && Array.isArray(response.content)) {
let textContent = '';
response.content.forEach(block => {
if (block.type === 'text' && block.text) {
textContent += block.text;
}
});
ollamaResponse.message = {
role: response.role || 'assistant',
content: textContent
};
if (response.stop_reason) {
ollamaResponse.done_reason = response.stop_reason === 'end_turn' ? 'stop' : response.stop_reason;
}
}
// Handle Gemini format (candidates array)
else if (response.candidates && response.candidates.length > 0) {
const candidate = response.candidates[0];
let textContent = '';
if (candidate.content && candidate.content.parts) {
textContent = candidate.content.parts
.filter(part => part.text)
.map(part => part.text)
.join('');
}
ollamaResponse.message = {
role: candidate.content?.role || 'assistant',
content: textContent
};
if (candidate.finishReason) {
ollamaResponse.done_reason = candidate.finishReason.toLowerCase();
}
}
// Add usage statistics if available
const usage = response.usage || response.usageMetadata;
if (usage) {
ollamaResponse.prompt_eval_count = usage.prompt_tokens || usage.input_tokens || usage.promptTokenCount || 0;
ollamaResponse.eval_count = usage.completion_tokens || usage.output_tokens || usage.candidatesTokenCount || 0;
ollamaResponse.total_duration = 0;
ollamaResponse.load_duration = 0;
ollamaResponse.prompt_eval_duration = 0;
ollamaResponse.eval_duration = 0;
}
return ollamaResponse;
}
/**
* OpenAI/Claude/Gemini generate响应 -> Ollama generate响应
*/
toOllamaGenerateResponse(response, model) {
const ollamaResponse = {
model: model || response.model || 'unknown',
created_at: new Date().toISOString(),
done: true
};
// Handle OpenAI format
if (response.choices && response.choices.length > 0) {
const choice = response.choices[0];
ollamaResponse.response = choice.message?.content || choice.text || '';
if (choice.finish_reason) {
ollamaResponse.done_reason = choice.finish_reason === 'stop' ? 'stop' : choice.finish_reason;
}
}
// Handle Claude format
else if (response.content && Array.isArray(response.content)) {
let textContent = '';
response.content.forEach(block => {
if (block.type === 'text' && block.text) {
textContent += block.text;
}
});
ollamaResponse.response = textContent;
if (response.stop_reason) {
ollamaResponse.done_reason = response.stop_reason === 'end_turn' ? 'stop' : response.stop_reason;
}
}
// Handle Gemini format
else if (response.candidates && response.candidates.length > 0) {
const candidate = response.candidates[0];
let textContent = '';
if (candidate.content && candidate.content.parts) {
textContent = candidate.content.parts
.filter(part => part.text)
.map(part => part.text)
.join('');
}
ollamaResponse.response = textContent;
if (candidate.finishReason) {
ollamaResponse.done_reason = candidate.finishReason.toLowerCase();
}
}
// Add usage statistics
const genUsage = response.usage || response.usageMetadata;
if (genUsage) {
ollamaResponse.prompt_eval_count = genUsage.prompt_tokens || genUsage.input_tokens || genUsage.promptTokenCount || 0;
ollamaResponse.eval_count = genUsage.completion_tokens || genUsage.output_tokens || genUsage.candidatesTokenCount || 0;
ollamaResponse.total_duration = 0;
ollamaResponse.load_duration = 0;
ollamaResponse.prompt_eval_duration = 0;
ollamaResponse.eval_duration = 0;
}
return ollamaResponse;
}
/**
* OpenAI/Claude/Gemini流式块 -> Ollama流式块
*/
toOllamaStreamChunk(chunk, model, isDone = false) {
const ollamaChunk = {
model: model || 'unknown',
created_at: new Date().toISOString(),
done: isDone
};
// Handle Claude SSE format
if (chunk.type) {
if (chunk.type === 'content_block_delta' && chunk.delta) {
ollamaChunk.message = {
role: 'assistant',
content: chunk.delta.text || ''
};
} else if (chunk.type === 'message_delta' && chunk.usage) {
ollamaChunk.message = {
role: 'assistant',
content: ''
};
ollamaChunk.prompt_eval_count = 0;
ollamaChunk.eval_count = chunk.usage.output_tokens || 0;
} else {
ollamaChunk.message = {
role: 'assistant',
content: ''
};
}
}
// Handle Gemini format
else if (!isDone && chunk.candidates && chunk.candidates.length > 0) {
const candidate = chunk.candidates[0];
let content = '';
if (candidate.content && candidate.content.parts) {
content = candidate.content.parts
.filter(part => part.text)
.map(part => part.text)
.join('');
}
ollamaChunk.message = {
role: 'assistant',
content: content
};
}
// Handle OpenAI format
else if (!isDone && chunk.choices && chunk.choices.length > 0) {
const delta = chunk.choices[0].delta;
ollamaChunk.message = {
role: delta.role || 'assistant',
content: delta.content || ''
};
}
// Handle final chunk
else if (isDone) {
ollamaChunk.message = {
role: 'assistant',
content: ''
};
ollamaChunk.done_reason = 'stop';
}
return ollamaChunk;
}
/**
* OpenAI/Claude/Gemini流式块 -> Ollama generate流式块
*/
toOllamaGenerateStreamChunk(chunk, model, isDone = false) {
const ollamaChunk = {
model: model || 'unknown',
created_at: new Date().toISOString(),
done: isDone
};
// Handle Claude SSE format
if (chunk.type) {
if (chunk.type === 'content_block_delta' && chunk.delta) {
ollamaChunk.response = chunk.delta.text || '';
} else if (chunk.type === 'message_delta' && chunk.usage) {
ollamaChunk.response = '';
ollamaChunk.prompt_eval_count = 0;
ollamaChunk.eval_count = chunk.usage.output_tokens || 0;
} else {
ollamaChunk.response = '';
}
}
// Handle OpenAI format
else if (!isDone && chunk.choices && chunk.choices.length > 0) {
const delta = chunk.choices[0].delta;
ollamaChunk.response = delta.content || '';
}
// Handle final chunk
else if (isDone) {
ollamaChunk.response = '';
ollamaChunk.done_reason = 'stop';
}
return ollamaChunk;
}
/**
* OpenAI/Claude/Gemini模型列表 -> Ollama tags
*/
toOllamaTags(modelList, sourceProtocol = null) {
const models = [];
// Handle both OpenAI format (data array) and Gemini format (models array)
const sourceModels = modelList.data || modelList.models || [];
if (Array.isArray(sourceModels)) {
sourceModels.forEach(model => {
// Get model name
let modelName = model.id || model.name || model.displayName || 'unknown';
// Remove "models/" prefix if present (for Gemini)
if (modelName.startsWith('models/')) {
modelName = modelName.substring(7); // Remove "models/"
}
// Skip models with invalid names
if (modelName === 'unknown' || !modelName) {
return;
}
// IMPORTANT: Copilot expects family: "Ollama" with capital O!
const modelOwner = 'Ollama';
models.push({
name: modelName,
model: modelName,
modified_at: new Date().toISOString(),
size: 0, // As in the old patch
digest: '', // Empty string, as in the old patch
details: {
parent_model: '',
format: 'gguf',
family: modelOwner, // "Ollama" with capital O
families: [modelOwner],
parameter_size: '0B', // As in the old patch
quantization_level: OLLAMA_DEFAULT_QUANTIZATION_LEVEL
}
});
});
}
return { models };
}
/**
* Generate Ollama show response
*/
toOllamaShowResponse(modelName) {
// Minimal implementation, as in the old patch
let contextLength = OLLAMA_DEFAULT_CONTEXT_LENGTH;
let maxOutputTokens = OLLAMA_DEFAULT_MAX_OUTPUT_TOKENS;
let family = 'Ollama'; // ВАЖНО: С большой буквы, как ожидает Copilot!
let architecture = 'transformer';
const lowerName = modelName.toLowerCase();
// Determine contextLength by model name
// Claude models
if (lowerName.includes('claude')) {
architecture = 'claude';
contextLength = OLLAMA_CLAUDE_DEFAULT_CONTEXT_LENGTH; // Default 200K
// Claude Sonnet 4.5
if (lowerName.includes('sonnet-4-5') || lowerName.includes('sonnet-4.5')) {
contextLength = OLLAMA_CLAUDE_SONNET_45_CONTEXT_LENGTH; // 200K (1M beta available)
maxOutputTokens = OLLAMA_CLAUDE_SONNET_45_MAX_OUTPUT_TOKENS; // 64K output
}
// Claude Haiku 4.5
else if (lowerName.includes('haiku-4-5') || lowerName.includes('haiku-4.5')) {
contextLength = OLLAMA_CLAUDE_HAIKU_45_CONTEXT_LENGTH; // 200K
maxOutputTokens = OLLAMA_CLAUDE_HAIKU_45_MAX_OUTPUT_TOKENS; // 64K output
}
// Claude Opus 4.1
else if (lowerName.includes('opus-4-1') || lowerName.includes('opus-4.1')) {
contextLength = OLLAMA_CLAUDE_OPUS_41_CONTEXT_LENGTH; // 200K
maxOutputTokens = OLLAMA_CLAUDE_OPUS_41_MAX_OUTPUT_TOKENS; // 32K output
}
// Claude Sonnet 4.0 (legacy)
else if (lowerName.includes('sonnet-4-0') || lowerName.includes('sonnet-4.0') || lowerName.includes('sonnet-4-20')) {
contextLength = OLLAMA_CLAUDE_SONNET_40_CONTEXT_LENGTH; // 200K (1M beta available)
maxOutputTokens = OLLAMA_CLAUDE_SONNET_40_MAX_OUTPUT_TOKENS; // 64K output
}
// Claude Sonnet 3.7 (legacy)
else if (lowerName.includes('3-7') || lowerName.includes('3.7')) {
contextLength = OLLAMA_CLAUDE_SONNET_37_CONTEXT_LENGTH; // 200K
maxOutputTokens = OLLAMA_CLAUDE_SONNET_37_MAX_OUTPUT_TOKENS; // 64K output (128K beta available)
}
// Claude Opus 4.0 (legacy)
else if (lowerName.includes('opus-4-0') || lowerName.includes('opus-4.0') || lowerName.includes('opus-4-20')) {
contextLength = OLLAMA_CLAUDE_OPUS_40_CONTEXT_LENGTH; // 200K
maxOutputTokens = OLLAMA_CLAUDE_OPUS_40_MAX_OUTPUT_TOKENS; // 32K output
}
// Claude Haiku 3.5 (legacy)
else if (lowerName.includes('haiku-3-5') || lowerName.includes('haiku-3.5')) {
contextLength = OLLAMA_CLAUDE_HAIKU_35_CONTEXT_LENGTH; // 200K
maxOutputTokens = OLLAMA_CLAUDE_HAIKU_35_MAX_OUTPUT_TOKENS; // 8K output
}
// Claude Haiku 3.0 (legacy)
else if (lowerName.includes('haiku-3-0') || lowerName.includes('haiku-3.0') || lowerName.includes('haiku-20240307')) {
contextLength = OLLAMA_CLAUDE_HAIKU_30_CONTEXT_LENGTH; // 200K
maxOutputTokens = OLLAMA_CLAUDE_HAIKU_30_MAX_OUTPUT_TOKENS; // 4K output
}
// Claude Sonnet 3.5 (legacy)
else if (lowerName.includes('sonnet-3-5') || lowerName.includes('sonnet-3.5')) {
contextLength = OLLAMA_CLAUDE_SONNET_35_CONTEXT_LENGTH; // 200K
maxOutputTokens = OLLAMA_CLAUDE_SONNET_35_MAX_OUTPUT_TOKENS; // 8K output
}
// Claude Opus 3.0 (legacy)
else if (lowerName.includes('opus-3-0') || lowerName.includes('opus-3.0') || lowerName.includes('opus') && lowerName.includes('20240229')) {
contextLength = OLLAMA_CLAUDE_OPUS_30_CONTEXT_LENGTH; // 200K
maxOutputTokens = OLLAMA_CLAUDE_OPUS_30_MAX_OUTPUT_TOKENS; // 4K output
}
// Default for Claude
else {
contextLength = OLLAMA_CLAUDE_DEFAULT_CONTEXT_LENGTH; // 200K
maxOutputTokens = OLLAMA_CLAUDE_HAIKU_35_MAX_OUTPUT_TOKENS; // 8K output
}
}
// Gemini models
else if (lowerName.includes('gemini')) {
architecture = 'gemini';
// Gemini 2.5 Pro
if (lowerName.includes('2.5') && lowerName.includes('pro')) {
contextLength = OLLAMA_GEMINI_25_PRO_CONTEXT_LENGTH; // 1M input tokens
maxOutputTokens = OLLAMA_GEMINI_25_PRO_MAX_OUTPUT_TOKENS; // 65K output tokens
}
// Gemini 2.5 Flash / Flash-Lite
else if (lowerName.includes('2.5') && (lowerName.includes('flash') || lowerName.includes('lite'))) {
contextLength = OLLAMA_GEMINI_25_FLASH_CONTEXT_LENGTH; // 1M input tokens
maxOutputTokens = OLLAMA_GEMINI_25_FLASH_MAX_OUTPUT_TOKENS; // 65K output tokens
}
// Gemini 2.5 Flash Image
else if (lowerName.includes('2.5') && lowerName.includes('image')) {
contextLength = OLLAMA_GEMINI_25_IMAGE_CONTEXT_LENGTH; // 65K input tokens
maxOutputTokens = OLLAMA_GEMINI_25_IMAGE_MAX_OUTPUT_TOKENS; // 32K output tokens
}
// Gemini 2.5 Flash Live / Native Audio
else if (lowerName.includes('2.5') && (lowerName.includes('live') || lowerName.includes('native-audio'))) {
contextLength = OLLAMA_GEMINI_25_LIVE_CONTEXT_LENGTH; // 131K input tokens
maxOutputTokens = OLLAMA_GEMINI_25_LIVE_MAX_OUTPUT_TOKENS; // 8K output tokens
}
// Gemini 2.5 TTS
else if (lowerName.includes('2.5') && lowerName.includes('tts')) {
contextLength = OLLAMA_GEMINI_25_TTS_CONTEXT_LENGTH; // 8K input tokens
maxOutputTokens = OLLAMA_GEMINI_25_TTS_MAX_OUTPUT_TOKENS; // 16K output tokens
}
// Gemini 2.0 Flash
else if (lowerName.includes('2.0') && lowerName.includes('flash')) {
contextLength = OLLAMA_GEMINI_20_FLASH_CONTEXT_LENGTH; // 1M input tokens
maxOutputTokens = OLLAMA_GEMINI_20_FLASH_MAX_OUTPUT_TOKENS; // 8K output tokens
}
// Gemini 2.0 Flash Image
else if (lowerName.includes('2.0') && lowerName.includes('image')) {
contextLength = OLLAMA_GEMINI_20_IMAGE_CONTEXT_LENGTH; // 32K input tokens
maxOutputTokens = OLLAMA_GEMINI_20_IMAGE_MAX_OUTPUT_TOKENS; // 8K output tokens
}
// Gemini 1.5 Pro (legacy)
else if (lowerName.includes('1.5') && lowerName.includes('pro')) {
contextLength = OLLAMA_GEMINI_15_PRO_CONTEXT_LENGTH; // 2M tokens
maxOutputTokens = OLLAMA_GEMINI_15_PRO_MAX_OUTPUT_TOKENS;
}
// Gemini 1.5 Flash (legacy)
else if (lowerName.includes('1.5') && lowerName.includes('flash')) {
contextLength = OLLAMA_GEMINI_15_FLASH_CONTEXT_LENGTH; // 1M tokens
maxOutputTokens = OLLAMA_GEMINI_15_FLASH_MAX_OUTPUT_TOKENS;
}
// Default for Gemini
else {
contextLength = OLLAMA_GEMINI_DEFAULT_CONTEXT_LENGTH; // 1M tokens
maxOutputTokens = OLLAMA_GEMINI_DEFAULT_MAX_OUTPUT_TOKENS;
}
}
// GPT-4 models
else if (lowerName.includes('gpt-4')) {
architecture = 'gpt';
if (lowerName.includes('turbo') || lowerName.includes('preview')) {
contextLength = OLLAMA_GPT4_TURBO_CONTEXT_LENGTH; // GPT-4 Turbo
maxOutputTokens = OLLAMA_GPT4_TURBO_MAX_OUTPUT_TOKENS;
} else if (lowerName.includes('32k')) {
contextLength = OLLAMA_GPT4_32K_CONTEXT_LENGTH;
maxOutputTokens = OLLAMA_GPT4_32K_MAX_OUTPUT_TOKENS;
} else {
contextLength = OLLAMA_GPT4_BASE_CONTEXT_LENGTH; // GPT-4 base
maxOutputTokens = OLLAMA_GPT4_BASE_MAX_OUTPUT_TOKENS;
}
}
// GPT-3.5 models
else if (lowerName.includes('gpt-3.5')) {
architecture = 'gpt';
if (lowerName.includes('16k')) {
contextLength = OLLAMA_GPT35_16K_CONTEXT_LENGTH;
maxOutputTokens = OLLAMA_GPT35_16K_MAX_OUTPUT_TOKENS;
} else {
contextLength = OLLAMA_GPT35_BASE_CONTEXT_LENGTH;
maxOutputTokens = OLLAMA_GPT35_BASE_MAX_OUTPUT_TOKENS;
}
}
// Qwen models
else if (lowerName.includes('qwen')) {
architecture = 'qwen';
// Qwen3 Coder Plus (coder-model)
if (lowerName.includes('coder-plus') || lowerName.includes('coder_plus') || lowerName.includes('coder-model')) {
contextLength = OLLAMA_QWEN_CODER_PLUS_CONTEXT_LENGTH; // 128K tokens
maxOutputTokens = OLLAMA_QWEN_CODER_PLUS_MAX_OUTPUT_TOKENS; // 65K output
}
// Qwen3 VL Plus (vision-model)
else if (lowerName.includes('vl-plus') || lowerName.includes('vl_plus') || lowerName.includes('vision-model')) {
contextLength = OLLAMA_QWEN_VL_PLUS_CONTEXT_LENGTH; // 256K tokens
maxOutputTokens = OLLAMA_QWEN_VL_PLUS_MAX_OUTPUT_TOKENS; // 32K output
}
// Qwen3 Coder Flash
else if (lowerName.includes('coder-flash') || lowerName.includes('coder_flash')) {
contextLength = OLLAMA_QWEN_CODER_FLASH_CONTEXT_LENGTH; // 128K tokens
maxOutputTokens = OLLAMA_QWEN_CODER_FLASH_MAX_OUTPUT_TOKENS; // 65K output
}
// Default for Qwen
else {
contextLength = OLLAMA_QWEN_DEFAULT_CONTEXT_LENGTH; // 32K tokens
maxOutputTokens = OLLAMA_QWEN_DEFAULT_MAX_OUTPUT_TOKENS;
}
}
// Minimal parameter_size, as in the old patch
let parameterSize = '0B';
return {
license: '',
modelfile: `# Modelfile for ${modelName}\nFROM ${modelName}`,
parameters: `num_ctx ${contextLength}\nnum_predict ${maxOutputTokens}\ntemperature ${OLLAMA_DEFAULT_TEMPERATURE}\ntop_p ${OLLAMA_DEFAULT_TOP_P}`,
template: '{{ if .System }}{{ .System }}\n{{ end }}{{ .Prompt }}',
details: {
parent_model: '',
format: 'gguf',
family: family,
families: [family],
parameter_size: parameterSize,
quantization_level: OLLAMA_SHOW_QUANTIZATION_LEVEL
},
model_info: {
'general.architecture': architecture,
'general.file_type': OLLAMA_DEFAULT_FILE_TYPE,
'general.parameter_count': 0,
'general.quantization_version': OLLAMA_DEFAULT_QUANTIZATION_VERSION,
'general.context_length': contextLength,
'llama.context_length': contextLength,
'llama.rope.freq_base': OLLAMA_DEFAULT_ROPE_FREQ_BASE
},
capabilities: ['tools', 'vision', 'completion'] // Indicate that the model supports tool calling
};
}
}