Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 17 additions & 2 deletions src/stores/remoteModelCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,25 @@ function extractOllamaCapabilities(data: Record<string, unknown>): RemoteModelIn
let contextLength = 4096;
let supportsVision = false;

// Newer Ollama versions expose a top-level `capabilities` array (e.g. ["vision", "tools"]).
// Gemma 4 and similar models use this field instead of model_info keys.
let supportsToolCalling: boolean | undefined;
if (Array.isArray(data.capabilities)) {
const caps = data.capabilities as unknown[];
supportsVision = caps.includes('vision');
supportsToolCalling = caps.includes('tools');
}

if (data.model_info && typeof data.model_info === 'object') {
const parsed = parseModelInfoKeys(data.model_info as Record<string, unknown>);
if (parsed.contextLength > 0) contextLength = parsed.contextLength;
supportsVision = parsed.supportsVision;
if (!supportsVision) supportsVision = parsed.supportsVision;
}

// projector_info is present for multimodal models when capabilities array is missing.
if (!supportsVision && data.projector_info && typeof data.projector_info === 'object') {
const projectorKeys = Object.keys(data.projector_info as Record<string, unknown>);
supportsVision = projectorKeys.some(k => k.includes('vision') || k.includes('clip'));
}

if (contextLength === 4096 && typeof data.parameters === 'string') {
Expand All @@ -63,7 +78,7 @@ function extractOllamaCapabilities(data: Record<string, unknown>): RemoteModelIn
/\.Think|\.Thinking|\.IsThinkSet/.test(template) ||
/^RENDERER\s/m.test(modelfile);

return { contextLength, supportsVision, supportsThinking };
return { contextLength, supportsVision, supportsToolCalling, supportsThinking };
}

/**
Expand Down
59 changes: 0 additions & 59 deletions src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,13 @@
import RNFS from 'react-native-fs';

const LOG_FILE_NAME = 'download-debug.log';
const MAX_LOG_FILE_BYTES = 2 * 1024 * 1024;
const RETAINED_LOG_LINES = 4000;

let writeQueue = Promise.resolve();

function getLogFilePath(): string {
return `${RNFS.DocumentDirectoryPath}/${LOG_FILE_NAME}`;
}

function formatArg(arg: unknown): string {
if (arg instanceof Error) {
return `${arg.name}: ${arg.message}${arg.stack ? `\n${arg.stack}` : ''}`;
}
if (typeof arg === 'string') return arg;
if (typeof arg === 'number' || typeof arg === 'boolean' || arg == null) return String(arg);
try {
return JSON.stringify(arg);
} catch {
return String(arg);
}
}

function appendPersistentLog(level: 'log' | 'warn' | 'error', args: unknown[]): void {
const timestamp = new Date().toISOString();
const line = `[${timestamp}] ${level.toUpperCase()}: ${args.map(formatArg).join(' ')}\n`;

writeQueue = writeQueue.then(async () => {
try {
const path = getLogFilePath();
if (await RNFS.exists(path)) {
await RNFS.appendFile(path, line, 'utf8');
} else {
await RNFS.writeFile(path, line, 'utf8');
}

const stat = await RNFS.stat(path);
const size = typeof stat.size === 'string' ? Number.parseInt(stat.size, 10) : stat.size;
if (size > MAX_LOG_FILE_BYTES) {
const content = await RNFS.readFile(path, 'utf8');
const trimmed = content.split('\n').filter(Boolean).slice(-RETAINED_LOG_LINES).join('\n');
await RNFS.writeFile(path, trimmed ? `${trimmed}\n` : '', 'utf8');
}
} catch {
// Logging must never break app execution.
}
});
}

function capture(level: 'log' | 'warn' | 'error', args: unknown[]): void {
appendPersistentLog(level, args);
}

const logger = {
log: (...args: unknown[]): void => {
capture('log', args);
if (__DEV__) console.log(...args); // NOSONAR
},
warn: (...args: unknown[]): void => {
capture('warn', args);
if (__DEV__) console.warn(...args); // NOSONAR
},
error: (...args: unknown[]): void => {
capture('error', args);
if (__DEV__) console.error(...args); // NOSONAR
},
getLogFilePath,
};

export default logger;
Loading