Commit graph

87 commits

Author SHA1 Message Date
Evan Rice
17f7beb88c fix(router,client): token estimation always enabled; Ollama native eval fallback
- _history_kwargs unconditionally populates input_text/output_text for
cost tracking, regardless of save_history toggle.
- OllamaClient falls back to prompt_eval_count/eval_count before tiktoken.
- Release 0.5.2.
2026-06-10 02:07:24 +00:00
Evan Rice
a80746454c fix(updater): stash + hard-reset instead of merge-pull; release 0.5.1
The Apply Update endpoint used 'git pull origin {branch}', which fails
with a merge conflict if the working tree has any local modifications.
This happened to every install that had leftover version-string edits from
a prior partial update, leaving the old code running silently.

Fix: unconditionally stash (including untracked files) and reset to the
exact remote commit before reinstalling. The update now succeeds even
if the repo is dirty.

Bumps all version strings to 0.5.1 so existing 0.4.2 → 0.5.x installs
immediately receive the new updater logic.
2026-06-10 01:38:02 +00:00
Evan Rice
63327c7cb3 hotfix(v0.5.0): sync version strings + fix importlib metadata fallback
- __init__.py, app.py, pyproject.toml: bump to 0.5.0
- __init__.py: query guanaco-llm-proxy (not guanaco) and only override
  hardcoded if metadata version >= hardcoded version

Prevents stale guanaco-0.4.2.dist-info from clobbering the version.
Fixes apply_update showing wrong version after git-pull.
2026-06-10 01:25:06 +00:00
Evan Rice
b4ed4faebd chore: bump version to 0.5.0 and add comprehensive changelog
CHANGELOG covers:
- Token estimation fallback + skimtoken + fallback_reason audit
- ROI dashboard + OpenRouter price fetcher + cache-hit discount
- Real usage-level scraping from ollama.com (per-tag + top-level)
- Multi-account Ollama Cloud rotation + premium model routing
- Search provider plugins (8 backends)
- Config migration, install robustness, package rename
- Schema changes (usage_multiplier column removed, fallback_reason added)
- CI/CD, Docker, project hygiene improvements
- API changes (/v1/models + /api/ollama/models new fields)
- Known issues and deprecations
2026-06-10 01:01:21 +00:00
Evan Rice
25a0b1b653 feat(usage-levels): scrape real per-tag usage tiers from ollama.com library pages
- Add _fetch_usage_level_sync() to parse ollama.com/library/{model} HTML
- Handle both top-level model badges (x-test-model-cost-slot-active)
  and per-tag listings (x-test-model-tag-cost + x-test-model-tag-usage-slot-active)
- Add async fetch_usage_levels() with 1h global cache + parallel fetching
- Wire usage_multiplier into /v1/models and /api/ollama/models responses
- Replace heuristic _get_model_multiplier fallback with real scraped levels
- Fixes gemma3:4b/12b showing 1.00x instead of 0.25x, qwen3-vl 0.25x→0.75x, etc.
2026-06-10 00:57:12 +00:00
Evan Rice
00d7509d32 feat(v0.4.3): token estimation fallback, ROI dashboard, per-model usage breakdown
Token tracking:
- analytics.py: fall back to skimtoken estimation when API omits usage
  (~15% error, better than zero). Proper total_tokens = prompt + completion.
  Removed broken usage_multiplier column.
- router.py: unchanged — already passes usage → analytics correctly

Dashboard / ROI:
- roi.py: OpenRouter price fetcher with cache-hit discount logic
- dashboard.py: expose ROI config with cache_hit_pct slider
- dashboard.html: cost breakdown UI, per-model value scoring

Models:
- client.py: added minimax-m3, nemotron-3-ultra, kimi-k2.6 200k context
- client.py: per-model Ollama usage breakdown scraping from /settings

Config:
- config.py: backward compat for installs missing SearchConfig
2026-06-10 00:16:40 +00:00
evangit2
9793ce2c44 fix(config): v0.4.3 backward compat and install robustness
Changes:
- Add missing UsageConfig fields (last_plan, redirect_on_full, last_session_reset, last_weekly_reset, last_checked) to fix AttributeError crashes
- Add config migration layer in load_config() for v0.4.2 to v0.4.3+ configs
- Rename package guanaco to guanaco-llm-proxy to avoid PyPI collision
- Add Ollama API key validation during install.sh setup (fixes: use real key var, fix env file write, fix grep pattern)
- Add startup version sanity check detecting repo/venv mismatch
- Fix systemd service WorkingDirectory to repo checkout
- Add GUANACO_CONFIG_DIR env var to systemd service
2026-05-23 23:35:17 +00:00
evangit2
07fe4fd587 feat: experimental Subscription Value Calculator (ROI) vs OpenRouter
Adds a new experimental feature to the Status tab that compares your
total subscription cost to what you'd pay with OpenRouter's cheapest
provider pricing.

Backend:
- guanaco/roi.py: Fetches live prices from OpenRouter API, matches
  Ollama model names to OpenRouter model IDs via family inference
  (exact → family prefix → same parameter size → size window →
  global average), multiplies tokens by $/Mt cost.
- Calculates this week's cost, extrapolates to 100% weekly usage,
  then monthly. Shows ROI multiplier (monthly value / subscription).
- /dashboard/api/roi/calculate: fresh calculation
- /dashboard/api/roi/last: cached last result
- /dashboard/api/roi/reset: clear data
- /dashboard/api/roi/config: enable/disable, set price tier

Dashboard (Status tab):
- Toggle to enable/disable the feature
- Pro (0/mo) / Max (00/mo) / Custom price selector
- Shows: this-week cost, weekly @ 100%, monthly value, ROI multiplier
- Per-model table: prompt_tokens, completion_tokens, $/Mt in/out,
  total cost, % of usage
- Warnings for unmatched models and stale prices
- Reset button clears cached data

ROIConfig added to config.py (persisted in config.yaml).
2026-05-23 21:32:54 +00:00
evangit2
5242f662da feat: auto-infer capabilities and multiplier for unknown models
Previously new Ollama Cloud models showed no capability badges and
assumed 1.00x multiplier until manually added to KNOWN_CLOUD_MODELS.
Now both backend (client.py) and frontend (dashboard.html) fall
back to name-based inference for unknown models:

- Capabilities: 'vision' from vl/gemma/gemini/kimi/deepseek, 'tools'
  from coder/minimax/glm/mistral/gpt-oss/devstral/nemotron/deepseek,
  'thinking' from deepseek/cogito/reason/think and kimi-k* families
- Multiplier: extract :XXb|:Xt from model name for size-based tier
  (<=20b=0.25, <=80b=0.50, <=400b=0.75, >400b/1t=1.00), then fall
  back to name heuristics (nano/mini/small=0.25, flash/gemma=0.50,
  pro/mistral-large=1.00)

Models explicitly in KNOWN_CLOUD_MODELS still use exact values.
This means new models like deepseek-v5-pro or kimi-k3 show up
correctly in the dashboard immediately without code changes.
2026-05-23 21:18:02 +00:00
evangit2
9a2d0d76cd feat: cost-weighted analytics (usage_multiplier) + add deepseek-v4 models
- Add usage_multiplier column to analytics DB (auto-resolved from model name)
- Log weighted token totals (prompt*multiplier, completion*multiplier) in summary
- Show cost bars + weighted totals in dashboard analytics tab
- Add per-model multiplier column to analytics table
- Auto-compute total_tokens in analytics if not provided
- Add deepseek-v4-pro (1.0x) and deepseek-v4-flash (0.50x) to KNOWN_CLOUD_MODELS
- Add gemini-3-flash-preview to available_models and dashboard capability maps
- Add new models to config.py available_models list
2026-05-23 19:56:44 +00:00
evangit2
7180b27a8a v0.4.3 — add kimi-k2.6 vision model, default all roles to nemotron-3-nano:30b
- KNOWN_CLOUD_MODELS: add kimi-k2.6 with [vision, tools, thinking, cloud]
- LLMConfig defaults: reranker/scraper/summary/default all set to nemotron-3-nano:30b
- available_models: add nemotron-3-nano:30b and kimi-k2.6
- dashboard.html: add kimi-k2.6 and nemotron-3-nano capability maps
- cli.py setup wizard: update all model prompts/default placeholders
- version bump: 0.4.2 → 0.4.3 in __init__.py, app.py, pyproject.toml

Update-safe: only new model registry entries + default value changes.
Existing configs preserve their saved values (pydantic).
Zero schema or API break.
2026-05-22 20:48:24 +00:00
evangit2
c6741f92f3 v0.4.2: Multi-account Ollama rotation with quota-aware load balancing
Features:
- Multi-account rotation: Add multiple Ollama Cloud accounts with API keys
  and session cookies via Dashboard Accounts tab or API
- Quota-aware selection: AccountPool picker prefers least-loaded accounts;
  new/untested accounts get priority so they're validated immediately
- Premium model routing: kimi-k2.6 and glm-5.1 restricted to Pro/Max
  accounts only; free-tier accounts show warning badge in dashboard
- Account-level usage tracking: Per-account session/weekly percentages,
  plan type, and reset timers displayed in Dashboard Accounts tab
- Analytics account_name: New DB column logs which account handled each
  request; provider field also includes account suffix (ollama:ollama2)
- Dashboard Accounts tab: Full CRUD (add/remove/update-key/set-cookie/
  check-usage/test), usage progress bars, plan badges, premium warnings
- Config backward compatibility: Old configs without ollama_accounts
  auto-generate the primary account from legacy ollama_api_key + usage
  fields; no data loss on save/restart cycles

APIs:
- GET /api/accounts - List all accounts with usage data
- POST /api/accounts/add - Add new account
- POST /api/accounts/remove - Remove account by name
- POST /api/accounts/test - Test account connectivity
- POST /api/accounts/update-key - Update API key
- POST /api/accounts/session-cookie - Set session cookie
- POST /api/accounts/check-usage - Refresh usage from Ollama settings

DB Migration:
- account_name TEXT column added to request_log (auto-migrated on startup)
  All existing rows get NULL for account_name (no data loss)

Files changed:
- guanaco/accounts.py: AccountPool with model-aware filtering, PREMIUM_MODELS
- guanaco/analytics.py: account_name param in log_llm(), migration
- guanaco/app.py: AccountPool wiring into create_app()
- guanaco/config.py: ollama_accounts list, legacy sync, primary_account property
- guanaco/dashboard/dashboard.py: Account management API endpoints
- guanaco/dashboard/templates/dashboard.html: Accounts tab UI
- guanaco/router/router.py: _select_account(), provider=logging with account
- pyproject.toml: version 0.4.2
2026-04-21 02:05:56 +00:00
evangit2
95da493c54 feat: wire AccountPool into app.py, router.py, and dashboard.py signatures 2026-04-21 00:09:06 +00:00
evangit2
c7f71f9414 feat: OllamaClient supports per-request api_key override for multi-account 2026-04-20 23:58:40 +00:00
evangit2
cb163c0a99 feat: add AccountPool with quota-aware selection and round-robin fallback 2026-04-20 23:50:44 +00:00
evangit2
c9f30161e2 feat: add OllamaAccount model and ollama_accounts list to AppConfig 2026-04-20 23:49:57 +00:00
evangit2
44f132b4a7 v0.4.1: fix update banner after applying, fix history toggle reverting, fix version reporting
- checkForUpdate() now runs on page load (not just System tab click),
  so stale 'update available' banners clear after refreshing
- checkForUpdate() retries on network errors (service may be restarting)
- saveHistoryConfig() now syncs UI state from server POST response,
  preventing toggle drift on tab switch
- __init__.py version parsing uses strict semver regex, preventing
  RC/pre-release versions like '0.4.0rc1' from overriding the hardcoded
  fallback. Only clean X.Y.Z versions >= baseline are accepted.
- Bumped version to 0.4.1 across __init__.py, app.py, pyproject.toml
2026-04-20 22:07:53 +00:00
evangit2
ff99d56e9b release: v0.4.0 2026-04-20 21:32:54 +00:00
evangit2
20eee78eff fix: prerelease update check discovers newer pre-releases over stable 2026-04-20 21:30:05 +00:00
evangit2
3bc114204d Bump version to 0.4.0-rc.1 2026-04-20 21:01:43 +00:00
evangit2
586df5dda8 v0.4.0-rc1: Ollama concurrency limiter + 429 retry + smart defaults
New features:
- OllamaConcurrencyLimiter: asyncio.Semaphore caps concurrent Ollama requests (default: 8)
- 429 retry with exponential backoff + jitter (default: 2 retries, 1s base)
- Dashboard Status tab: concurrency stats (active, max, 429s/min)
- Dashboard Fallback tab: Max Concurrent + 429 Retries config inputs
- Dashboard /api/concurrency endpoint for real-time stats
- get_concurrency_limiter() module accessor for dashboard API

Config changes (FallbackProviderConfig defaults):
- primary_timeout: 30s → 120s (lets Ollama actually respond, semaphore handles congestion)
- max_concurrent_ollama: new field, default 8 (0 = unlimited)
- max_429_retries: new field, default 2
- backoff_base: new field, default 1.0s

Fixes:
- Prevents 429 'too many concurrent requests' from Ollama Cloud
- Queues excess requests instead of blasting them all at once
- Retries 429s with backoff instead of immediately failing to fallback
- Streaming path: 429 retry + semaphore on first chunk, proper release on stream end
- Non-streaming path: semaphore wrap + 429 retry loop
2026-04-20 20:58:49 +00:00
evangit2
c619747543 docs: add multi-account manager implementation plan 2026-04-20 00:01:57 +00:00
evangit2
eac061f512 Add fallback_reason column with explanation for why fallback triggered
- New fallback_reason TEXT column in request_log (auto-migrated)
- log_llm now accepts fallback_reason parameter
- All fallback log_llm calls in router.py now pass a reason:
  - 'Quota full (session=X%, weekly=Y%)' for quota redirects
  - 'Ollama error: <description>' for error-triggered fallbacks
  - Works for both streaming and non-streaming paths
- History list: fallback badge has title tooltip with reason,
  plus inline orange snippet showing reason text
- History modal: fallback banner shows 'Reason: <text>'
- Also: replaced emoji ℹ️ info icon with SVG icon for token tooltip
2026-04-19 23:56:34 +00:00
evangit2
44e8fea950 History: replace blocking alert with modal warning for no-content entries
- Remove the alert() blocker that prevented modal from opening for entries
  without content (was showing 'logged before history was enabled')
- Now the modal always opens showing metadata (model, duration, tokens, etc.)
- Add orange warning banner when content is missing: explains it was either
  from an older instance or pre-history code
- Remove redundant 'Input not saved' / 'Output not saved' stubs
- If content exists, input/output sections render with full text
- If content is missing, just the orange warning explains why
2026-04-19 23:28:54 +00:00
evangit2
980d01c444 history: skip detail modal for entries with no content or error 2026-04-19 22:54:09 +00:00
evangit2
1c3d80962e analytics: add info tooltip on Per-Model Stats explaining token estimation 2026-04-19 22:48:42 +00:00
evangit2
1f06a45382 history: show failed requests with error details, add filter
- List rows: red left border + error snippet on failed requests
- Detail modal: full error with monospace formatting, provider info
- Add 'Failed Only' filter option in dropdown
- Add errors_only param to get_history() API
- Add error_count to history stats display
2026-04-19 22:32:17 +00:00
evangit2
47a01aee3b fix: modal close button needs double-click — use class selector instead of traversing DOM
closest('div') hits the sticky header div first, not the modal overlay.
Added .history-modal class to both modals, close with closest('.history-modal').remove()
2026-04-19 22:28:47 +00:00
evangit2
391af6630e v0.4.0: History logging fixes and improvements
- Fix: showTab() now includes 'history' in tab visibility list
- Fix: showTab() no longer crashes on programmatic calls (uses querySelector)
- Fix: Add has_content boolean to get_history() for badge rendering
- Fix: _extract_output_text() captures reasoning content from GLM models
- Fix: _extract_sse_content() extracts both content and reasoning from SSE
- Fix: Router streaming paths properly accumulate output for history
- Enhancement: History tab now loads config/logs when switched to

These fixes enable the History tab to display request entries with content badges,
and properly capture both input_text and output_text (including reasoning) for
both streaming and non-streaming LLM requests.
2026-04-19 22:18:18 +00:00
evangit2
030bcdb1f2 Merge remote-tracking branch 'origin/main' into develop 2026-04-13 20:34:06 +00:00
evan
b7c6245f39 Add fallback rate metric to analytics dashboard
- Change 'Ollama Weekly' label to 'Fallback Rate' in analytics overview
- Calculate fallback_rate as % of main provider requests routed to fallback
- Uses 24h rolling window for rate calculation
- Add get_fallback_rate() method for dedicated fallback queries
- Add optional full history fields (opt-in): source_ip, user_agent, input/output text
2026-04-13 18:27:29 +00:00
evangit2
d7501c84b6 fix: update flow uses stop+start instead of restart
The auto-update now does stop → 2s gap → start instead of restart.
This ensures the old process is fully dead before the new one starts,
preventing the common issue where the new process fails to bind
the port because the old one hasn't released it yet.

Also increased the gap between stop and start from 1s to 2s.
2026-04-10 21:05:42 +00:00
evangit2
88a97a1770 v0.3.9: fallback stream fix, summarize toggle fix, version single-source
## Bug Fixes

- **Fallback stream payload**: _call_fallback_provider() now always injects
  payload["stream"] = stream into the JSON body. Previously, stream was only
  passed as a function arg for httpx routing, not included in the actual JSON.
  This caused providers like Fireworks to reject requests with max_tokens > 4096
  that lacked "stream": true in the body.

- **Summarize toggle persistence**: Two-part fix:
  1. Backend: POST /api/config now returns the search config in the response
     (was only returning llm + fallback, so the JS callback had no data to sync)
  2. Frontend: saveSearchConfig() now uses the POST response to update toggle
     state instead of re-fetching via GET, which could return stale/empty data
     on partially-updated installs

- **Version single-source**: Removed all importlib.metadata version lookups.
  app.py now hardcodes __version__ = "0.3.9". check_for_update and apply_update
  import from guanaco.app. This fixes health endpoint returning stale versions
  (e.g. "0.3.0" while dashboard showed "0.3.8")

- **More reliable auto-update restart**: Changed from systemctl restart to
  stop + 1s gap + start, which is more reliable for killing stubborn processes

## Tests

- Added tests/test_fallback_stream.py covering streaming and non-streaming
  fallback payload injection, and verifying original payloads aren't mutated
2026-04-10 20:49:00 +00:00
evangit2
0bbc3298e6 hotfix: JS fallback for empty endpoints — KNOWN_ENDPOINTS hardcodes all 8 providers as client-side safety net 2026-04-10 20:03:18 +00:00
evangit2
5c1e226bac Merge develop: v0.3.8
# Conflicts:
#	guanaco/__init__.py
2026-04-10 19:53:33 +00:00
evangit2
2f133097dd v0.3.8: Endpoints hotfix, search settings with summarize toggle, remove playground 2026-04-10 19:53:22 +00:00
evangit2
3cc9b62e4a v0.3.8-dev: Fix endpoints hotfix (immutable tuples, union merge, tuple-to-dict serialization), fix broken JS from playground removal, add search config with summarize toggle 2026-04-10 19:51:17 +00:00
evangit2
331117c4dd v0.3.8-dev: Fix mutable endpoints bug, replace search playground with search settings + summarize toggle 2026-04-10 19:37:53 +00:00
evangit2
b33fe92e17 Merge develop: v0.3.7 2026-04-10 18:56:03 +00:00
evangit2
1b6eb603c2 v0.3.7 - Search tab, summarize feature, enhanced endpoints
- Endpoints tab: restored search provider endpoints card with dynamic
  rendering showing all endpoints per provider with HTTP method badges
- New Search tab with playground: test any search provider or get an
  AI summary of results in one click
- Summarize feature (BETA): /dashboard/api/summarize endpoint that
  searches the web then feeds results to the configured summary_model
  for an AI-generated summary with source citations
- Handles thinking models (qwen3.5) correctly: checks both content
  and reasoning fields, uses max_tokens=4096 to avoid empty responses
- Provider endpoints are now defined as class metadata (endpoints
  property) and rendered dynamically instead of hardcoded in JS
- Bump version to 0.3.7
2026-04-10 18:55:50 +00:00
evangit2
252b59eb9b v0.3.7-dev: Dynamic provider endpoint display
- Add endpoints class property to ProviderEmulator base + all 8 providers
  listing their routes with path and HTTP method
- Dashboard backend now includes endpoint metadata in PROVIDERS JS object
- Dashboard JS renders all endpoints per provider dynamically instead of
  hardcoding a single endpoint per provider
- Each endpoint shows HTTP method badge (POST, GET/POST) next to copy URL
- updateEndpoints() function uses PROVIDERS data instead of hardcoded map
2026-04-10 18:13:33 +00:00
evangit2
05a17b904e Merge develop: v0.3.6 - Fix auto-update system 2026-04-10 17:29:45 +00:00
evangit2
982a4f8270 v0.3.6 - Fix auto-update system
- app.py: use importlib.metadata for version (no cross-module import
  that breaks under systemd/uvicorn). Fallback to hardcoded 0.3.6.
- dashboard: apply_update uses BackgroundTask for restart, so the HTTP
  response is sent BEFORE the service kills itself. Previously the
  response was lost because systemctl restart terminated the process.
- dashboard: add import validation step before restart — tests that
  create_app() and importlib.metadata.version() both work. If the
  update would crash-loop, the handler returns an error instead and
  does NOT restart. This prevents the v0.3.3→0.3.5 bootloop scenario.
- dashboard: remove unused subprocess import in check_for_update.
- All version sources now use importlib.metadata consistently
  (app.py, __init__.py, update check, health endpoint).
- Bump version to 0.3.6.
2026-04-10 17:27:46 +00:00
evangit2
787a1b10f5 v0.3.5 - Thinking model support, dynamic versioning, prerelease updates 2026-04-10 16:57:48 +00:00
evangit2
449df52da1 Remove stale hardcoded version fallbacks in dashboard 2026-04-10 16:53:23 +00:00
evangit2
a68b8bb709 Make prerelease updates opt-in via allow_prerelease config
Default behavior on main: only check stable releases (/releases/latest).
Set router.allow_prerelease: true in config to also see prereleases.
This keeps production users on stable releases while develop/test
instances can opt into cutting-edge builds.
2026-04-10 16:47:58 +00:00
evangit2
b8fd5621fd Clean up version management: single source of truth from importlib
- app.py: replace hardcoded __version__ with import from guanaco.__init__
- __init__.py: update fallback version to 0.3.5 to match pyproject.toml
- Version now lives in pyproject.toml only, propagates via importlib
2026-04-10 16:43:44 +00:00
evangit2
469d2ae92b Fix update check: include prereleases, auto-detect current branch 2026-04-10 16:35:24 +00:00
evangit2
5497ee9868 Bump version to 0.3.5 for develop prerelease 2026-04-10 16:14:21 +00:00
evangit2
fd800dd661 Fix streaming metrics, reasoning token handling, and version display
- Fix: thinking models (GLM) sent 0 tokens in streaming analytics because
  only 'content' was counted, not 'reasoning'/'reasoning_content' fields.
  Now tracks content_chars and reasoning_chars separately, estimates tokens
  at 4 chars/token
- Fix: _is_empty_stream_buffer false-positive on GLM streams — checked
  'reasoning_content' but Ollama Cloud sends 'reasoning'. Now checks both
- Fix: _describe_error crashes on streaming httpx errors — catches
  ResponseNotRead when accessing response.text
- Fix: fallback provider ReadTimeout on thinking models — streaming fallback
  now uses 120s read timeout (was 30s)
- Fix: /health and FastAPI version hardcoded to 0.3.0 — now uses __version__
  from importlib.metadata
- Fix: double [DONE] in streaming output — client uses return instead of
  break in [DONE] handler
- Feat: streaming analytics captures prompt_tokens from usage data
- Feat: avg TPS/TTFT calculated over most recent 10k completion tokens
  instead of all-time average
- Feat: completion_tokens prefers API usage data over per-chunk count
2026-04-10 16:05:12 +00:00