diff --git a/packages/ui/src/hooks/useProviderLogo.ts b/packages/ui/src/hooks/useProviderLogo.ts index 1caedf14..3a0f7c67 100644 --- a/packages/ui/src/hooks/useProviderLogo.ts +++ b/packages/ui/src/hooks/useProviderLogo.ts @@ -22,6 +22,7 @@ const LOGO_ALIAS = new Map([ ['gemini', 'google'], ['evroc-ai', 'evroc'], ['evrocai', 'evroc'], + ['ollama-cloud', 'ollama'], ]); const normalizeProviderId = (providerId: string | null | undefined) => { diff --git a/packages/ui/src/lib/quota/providers/index.ts b/packages/ui/src/lib/quota/providers/index.ts index db74e32c..c161b749 100644 --- a/packages/ui/src/lib/quota/providers/index.ts +++ b/packages/ui/src/lib/quota/providers/index.ts @@ -16,6 +16,7 @@ export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [ { id: 'zai-coding-plan', name: 'z.ai' }, { id: 'minimax-cn-coding-plan', name: 'MiniMax Coding Plan (minimaxi.com)' }, { id: 'minimax-coding-plan', name: 'MiniMax Coding Plan (minimax.io)' }, + { id: 'ollama-cloud', name: 'Ollama Cloud' }, ]; export const QUOTA_PROVIDER_MAP = QUOTA_PROVIDERS.reduce< diff --git a/packages/ui/src/lib/quota/utils.ts b/packages/ui/src/lib/quota/utils.ts index 0aceb766..14c13a54 100644 --- a/packages/ui/src/lib/quota/utils.ts +++ b/packages/ui/src/lib/quota/utils.ts @@ -35,6 +35,7 @@ export const formatWindowLabel = (label: string): string => { if (label === 'daily') return 'Daily'; if (label === 'monthly') return 'Monthly Limit'; if (label === 'credits') return 'Credits'; + if (label === 'session') return 'Session'; if (label === 'premium') return 'Premium Interactions'; if (label === 'chat') return 'Chat Requests'; if (label === 'completions') return 'Completions'; diff --git a/packages/ui/src/types/quota.ts b/packages/ui/src/types/quota.ts index 856ec026..a2e64de4 100644 --- a/packages/ui/src/types/quota.ts +++ b/packages/ui/src/types/quota.ts @@ -10,7 +10,8 @@ export type QuotaProviderId = | 'openrouter' | 'zai-coding-plan' | 'minimax-coding-plan' - | 'minimax-cn-coding-plan'; + | 'minimax-cn-coding-plan' + | 'ollama-cloud'; export interface UsageWindow { usedPercent: number | null; diff --git a/packages/web/server/lib/quota/DOCUMENTATION.md b/packages/web/server/lib/quota/DOCUMENTATION.md index f7c1d941..c9dc10f9 100644 --- a/packages/web/server/lib/quota/DOCUMENTATION.md +++ b/packages/web/server/lib/quota/DOCUMENTATION.md @@ -27,6 +27,7 @@ These provider IDs are currently dispatchable via `fetchQuotaForProvider(provide | `zai-coding-plan` | z.ai | `providers/zai.js` | `zai-coding-plan`, `zai`, `z.ai` | | `minimax-coding-plan` | MiniMax Coding Plan (minimax.io) | `providers/minimax-coding-plan.js` | `minimax-coding-plan` | | `minimax-cn-coding-plan` | MiniMax Coding Plan (minimaxi.com) | `providers/minimax-cn-coding-plan.js` | `minimax-cn-coding-plan` | +| `ollama-cloud` | Ollama Cloud | `providers/ollama-cloud.js` | Cookie file at `~/.config/ollama-quota/cookie` (raw session cookie string) | ## Internal-only provider module - `providers/openai.js` exists for logic parity/reuse but is intentionally not registered for dispatcher ID routing. diff --git a/packages/web/server/lib/quota/index.js b/packages/web/server/lib/quota/index.js index e05cbade..926f1185 100644 --- a/packages/web/server/lib/quota/index.js +++ b/packages/web/server/lib/quota/index.js @@ -19,5 +19,6 @@ export { fetchZaiQuota, fetchNanoGptQuota, fetchMinimaxCodingPlanQuota, - fetchMinimaxCnCodingPlanQuota + fetchMinimaxCnCodingPlanQuota, + fetchOllamaCloudQuota } from './providers/index.js'; diff --git a/packages/web/server/lib/quota/providers/index.js b/packages/web/server/lib/quota/providers/index.js index b7f11014..33311b5a 100644 --- a/packages/web/server/lib/quota/providers/index.js +++ b/packages/web/server/lib/quota/providers/index.js @@ -18,6 +18,7 @@ import * as openrouter from './openrouter.js'; import * as zai from './zai.js'; import * as minimaxCodingPlan from './minimax-coding-plan.js'; import * as minimaxCnCodingPlan from './minimax-cn-coding-plan.js'; +import * as ollamaCloud from './ollama-cloud.js'; const registry = { claude: { @@ -85,6 +86,12 @@ const registry = { providerName: minimaxCnCodingPlan.providerName, isConfigured: minimaxCnCodingPlan.isConfigured, fetchQuota: minimaxCnCodingPlan.fetchQuota + }, + 'ollama-cloud': { + providerId: ollamaCloud.providerId, + providerName: ollamaCloud.providerName, + isConfigured: ollamaCloud.isConfigured, + fetchQuota: ollamaCloud.fetchQuota } }; @@ -142,3 +149,4 @@ export const fetchZaiQuota = zai.fetchQuota; export const fetchNanoGptQuota = nanogpt.fetchQuota; export const fetchMinimaxCodingPlanQuota = minimaxCodingPlan.fetchQuota; export const fetchMinimaxCnCodingPlanQuota = minimaxCnCodingPlan.fetchQuota; +export const fetchOllamaCloudQuota = ollamaCloud.fetchQuota; diff --git a/packages/web/server/lib/quota/providers/ollama-cloud.js b/packages/web/server/lib/quota/providers/ollama-cloud.js new file mode 100644 index 00000000..463b7dd7 --- /dev/null +++ b/packages/web/server/lib/quota/providers/ollama-cloud.js @@ -0,0 +1,112 @@ +import { homedir } from 'os'; +import { readFileSync, existsSync } from 'fs'; +import { join } from 'path'; +import { buildResult, toUsageWindow, toNumber } from '../utils/index.js'; + +const COOKIE_PATH = join(homedir(), '.config', 'ollama-quota', 'cookie'); + +export const providerId = 'ollama-cloud'; +export const providerName = 'Ollama Cloud'; +export const aliases = ['ollama-cloud', 'ollamacloud']; + +const readCookieFile = () => { + try { + if (!existsSync(COOKIE_PATH)) return null; + const content = readFileSync(COOKIE_PATH, 'utf-8'); + const trimmed = content.trim(); + return trimmed || null; + } catch { + return null; + } +}; + +const parseOllamaSettingsHtml = (html) => { + const windows = {}; + const sessionMatch = html.match(/Session\s+usage[^0-9]*([0-9.]+)%/i); + if (sessionMatch) { + windows.session = toUsageWindow({ + usedPercent: toNumber(sessionMatch[1]), + windowSeconds: null, + resetAt: null + }); + } + const weeklyMatch = html.match(/Weekly\s+usage[^0-9]*([0-9.]+)%/i); + if (weeklyMatch) { + windows.weekly = toUsageWindow({ + usedPercent: toNumber(weeklyMatch[1]), + windowSeconds: null, + resetAt: null + }); + } + const premiumMatch = html.match(/Premium[^0-9]*([0-9]+)\s*\/\s*([0-9]+)/i); + if (premiumMatch) { + const used = toNumber(premiumMatch[1]); + const total = toNumber(premiumMatch[2]); + const usedPercent = total && used !== null ? Math.min(100, (used / total) * 100) : null; + windows.premium = toUsageWindow({ + usedPercent, + windowSeconds: null, + resetAt: null, + valueLabel: `${used ?? 0} / ${total ?? 0}` + }); + } + return windows; +}; + +export const isConfigured = () => { + const cookie = readCookieFile(); + return Boolean(cookie); +}; + +export const fetchQuota = async () => { + const cookie = readCookieFile(); + + if (!cookie) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: false, + error: 'Not configured' + }); + } + + try { + const response = await fetch('https://ollama.com/settings', { + method: 'GET', + headers: { + Cookie: cookie, + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' + } + }); + + if (!response.ok) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: `API error: ${response.status}` + }); + } + + const html = await response.text(); + const windows = parseOllamaSettingsHtml(html); + + return buildResult({ + providerId, + providerName, + ok: true, + configured: true, + usage: { windows } + }); + } catch (error) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: error instanceof Error ? error.message : 'Request failed' + }); + } +}; \ No newline at end of file