perplexity-web-mcp-cli 0.12.0__tar.gz → 0.12.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/PKG-INFO +9 -2
  2. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/README.md +8 -1
  3. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/pyproject.toml +1 -1
  4. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/cli/ai_doc.py +17 -13
  5. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/cli/main.py +21 -18
  6. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/council.py +9 -13
  7. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/data/SKILL.md +12 -9
  8. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/data/references/mcp-tools.md +1 -1
  9. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/data/references/models.md +1 -1
  10. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/mcp/server.py +32 -21
  11. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/models.py +1 -1
  12. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/shared.py +85 -20
  13. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/__init__.py +0 -0
  14. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/api/__init__.py +0 -0
  15. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/api/responses.py +0 -0
  16. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/api/server.py +0 -0
  17. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/api/session_manager.py +0 -0
  18. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/api/tool_calling.py +0 -0
  19. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/cli/__init__.py +0 -0
  20. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/cli/auth.py +0 -0
  21. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/cli/doctor.py +0 -0
  22. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/cli/hack.py +0 -0
  23. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/cli/setup.py +0 -0
  24. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/cli/skill.py +0 -0
  25. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/config.py +0 -0
  26. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/constants.py +0 -0
  27. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/core.py +0 -0
  28. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/data/references/api-endpoints.md +0 -0
  29. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/enums.py +0 -0
  30. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/exceptions.py +0 -0
  31. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/http.py +0 -0
  32. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/limits.py +0 -0
  33. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/logging.py +0 -0
  34. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/mcp/__init__.py +0 -0
  35. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/mcp/__main__.py +0 -0
  36. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/py.typed +0 -0
  37. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/rate_limits.py +0 -0
  38. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/resilience.py +0 -0
  39. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/router.py +0 -0
  40. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/sessions.py +0 -0
  41. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/token_store.py +0 -0
  42. {perplexity_web_mcp_cli-0.12.0 → perplexity_web_mcp_cli-0.12.2}/src/perplexity_web_mcp/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: perplexity-web-mcp-cli
3
- Version: 0.12.0
3
+ Version: 0.12.2
4
4
  Summary: CLI, MCP server, and Anthropic/OpenAI API-compatible interface for Perplexity AI.
5
5
  Keywords: perplexity,ai,mcp,anthropic,api,client
6
6
  Author: Jacob BD
@@ -41,6 +41,13 @@ Description-Content-Type: text/markdown
41
41
 
42
42
  # Perplexity Web MCP & CLI
43
43
 
44
+ [![PyPI version](https://img.shields.io/pypi/v/perplexity-web-mcp-cli)](https://pypi.org/project/perplexity-web-mcp-cli/)
45
+ [![PyPI downloads](https://img.shields.io/pypi/dm/perplexity-web-mcp-cli)](https://pypistats.org/packages/perplexity-web-mcp-cli)
46
+ [![Total downloads](https://static.pepy.tech/badge/perplexity-web-mcp-cli)](https://pepy.tech/projects/perplexity-web-mcp-cli)
47
+ [![Python](https://img.shields.io/pypi/pyversions/perplexity-web-mcp-cli)](https://pypi.org/project/perplexity-web-mcp-cli/)
48
+ [![License](https://img.shields.io/pypi/l/perplexity-web-mcp-cli)](https://github.com/jacob-bd/perplexity-web-mcp/blob/main/LICENSE)
49
+ [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-FFDD00?style=flat-square&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/jacobbd)
50
+
44
51
  <p align="center">
45
52
  <a href="https://youtu.be/9xyClDvmoZ0">
46
53
  <img src="https://img.youtube.com/vi/9xyClDvmoZ0/maxresdefault.jpg" alt="Perplexity Powerhouse + Model Council Demo" width="400">
@@ -233,7 +240,7 @@ pwm research "NVIDIA competitive landscape" -s finance --json
233
240
  Query multiple models in parallel and get a synthesized consensus. Each model costs 1 Pro Search. Default synthesis uses Sonar 2 (also 1 Pro Search).
234
241
 
235
242
  ```bash
236
- # Default: GPT-5.4, Claude Opus, Gemini Pro + Sonar 2 synthesis (4 Pro Searches)
243
+ # Default: GPT-5.4, Claude Sonnet, Gemini Pro + Sonar 2 synthesis (4 Pro Searches)
237
244
  pwm council "What are best practices for microservices?"
238
245
  ```
239
246
 
@@ -4,6 +4,13 @@
4
4
 
5
5
  # Perplexity Web MCP & CLI
6
6
 
7
+ [![PyPI version](https://img.shields.io/pypi/v/perplexity-web-mcp-cli)](https://pypi.org/project/perplexity-web-mcp-cli/)
8
+ [![PyPI downloads](https://img.shields.io/pypi/dm/perplexity-web-mcp-cli)](https://pypistats.org/packages/perplexity-web-mcp-cli)
9
+ [![Total downloads](https://static.pepy.tech/badge/perplexity-web-mcp-cli)](https://pepy.tech/projects/perplexity-web-mcp-cli)
10
+ [![Python](https://img.shields.io/pypi/pyversions/perplexity-web-mcp-cli)](https://pypi.org/project/perplexity-web-mcp-cli/)
11
+ [![License](https://img.shields.io/pypi/l/perplexity-web-mcp-cli)](https://github.com/jacob-bd/perplexity-web-mcp/blob/main/LICENSE)
12
+ [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-FFDD00?style=flat-square&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/jacobbd)
13
+
7
14
  <p align="center">
8
15
  <a href="https://youtu.be/9xyClDvmoZ0">
9
16
  <img src="https://img.youtube.com/vi/9xyClDvmoZ0/maxresdefault.jpg" alt="Perplexity Powerhouse + Model Council Demo" width="400">
@@ -196,7 +203,7 @@ pwm research "NVIDIA competitive landscape" -s finance --json
196
203
  Query multiple models in parallel and get a synthesized consensus. Each model costs 1 Pro Search. Default synthesis uses Sonar 2 (also 1 Pro Search).
197
204
 
198
205
  ```bash
199
- # Default: GPT-5.4, Claude Opus, Gemini Pro + Sonar 2 synthesis (4 Pro Searches)
206
+ # Default: GPT-5.4, Claude Sonnet, Gemini Pro + Sonar 2 synthesis (4 Pro Searches)
200
207
  pwm council "What are best practices for microservices?"
201
208
  ```
202
209
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "perplexity-web-mcp-cli"
3
- version = "0.12.0"
3
+ version = "0.12.2"
4
4
  description = "CLI, MCP server, and Anthropic/OpenAI API-compatible interface for Perplexity AI."
5
5
  authors = [{ name = "Jacob BD" }]
6
6
  license = "MIT"
@@ -66,7 +66,7 @@ MODEL COUNCIL
66
66
  pwm council "query" --json Output as JSON
67
67
 
68
68
  Each model in the council costs 1 Pro Search, plus 1 for synthesis. Default = 4 Pro Searches.
69
- Available models: gpt54, gpt55, claude_sonnet, claude_opus, gemini_pro, nemotron, kimi_k26
69
+ Available models: sonar, gpt54, gpt55, claude_sonnet, claude_opus, gemini_pro, nemotron, kimi_k26
70
70
  Thinking toggle: -t / --thinking (gpt54, gpt55, claude_sonnet, claude_opus, kimi_k26 support toggle;
71
71
  gemini_pro and nemotron are always thinking)
72
72
 
@@ -110,7 +110,7 @@ MODELS
110
110
  Name Identifier Thinking Notes
111
111
  ----------- ---------------------- --------- ---------------------------
112
112
  auto pplx_pro No Auto-selects best model
113
- sonar experimental No Sonar 2 (latest in-house)
113
+ sonar experimental No Sonar 2 (concise search mode for grounded responses)
114
114
  deep_research pplx_alpha No In-depth reports (monthly quota)
115
115
  gpt54 gpt54 Yes OpenAI GPT-5.4 (versatile)
116
116
  gpt55 gpt55 Yes OpenAI GPT-5.5 (latest, Max tier)
@@ -168,12 +168,13 @@ QUERY TOOLS (each call costs 1 Pro Search query unless noted):
168
168
  pplx_ask(query, source_focus="web")
169
169
  Auto-selects best model. 1 PRO SEARCH per call.
170
170
 
171
- pplx_council(query, source_focus="web", models="gpt54,claude_opus,gemini_pro",
171
+ pplx_council(query, source_focus="web", models="gpt54,claude_sonnet,gemini_pro",
172
172
  synthesize=True, thinking=False, chairman="sonar")
173
173
  Model Council — N PRO SEARCHES (1 per model selected).
174
174
  BEFORE CALLING: You MUST ask the user which models and how many.
175
- Available: gpt54, gpt55, claude_sonnet, claude_opus, gemini_pro, nemotron, kimi_k26.
176
- Default: 3 models (GPT-5.4, Claude Opus, Gemini Pro) + synthesis = 4 Pro Searches.
175
+ Available: sonar, gpt54, gpt55, claude_sonnet, claude_opus, gemini_pro, nemotron, kimi_k26.
176
+ Max-only: gpt55, claude_opus. Exclude these when Subscription is Pro.
177
+ Default: 3 Pro-compatible models (GPT-5.4, Claude Sonnet, Gemini Pro) + synthesis = 4 Pro Searches.
177
178
  Synthesis uses Sonar 2 by default. Set chairman to override.
178
179
  Non-sonar chairman costs 1 extra Pro Search.
179
180
  Set synthesize=False to skip synthesis entirely.
@@ -200,9 +201,9 @@ QUERY TOOLS (each call costs 1 Pro Search query unless noted):
200
201
  All query tools accept source_focus: "none", "web", "academic", "social",
201
202
  "finance", "all". Use "none" for model-only queries without web search.
202
203
 
203
- All query tools also accept an optional `conversation_id` (str) parameter.
204
- The server returns `[Conversation ID: <uuid>]` at the end of each response.
205
- Extract this UUID and pass it to the next query to maintain context across
204
+ All query tools also accept an optional `conversation_id` (str) parameter.
205
+ The server returns `[Conversation ID: <uuid>]` at the end of each response.
206
+ Extract this UUID and pass it to the next query to maintain context across
206
207
  multiple turns. State is retained in memory for 1 hour.
207
208
 
208
209
  USAGE TOOL (1):
@@ -271,8 +272,8 @@ weekly pool (~300). Deep Research draws from a tiny monthly pool (~5-10).
271
272
  Wasting Pro queries on simple lookups means nothing left for real questions.
272
273
 
273
274
  COST MODEL:
274
- Sonar 2 (pplx_sonar, In-house model; still uses your session and Perplexity
275
- quick intent) counters check pplx_usage() / pwm usage.
275
+ Sonar 2 (pplx_sonar, In-house model; uses concise search mode to guarantee responses
276
+ quick intent) are grounded. Decrements Perplexity session counters.
276
277
  Pro Search (standard, Typically 1 from weekly Pro Search pool (~300/week
277
278
  detailed, pplx_ask, on Pro/Max; exact rules are enforced by Perplexity).
278
279
  pplx_query, premium
@@ -283,11 +284,14 @@ COST MODEL:
283
284
  MANDATORY PROTOCOL:
284
285
  1. CHECK QUOTA FIRST: Call pplx_usage() before your first query each session.
285
286
  2. DEFAULT TO QUICK: Use pplx_smart_query(intent='quick') for most lookups.
286
- It prefers Sonar 2 first and only escalates when the query needs a premium model.
287
+ It prefers Sonar 2 first (using concise mode to guarantee grounding) and
288
+ only escalates when the query needs a premium model.
287
289
  3. ESCALATE ONLY WHEN NEEDED: Use 'standard' for multi-source synthesis,
288
290
  'detailed' for complex analysis, 'research' only when user requests it.
289
291
  4. NEVER USE DEEP RESEARCH AUTONOMOUSLY — always ask the user first.
290
- 5. COUNCIL: Before calling pplx_council, ASK the user which models and how
292
+ 5. SUBSCRIPTION-AWARE MODELS: Read the Subscription line from pplx_usage().
293
+ If it is Pro, exclude Max-only models: gpt55 and claude_opus.
294
+ 6. COUNCIL: Before calling pplx_council, ASK the user which models and how
291
295
  many. Each model = 1 Pro Search. List available models for them to choose.
292
296
 
293
297
  WHEN TO USE EACH INTENT:
@@ -296,7 +300,7 @@ WHEN TO USE EACH INTENT:
296
300
  detailed Complex analysis, deep reasoning, premium model → 1 Pro
297
301
  research Comprehensive reports (user must request explicitly) → 1 Research
298
302
 
299
- DECISION RULE: Ask "Can Sonar 2 answer this?" If yes → quick. If no → standard.
303
+ DECISION RULE: Ask "Can Sonar 2 answer this?" If yes → quick (grounded on concise search). If no → standard.
300
304
  Only use detailed/research when the complexity genuinely demands it.
301
305
  When in doubt, start with quick and escalate if the answer is insufficient.
302
306
 
@@ -27,14 +27,16 @@ import rich_click as click
27
27
 
28
28
  from perplexity_web_mcp.exceptions import AuthenticationError, RateLimitError
29
29
  from perplexity_web_mcp.shared import (
30
+ COUNCIL_DEFAULT_MODELS_STR,
30
31
  COUNCIL_DISPLAY_NAMES,
32
+ COUNCIL_ELIGIBLE_MODEL_NAMES,
31
33
  MODEL_MAP,
32
34
  MODEL_NAMES,
33
35
  SOURCE_FOCUS_NAMES,
34
- THINKING_TOGGLEABLE,
35
36
  Models,
36
37
  SourceFocusName,
37
38
  ask,
39
+ build_council_model_list,
38
40
  get_limit_cache,
39
41
  resolve_model,
40
42
  )
@@ -230,7 +232,7 @@ def _cmd_research_impl(query, source, json_output):
230
232
 
231
233
  # ── Council ────────────────────────────────────────────────────────────────
232
234
 
233
- COUNCIL_MODEL_NAMES = tuple(n for n in MODEL_NAMES if n not in {"auto", "sonar", "deep_research"})
235
+ COUNCIL_MODEL_NAMES = COUNCIL_ELIGIBLE_MODEL_NAMES
234
236
 
235
237
 
236
238
  @cli.command()
@@ -239,7 +241,7 @@ COUNCIL_MODEL_NAMES = tuple(n for n in MODEL_NAMES if n not in {"auto", "sonar",
239
241
  "-m",
240
242
  "--models",
241
243
  "models_str",
242
- default="gpt54,claude_opus,gemini_pro",
244
+ default=COUNCIL_DEFAULT_MODELS_STR,
243
245
  help=f"Comma-separated models ({', '.join(COUNCIL_MODEL_NAMES)}).",
244
246
  )
245
247
  @click.option("-t", "--thinking", is_flag=True, help="Enable extended thinking mode.")
@@ -301,14 +303,8 @@ def _cmd_council_impl(query, models_str, source, synthesize, json_output, thinki
301
303
 
302
304
  # Build model list (None = use defaults)
303
305
  model_list = None
304
- if models_str != "gpt54,claude_opus,gemini_pro":
305
- model_list = []
306
- for name in model_names:
307
- resolved = resolve_model(name, thinking=thinking)
308
- display = COUNCIL_DISPLAY_NAMES.get(name, name)
309
- if thinking and name in THINKING_TOGGLEABLE:
310
- display += " Thinking"
311
- model_list.append((display, resolved))
306
+ if models_str != COUNCIL_DEFAULT_MODELS_STR:
307
+ model_list = build_council_model_list(model_names, thinking=thinking)
312
308
 
313
309
  synthesis_model = resolve_model(chairman) if chairman != "sonar" else None
314
310
 
@@ -454,16 +450,23 @@ def _cmd_usage_impl(refresh):
454
450
 
455
451
  # ── Account Info ───────────────────────────────────────────────────────
456
452
  settings = cache.get_user_settings(force_refresh=refresh)
457
- if settings:
453
+ from perplexity_web_mcp.cli.auth import get_user_info
454
+
455
+ user_info = get_user_info(token)
456
+ if settings or user_info:
458
457
  table = Table(title="👤 Account", show_header=True, header_style="bold cyan")
459
458
  table.add_column("Field", style="bold")
460
459
  table.add_column("Value", justify="right")
461
460
 
462
- tier = (settings.subscription_tier or "unknown").title()
463
- status = settings.subscription_status
464
- table.add_row("Subscription", f"[bold]{tier}[/] ({status})")
465
- table.add_row("Total Queries", f"{settings.query_count:,}")
466
- table.add_row("Pro Queries", f"{settings.query_count_copilot:,}")
461
+ if user_info:
462
+ table.add_row("Subscription", f"[bold]{user_info.tier_display}[/]")
463
+
464
+ if settings:
465
+ billing = settings.subscription_tier or "unknown"
466
+ status = settings.subscription_status
467
+ table.add_row("Billing", f"[bold]{billing}[/] ({status})")
468
+ table.add_row("Total Queries", f"{settings.query_count:,}")
469
+ table.add_row("Pro Queries", f"{settings.query_count_copilot:,}")
467
470
 
468
471
  console.print(table)
469
472
 
@@ -732,7 +735,7 @@ def _cmd_council(args: list[str]) -> int:
732
735
  return 1
733
736
 
734
737
  query = args[0]
735
- models_str = "gpt54,claude_opus,gemini_pro"
738
+ models_str = COUNCIL_DEFAULT_MODELS_STR
736
739
  source: SourceFocusName = "web"
737
740
  synthesize = True
738
741
  json_output = False
@@ -16,6 +16,7 @@ from .config import ConversationConfig
16
16
  from .enums import CitationMode, SearchFocus, SourceFocus
17
17
  from .logging import get_logger
18
18
  from .models import Model, Models
19
+ from .shared import COUNCIL_DEFAULT_MODEL_NAMES, build_council_model_list
19
20
 
20
21
 
21
22
  if TYPE_CHECKING:
@@ -29,19 +30,14 @@ logger = get_logger(__name__)
29
30
  # Default council composition
30
31
  # ---------------------------------------------------------------------------
31
32
 
32
- COUNCIL_DEFAULT_MODELS: list[tuple[str, Model]] = [
33
- ("GPT-5.4", Models.GPT_54),
34
- ("Claude Opus 4.7", Models.CLAUDE_47_OPUS),
35
- ("Gemini 3.1 Pro", Models.GEMINI_31_PRO_THINKING),
36
- ]
37
- """Default models for the council (3 diverse providers)."""
33
+ COUNCIL_DEFAULT_MODELS: list[tuple[str, Model]] = build_council_model_list(COUNCIL_DEFAULT_MODEL_NAMES)
34
+ """Default Pro-compatible models for the council (3 diverse providers)."""
38
35
 
39
- COUNCIL_DEFAULT_MODELS_THINKING: list[tuple[str, Model]] = [
40
- ("GPT-5.4 Thinking", Models.GPT_54_THINKING),
41
- ("Claude Opus 4.7 Thinking", Models.CLAUDE_47_OPUS_THINKING),
42
- ("Gemini 3.1 Pro", Models.GEMINI_31_PRO_THINKING),
43
- ]
44
- """Default models for the council with extended thinking enabled."""
36
+ COUNCIL_DEFAULT_MODELS_THINKING: list[tuple[str, Model]] = build_council_model_list(
37
+ COUNCIL_DEFAULT_MODEL_NAMES,
38
+ thinking=True,
39
+ )
40
+ """Default Pro-compatible models for the council with extended thinking enabled."""
45
41
 
46
42
 
47
43
  # ---------------------------------------------------------------------------
@@ -220,7 +216,7 @@ def council_ask(
220
216
  Args:
221
217
  query: The question to ask all models.
222
218
  models: List of (display_name, Model) tuples. Defaults to
223
- COUNCIL_DEFAULT_MODELS (GPT-5.4, Claude Opus, Gemini Pro).
219
+ COUNCIL_DEFAULT_MODELS (GPT-5.4, Claude Sonnet, Gemini Pro).
224
220
  source_focus: Source focus for all queries (none/web/academic/social/finance/all).
225
221
  synthesize: Whether to produce a synthesized consensus (adds 1 Sonar 2 synthesis query by default).
226
222
  thinking: Use thinking model variants for default council members.
@@ -2,7 +2,7 @@
2
2
  name: perplexity-web-mcp
3
3
  description: "Search the web and query AI models via Perplexity AI using perplexity-web-mcp-cli. Supports CLI commands (pwm ask, pwm research), MCP tools (pplx_*), and Anthropic/OpenAI-compatible API server. Use when the user mentions \"perplexity\", \"pplx\", \"pwm\", \"web search with AI\", \"deep research\", \"search the internet\", or wants to query premium models like GPT-5.4, GPT-5.5, Claude, Gemini, Nemotron through Perplexity's web interface."
4
4
  metadata:
5
- version: "0.10.7"
5
+ version: "0.12.2"
6
6
  author: "Jacob BD"
7
7
  ---
8
8
 
@@ -46,8 +46,9 @@ the weekly pool fast, leaving nothing for questions that actually need it.
46
46
  ### Before Every Session
47
47
 
48
48
  1. **Check quota first**: Call `pplx_usage()` (MCP) or `pwm usage` (CLI) before your first query.
49
- 2. Review the remaining Pro and Research counts.
50
- 3. If Pro < 20% remaining, restrict yourself to quick/Sonar 2 for everything except user-requested Pro queries.
49
+ 2. Review the remaining Pro and Research counts and the `Subscription` line.
50
+ 3. If Subscription is Pro, exclude Max-only models (`gpt55`, `claude_opus`) from model selection and councils.
51
+ 4. If Pro < 20% remaining, restrict yourself to quick/Sonar 2 for everything except user-requested Pro queries.
51
52
 
52
53
  ### Before Every Query: Choose the Lowest Sufficient Tier
53
54
 
@@ -83,8 +84,9 @@ Ask yourself: **"Can Sonar 2 answer this?"** If yes, use `quick`. Only escalate
83
84
  - The user needs high-confidence answers validated across multiple AI providers
84
85
  - Important decisions, fact-checking, or complex analysis
85
86
  - BEFORE calling: ASK the user which models and how many (each = 1 Pro Search)
86
- - Available models: gpt54, gpt55, claude_sonnet, claude_opus, gemini_pro, nemotron, kimi_k26
87
- - Default: 3 models (GPT-5.4, Claude Opus, Gemini Pro) + synthesis = 4 Pro Searches
87
+ - Available models: sonar, gpt54, gpt55, claude_sonnet, claude_opus, gemini_pro, nemotron, kimi_k26
88
+ - Max-only models: gpt55, claude_opus. Do not use these for Pro subscriptions.
89
+ - Default: 3 Pro-compatible models (GPT-5.4, Claude Sonnet, Gemini Pro) + synthesis = 4 Pro Searches
88
90
 
89
91
  ### Decision Flowchart
90
92
 
@@ -127,7 +129,7 @@ The smart router automatically protects you:
127
129
  - **Healthy quota**: Uses the ideal model for your intent
128
130
  - **Low quota (<20% pro remaining)**: Response footer warns you to conserve
129
131
  - **Critical quota (<10% pro remaining)**: Downgrades detailed→auto to conserve
130
- - **Exhausted quota**: Falls back to Sonar 2 for everything except research
132
+ - **Exhausted quota**: Falls back to Sonar 2 for everything except research (Sonar 2 is forced to concise mode to ensure grounded responses using search results)
131
133
  - **Research exhausted**: Falls back to premium Pro Search
132
134
  - Response metadata shows what model was used, why, and remaining quota
133
135
 
@@ -237,7 +239,8 @@ pwm ask "protein folding advances" -m gemini_pro -s academic --json
237
239
  ### Model Council
238
240
 
239
241
  Query multiple models in parallel and get a synthesized consensus.
240
- Each model in the council costs 1 Pro Search, plus 1 for Sonar 2 synthesis. Default: 3 models + synthesis = 4 Pro Searches.
242
+ Each model in the council costs 1 Pro Search, plus 1 for Sonar 2 synthesis. Default: 3 Pro-compatible models + synthesis = 4 Pro Searches.
243
+ Before selecting models, check `pplx_usage()` or `pwm usage`. If the subscription is Pro, exclude Max-only models (`gpt55`, `claude_opus`).
241
244
 
242
245
  ```bash
243
246
  pwm council "What are the best practices for microservices?" # default 3 models
@@ -283,7 +286,7 @@ pwm usage --refresh # Force-refresh from server
283
286
  | `pplx_sonar` | 1 Pro Search | Perplexity Sonar 2 |
284
287
  | `pplx_query` | 1 Pro | Explicit model selection with thinking toggle |
285
288
  | `pplx_ask` | 1 Pro | Quick Q&A (auto model) |
286
- | `pplx_council` | **N+1 Pro** (1 per model + 1 synthesis) | Model Council — **ASK USER which models first!** Supports `thinking=True` and `chairman` for synthesis model. |
289
+ | `pplx_council` | **N+1 Pro** (1 per model + 1 synthesis) | Model Council — **ASK USER which models first!** Check subscription first; exclude Max-only `gpt55`/`claude_opus` on Pro. Supports `thinking=True` and `chairman` for synthesis model. |
287
290
  | `pplx_gpt54` / `_thinking` | 1 Pro | OpenAI GPT-5.4 (versatile) |
288
291
  | `pplx_gpt55` / `_thinking` | 1 Pro | OpenAI GPT-5.5 (latest, Max tier) |
289
292
  | `pplx_claude_sonnet` / `_think` | 1 Pro | Anthropic Claude 4.6 Sonnet |
@@ -309,7 +312,7 @@ For full MCP tool parameters: See [references/mcp-tools.md](references/mcp-tools
309
312
  | CLI Name | Provider | Thinking | Notes |
310
313
  |----------|----------|----------|-------|
311
314
  | auto | Perplexity | No | Auto-selects best |
312
- | sonar | Perplexity | No | Sonar 2 (API id `experimental`) |
315
+ | sonar | Perplexity | No | Sonar 2 (API id `experimental`). Uses `mode="concise"` to ensure grounded answers. |
313
316
  | deep_research | Perplexity | No | Monthly quota |
314
317
  | gpt54 | OpenAI | Toggle | GPT-5.4 (versatile) |
315
318
  | gpt55 | OpenAI | Toggle | GPT-5.5 (latest, Max tier) |
@@ -120,7 +120,7 @@ Returns a summary including:
120
120
  - Deep Research remaining (monthly)
121
121
  - Create Files & Apps remaining (monthly)
122
122
  - Browser Agent remaining (monthly)
123
- - Subscription tier and account info
123
+ - Subscription tier, billing detail, and account info
124
124
 
125
125
  ## Authentication Tools
126
126
 
@@ -16,7 +16,7 @@ Complete list of models available through Perplexity Web MCP.
16
16
  - **Thinking:** No
17
17
  - **CLI:** `pwm ask "query" -m sonar`
18
18
  - **MCP:** `pplx_sonar(query)` or `pplx_query(query, model="sonar")`
19
- - **Notes:** Perplexity's latest in-house model.
19
+ - **Notes:** Perplexity's latest in-house model. Settings default to concise search mode (`mode="concise"`), which bypasses interactive copilot to guarantee responses are grounded on retrieved search citations on all accounts (including Free tier fallback).
20
20
 
21
21
  ### deep_research (Deep Research)
22
22
  - **Identifier:** `pplx_alpha`
@@ -15,11 +15,11 @@ from fastmcp import FastMCP
15
15
 
16
16
  from perplexity_web_mcp.models import Models
17
17
  from perplexity_web_mcp.shared import (
18
- COUNCIL_DISPLAY_NAMES,
19
- THINKING_TOGGLEABLE,
18
+ COUNCIL_DEFAULT_MODELS_STR,
20
19
  ModelName,
21
20
  SourceFocusName,
22
21
  ask,
22
+ build_council_model_list,
23
23
  council_ask,
24
24
  get_limit_cache,
25
25
  resolve_model,
@@ -39,6 +39,7 @@ mcp = FastMCP(
39
39
  "- pplx_deep_research: 1 DEEP RESEARCH each (small monthly pool, ~5-10 total)\n\n"
40
40
  "MANDATORY PROTOCOL:\n"
41
41
  "1. On your FIRST query of the session, call pplx_usage() to check remaining quotas.\n"
42
+ " Read the Subscription line: Pro users must avoid Max-only models.\n"
42
43
  "2. DEFAULT to pplx_smart_query(intent='quick') for most lookups — it prefers Sonar 2 "
43
44
  "before premium models when that fits the question.\n"
44
45
  "3. Only use 'standard' or 'detailed' intent when the question requires synthesis, "
@@ -288,7 +289,7 @@ def pplx_smart_query(
288
289
  def pplx_council(
289
290
  query: str,
290
291
  source_focus: SourceFocusName = "web",
291
- models: str = "gpt54,claude_opus,gemini_pro",
292
+ models: str = COUNCIL_DEFAULT_MODELS_STR,
292
293
  synthesize: bool = True,
293
294
  thinking: bool = False,
294
295
  chairman: ModelName = "sonar",
@@ -296,20 +297,22 @@ def pplx_council(
296
297
  """Model Council — query multiple models in parallel, get synthesized consensus.
297
298
 
298
299
  IMPORTANT — BEFORE calling this tool, you MUST:
299
- 1. Tell the user the available models: gpt54, gpt55, claude_sonnet, claude_opus, gemini_pro, nemotron, kimi_k26
300
- 2. Ask the user WHICH models they want in their council and HOW MANY
301
- 3. Inform them of the cost: each council model = 1 Pro Search query, plus synthesis
300
+ 1. Tell the user the available models: sonar, gpt54, gpt55, claude_sonnet, claude_opus, gemini_pro, nemotron, kimi_k26
301
+ 2. Check pplx_usage() first. If Subscription is Pro, do not include Max-only models: gpt55, claude_opus
302
+ 3. Ask the user WHICH models they want in their council and HOW MANY
303
+ 4. Inform them of the cost: each council model = 1 Pro Search query, plus synthesis
302
304
  (default chairman sonar = Sonar 2 pass — still counts as a normal query toward limits)
303
- 4. Get explicit confirmation before executing
305
+ 5. Get explicit confirmation before executing
304
306
 
305
- Default council: GPT-5.4, Claude Opus 4.7, Gemini 3.1 Pro (3 diverse providers).
307
+ Default council: GPT-5.4, Claude Sonnet 4.6, Gemini 3.1 Pro (Pro-compatible, 3 diverse providers).
306
308
 
307
309
  Args:
308
310
  query: The question to ask all council models
309
311
  source_focus: Source type for all models (none/web/academic/social/finance/all)
310
312
  models: Comma-separated model names to use as council members.
311
- Available: gpt54, gpt55, claude_sonnet, claude_opus, gemini_pro, nemotron, kimi_k26.
312
- Default: "gpt54,claude_opus,gemini_pro" (3 models + synthesis = 4 Pro Searches)
313
+ Available: sonar, gpt54, gpt55, claude_sonnet, claude_opus, gemini_pro, nemotron, kimi_k26.
314
+ Default: "gpt54,claude_sonnet,gemini_pro" (3 models + synthesis = 4 Pro Searches)
315
+ Max-only: gpt55, claude_opus. Exclude these when pplx_usage shows a Pro subscription.
313
316
  synthesize: Whether to synthesize a consensus from all responses.
314
317
  Set false to get only individual responses (saves 1 Sonar 2 call).
315
318
  thinking: Enable extended thinking for council models (gpt54, gpt55, claude_sonnet,
@@ -319,15 +322,9 @@ def pplx_council(
319
322
  """
320
323
  # Parse custom model list if provided
321
324
  model_list = None
322
- if models != "gpt54,claude_opus,gemini_pro":
323
- model_list = []
324
- for name in models.split(","):
325
- name = name.strip()
326
- resolved = resolve_model(name, thinking=thinking)
327
- display = COUNCIL_DISPLAY_NAMES.get(name, name)
328
- if thinking and name in THINKING_TOGGLEABLE:
329
- display += " Thinking"
330
- model_list.append((display, resolved))
325
+ if models != COUNCIL_DEFAULT_MODELS_STR:
326
+ model_names = [name.strip() for name in models.split(",") if name.strip()]
327
+ model_list = build_council_model_list(model_names, thinking=thinking)
331
328
 
332
329
  synthesis_model = resolve_model(chairman) if chairman != "sonar" else None
333
330
 
@@ -376,12 +373,26 @@ def pplx_usage(refresh: bool = False) -> str:
376
373
  else:
377
374
  parts.append("WARNING: Could not fetch rate limits (network error or token issue).")
378
375
 
376
+ from perplexity_web_mcp.cli.auth import get_user_info
377
+
378
+ user_info = get_user_info(token)
379
379
  settings = cache.get_user_settings(force_refresh=refresh)
380
- if settings:
380
+ if settings or user_info:
381
381
  parts.append("")
382
382
  parts.append("ACCOUNT INFO")
383
383
  parts.append("=" * 40)
384
- parts.append(settings.format_summary())
384
+ if user_info:
385
+ parts.append(f"Subscription: {user_info.tier_display}")
386
+ if settings:
387
+ parts.append(f"Billing: {settings.subscription_tier} ({settings.subscription_status})")
388
+ parts.append(f"Total queries: {settings.query_count:,}")
389
+ parts.append(f"Pro queries: {settings.query_count_copilot:,}")
390
+ parts.append(f"Upload limit: {settings.upload_limit} files")
391
+ parts.append(f"Create limit: {settings.create_limit}")
392
+ parts.append(f"Pages limit: {settings.pages_limit}")
393
+ parts.append(f"Max files/user: {settings.max_files_per_user:,}")
394
+ parts.append(f"Max file size: {settings.connector_limits.max_file_size_mb} MB")
395
+ parts.append(f"Daily attachments: {settings.connector_limits.daily_attachment_limit}")
385
396
 
386
397
  credits = cache.get_credits(force_refresh=refresh)
387
398
  if credits:
@@ -25,7 +25,7 @@ class Models:
25
25
  BEST = Model(identifier="pplx_pro")
26
26
  """Best - Automatically selects the best model based on the query."""
27
27
 
28
- SONAR = Model(identifier="experimental")
28
+ SONAR = Model(identifier="experimental", mode="concise")
29
29
  """Sonar 2 — Perplexity's latest in-house model (backend id: experimental)."""
30
30
 
31
31
  GEMINI_31_PRO_THINKING = Model(identifier="gemini31pro_high")
@@ -7,6 +7,7 @@ Both the MCP server (mcp/server.py) and CLI (cli/main.py) import from here.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from dataclasses import dataclass
10
11
  from threading import Lock
11
12
  from typing import TYPE_CHECKING, Literal
12
13
  from uuid import uuid4
@@ -22,6 +23,7 @@ from .token_store import get_token_or_raise, load_token
22
23
 
23
24
 
24
25
  if TYPE_CHECKING:
26
+ from .council import CouncilResponse
25
27
  from .types import SearchResultItem
26
28
 
27
29
 
@@ -29,6 +31,20 @@ if TYPE_CHECKING:
29
31
  # Model and source focus mappings (single source of truth)
30
32
  # ---------------------------------------------------------------------------
31
33
 
34
+ SubscriptionMinimumTier = Literal["free", "pro", "max"]
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class ModelDefinition:
39
+ """Metadata and model instances for one user-facing model key."""
40
+
41
+ base_model: Model
42
+ thinking_model: Model | None
43
+ display_name: str
44
+ provider: str
45
+ minimum_tier: SubscriptionMinimumTier = "pro"
46
+ council_eligible: bool = True
47
+
32
48
  SOURCE_FOCUS_MAP: dict[str, list[SourceFocus]] = {
33
49
  "none": [],
34
50
  "web": [SourceFocus.WEB],
@@ -38,18 +54,49 @@ SOURCE_FOCUS_MAP: dict[str, list[SourceFocus]] = {
38
54
  "all": [SourceFocus.WEB, SourceFocus.ACADEMIC, SourceFocus.SOCIAL],
39
55
  }
40
56
 
57
+ MODEL_METADATA: dict[str, ModelDefinition] = {
58
+ "auto": ModelDefinition(Models.BEST, None, "Auto (Best)", "Perplexity", council_eligible=False),
59
+ "sonar": ModelDefinition(Models.SONAR, None, "Sonar 2", "Perplexity"),
60
+ "deep_research": ModelDefinition(
61
+ Models.DEEP_RESEARCH,
62
+ None,
63
+ "Deep Research",
64
+ "Perplexity",
65
+ council_eligible=False,
66
+ ),
67
+ "gpt54": ModelDefinition(Models.GPT_54, Models.GPT_54_THINKING, "GPT-5.4", "OpenAI"),
68
+ "gpt55": ModelDefinition(Models.GPT_55, Models.GPT_55_THINKING, "GPT-5.5", "OpenAI", minimum_tier="max"),
69
+ "claude_sonnet": ModelDefinition(
70
+ Models.CLAUDE_46_SONNET,
71
+ Models.CLAUDE_46_SONNET_THINKING,
72
+ "Claude Sonnet 4.6",
73
+ "Anthropic",
74
+ ),
75
+ "claude_opus": ModelDefinition(
76
+ Models.CLAUDE_47_OPUS,
77
+ Models.CLAUDE_47_OPUS_THINKING,
78
+ "Claude Opus 4.7",
79
+ "Anthropic",
80
+ minimum_tier="max",
81
+ ),
82
+ "gemini_pro": ModelDefinition(
83
+ Models.GEMINI_31_PRO_THINKING,
84
+ Models.GEMINI_31_PRO_THINKING,
85
+ "Gemini 3.1 Pro",
86
+ "Google",
87
+ ),
88
+ "nemotron": ModelDefinition(
89
+ Models.NEMOTRON_3_SUPER,
90
+ Models.NEMOTRON_3_SUPER,
91
+ "Nemotron 3 Super",
92
+ "NVIDIA",
93
+ ),
94
+ "kimi_k26": ModelDefinition(Models.KIMI_K2_6, Models.KIMI_K2_6_THINKING, "Kimi K2.6", "Moonshot"),
95
+ }
96
+ """User-facing model metadata. Update this table when model names or tier availability changes."""
97
+
41
98
  MODEL_MAP: dict[str, tuple[Model, Model | None]] = {
42
- # (base_model, thinking_model) - None if no thinking variant
43
- "auto": (Models.BEST, None),
44
- "sonar": (Models.SONAR, None),
45
- "deep_research": (Models.DEEP_RESEARCH, None),
46
- "gpt54": (Models.GPT_54, Models.GPT_54_THINKING),
47
- "gpt55": (Models.GPT_55, Models.GPT_55_THINKING),
48
- "claude_sonnet": (Models.CLAUDE_46_SONNET, Models.CLAUDE_46_SONNET_THINKING),
49
- "claude_opus": (Models.CLAUDE_47_OPUS, Models.CLAUDE_47_OPUS_THINKING),
50
- "gemini_pro": (Models.GEMINI_31_PRO_THINKING, Models.GEMINI_31_PRO_THINKING),
51
- "nemotron": (Models.NEMOTRON_3_SUPER, Models.NEMOTRON_3_SUPER),
52
- "kimi_k26": (Models.KIMI_K2_6, Models.KIMI_K2_6_THINKING),
99
+ name: (definition.base_model, definition.thinking_model) for name, definition in MODEL_METADATA.items()
53
100
  }
54
101
 
55
102
  SourceFocusName = Literal["none", "web", "academic", "social", "finance", "all"]
@@ -70,21 +117,39 @@ MODEL_NAMES: list[str] = list(MODEL_MAP.keys())
70
117
  SOURCE_FOCUS_NAMES: list[str] = list(SOURCE_FOCUS_MAP.keys())
71
118
 
72
119
  COUNCIL_DISPLAY_NAMES: dict[str, str] = {
73
- "auto": "Auto (Best)",
74
- "sonar": "Sonar 2",
75
- "gpt54": "GPT-5.4",
76
- "gpt55": "GPT-5.5",
77
- "claude_sonnet": "Claude Sonnet 4.6",
78
- "claude_opus": "Claude Opus 4.7",
79
- "gemini_pro": "Gemini 3.1 Pro",
80
- "nemotron": "Nemotron 3 Super",
81
- "kimi_k26": "Kimi K2.6",
120
+ name: definition.display_name for name, definition in MODEL_METADATA.items()
82
121
  }
83
122
 
84
123
  THINKING_TOGGLEABLE: frozenset[str] = frozenset(
85
124
  name for name, (base, thinking) in MODEL_MAP.items() if thinking is not None and thinking is not base
86
125
  )
87
126
 
127
+ MAX_ONLY_MODEL_NAMES: frozenset[str] = frozenset(
128
+ name for name, definition in MODEL_METADATA.items() if definition.minimum_tier == "max"
129
+ )
130
+
131
+ COUNCIL_ELIGIBLE_MODEL_NAMES: tuple[str, ...] = tuple(
132
+ name for name, definition in MODEL_METADATA.items() if definition.council_eligible
133
+ )
134
+
135
+ COUNCIL_DEFAULT_MODEL_NAMES: tuple[str, ...] = ("gpt54", "claude_sonnet", "gemini_pro")
136
+ COUNCIL_DEFAULT_MODELS_STR = ",".join(COUNCIL_DEFAULT_MODEL_NAMES)
137
+
138
+
139
+ def build_council_model_list(
140
+ model_names: tuple[str, ...] | list[str],
141
+ thinking: bool = False,
142
+ ) -> list[tuple[str, Model]]:
143
+ """Build display/model pairs for council execution from model metadata."""
144
+ model_list: list[tuple[str, Model]] = []
145
+ for name in model_names:
146
+ resolved = resolve_model(name, thinking=thinking)
147
+ display = COUNCIL_DISPLAY_NAMES.get(name, name)
148
+ if thinking and name in THINKING_TOGGLEABLE:
149
+ display += " Thinking"
150
+ model_list.append((display, resolved))
151
+ return model_list
152
+
88
153
 
89
154
  def resolve_model(name: str, thinking: bool = False) -> Model:
90
155
  """Resolve a model name string to a Model instance.