@oh-my-pi/pi-coding-agent 12.10.1 → 12.11.0

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.
@@ -2,39 +2,16 @@
2
2
  * Model resolution, scoping, and initial selection
3
3
  */
4
4
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
5
- import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
5
+ import { type Api, DEFAULT_MODEL_PER_PROVIDER, type KnownProvider, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
6
6
  import chalk from "chalk";
7
7
  import { isValidThinkingLevel } from "../cli/args";
8
+ import MODEL_PRIO from "../priority.json" with { type: "json" };
8
9
  import { fuzzyMatch } from "../utils/fuzzy";
9
10
  import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
10
11
  import type { Settings } from "./settings";
11
12
 
12
13
  /** Default model IDs for each known provider */
13
- export const defaultModelPerProvider: Record<KnownProvider, string> = {
14
- "amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
15
- anthropic: "claude-sonnet-4-6",
16
- openai: "gpt-5.1-codex",
17
- "openai-codex": "gpt-5.3-codex",
18
- google: "gemini-2.5-pro",
19
- "google-gemini-cli": "gemini-2.5-pro",
20
- "google-antigravity": "gemini-3-pro-high",
21
- "google-vertex": "gemini-3-pro-preview",
22
- "github-copilot": "gpt-4o",
23
- cursor: "claude-sonnet-4-6",
24
- openrouter: "openai/gpt-5.1-codex",
25
- "vercel-ai-gateway": "anthropic/claude-sonnet-4-6",
26
- xai: "grok-4-fast-non-reasoning",
27
- groq: "openai/gpt-oss-120b",
28
- cerebras: "zai-glm-4.6",
29
- zai: "glm-4.6",
30
- mistral: "devstral-medium-latest",
31
- minimax: "MiniMax-M2.5",
32
- "minimax-code": "MiniMax-M2.5",
33
- "minimax-code-cn": "MiniMax-M2.5",
34
- opencode: "claude-sonnet-4-6",
35
- "kimi-code": "kimi-k2.5",
36
- synthetic: "hf:moonshotai/Kimi-K2.5",
37
- };
14
+ export const defaultModelPerProvider: Record<KnownProvider, string> = DEFAULT_MODEL_PER_PROVIDER;
38
15
 
39
16
  export interface ScopedModel {
40
17
  model: Model<Api>;
@@ -42,47 +19,6 @@ export interface ScopedModel {
42
19
  explicitThinkingLevel: boolean;
43
20
  }
44
21
 
45
- /** Priority chain for auto-discovering smol/fast models */
46
- export const SMOL_MODEL_PRIORITY = [
47
- // any spark
48
- "gpt-5.3-codex-spark",
49
- "gpt-5.3-spark",
50
- "spark",
51
- // cerebras zai
52
- "cerebras/zai-glm-4.7",
53
- "cerebras/zai-glm-4.6",
54
- "cerebras/zai-glm",
55
- // any haiku
56
- "haiku-4-5",
57
- "haiku-4.5",
58
- "haiku",
59
- // any flash
60
- "flash",
61
- // any mini
62
- "mini",
63
- ];
64
-
65
- /** Priority chain for auto-discovering slow/comprehensive models (reasoning, codex) */
66
- export const SLOW_MODEL_PRIORITY = [
67
- // any codex
68
- "gpt-5.3-codex",
69
- "gpt-5.3",
70
- "gpt-5.2-codex",
71
- "gpt-5.2",
72
- "gpt-5.1-codex",
73
- "gpt-5.1",
74
- "codex",
75
- // any opus
76
- "opus-4.6",
77
- "opus-4-6",
78
- "opus-4.5",
79
- "opus-4-5",
80
- "opus-4.1",
81
- "opus-4-1",
82
- // whatever
83
- "pro",
84
- ];
85
-
86
22
  /**
87
23
  * Parse a model string in "provider/modelId" format.
88
24
  * Returns undefined if the format is invalid.
@@ -812,7 +748,7 @@ export async function findSmolModel(
812
748
  }
813
749
 
814
750
  // 2. Try priority chain
815
- for (const pattern of SMOL_MODEL_PRIORITY) {
751
+ for (const pattern of MODEL_PRIO.smol) {
816
752
  // Try exact match with provider prefix
817
753
  const providerMatch = availableModels.find(m => `${m.provider}/${m.id}`.toLowerCase() === pattern);
818
754
  if (providerMatch) return providerMatch;
@@ -855,7 +791,7 @@ export async function findSlowModel(
855
791
  }
856
792
 
857
793
  // 2. Try priority chain
858
- for (const pattern of SLOW_MODEL_PRIORITY) {
794
+ for (const pattern of MODEL_PRIO.slow) {
859
795
  // Try exact match first
860
796
  const exactMatch = availableModels.find(m => m.id.toLowerCase() === pattern.toLowerCase());
861
797
  if (exactMatch) return exactMatch;
@@ -244,6 +244,16 @@ export const SETTINGS_SCHEMA = {
244
244
  default: false,
245
245
  ui: { tab: "input", label: "Collapse changelog", description: "Show condensed changelog after updates" },
246
246
  },
247
+ autocompleteMaxVisible: {
248
+ type: "number",
249
+ default: 5,
250
+ ui: {
251
+ tab: "input",
252
+ label: "Autocomplete max items",
253
+ description: "Max visible items in autocomplete dropdown (3-20)",
254
+ submenu: true,
255
+ },
256
+ },
247
257
  normativeRewrite: {
248
258
  type: "boolean",
249
259
  default: false,
@@ -223,6 +223,16 @@ export class ModelSelectorComponent extends Container {
223
223
  const providerCmp = a.provider.localeCompare(b.provider);
224
224
  if (providerCmp !== 0) return providerCmp;
225
225
 
226
+ // Priority field (lower = better, e.g. Codex priority values)
227
+ const aPri = a.model.priority ?? Number.MAX_SAFE_INTEGER;
228
+ const bPri = b.model.priority ?? Number.MAX_SAFE_INTEGER;
229
+ if (aPri !== bPri) return aPri - bPri;
230
+
231
+ // Version number descending (higher version = better model)
232
+ const aVer = extractVersionNumber(a.id);
233
+ const bVer = extractVersionNumber(b.id);
234
+ if (aVer !== bVer) return bVer - aVer;
235
+
226
236
  const aIsLatest = latestRe.test(a.id);
227
237
  const bIsLatest = latestRe.test(b.id);
228
238
  const aDate = a.id.match(dateRe)?.[1] ?? "";
@@ -596,3 +606,17 @@ export class ModelSelectorComponent extends Container {
596
606
  return this.#searchInput;
597
607
  }
598
608
  }
609
+
610
+ /** Extract the first version number from a model ID (e.g. "gemini-2.5-pro" → 2.5, "claude-sonnet-4-6" → 4.6). */
611
+ function extractVersionNumber(id: string): number {
612
+ // Dot-separated version: "gemini-2.5-pro" → 2.5
613
+ const dotMatch = id.match(/(?:^|[-_])(\d+\.\d+)/);
614
+ if (dotMatch) return Number.parseFloat(dotMatch[1]);
615
+ // Dash-separated short segments: "claude-sonnet-4-6" → 4.6, "llama-3-1-8b" → 3.1
616
+ const dashMatch = id.match(/(?:^|[-_])(\d{1,2})-(\d{1,2})(?=-|$)/);
617
+ if (dashMatch) return Number.parseFloat(`${dashMatch[1]}.${dashMatch[2]}`);
618
+ // Single number after separator: "gpt-4o" → 4
619
+ const singleMatch = id.match(/(?:^|[-_])(\d+)/);
620
+ if (singleMatch) return Number.parseFloat(singleMatch[1]);
621
+ return 0;
622
+ }
@@ -116,6 +116,15 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
116
116
  { value: "5", label: "5 lines" },
117
117
  { value: "10", label: "10 lines" },
118
118
  ],
119
+ // Autocomplete max visible
120
+ autocompleteMaxVisible: [
121
+ { value: "3", label: "3 items" },
122
+ { value: "5", label: "5 items" },
123
+ { value: "7", label: "7 items" },
124
+ { value: "10", label: "10 items" },
125
+ { value: "15", label: "15 items" },
126
+ { value: "20", label: "20 items" },
127
+ ],
119
128
  // Ask timeout
120
129
  "ask.timeout": [
121
130
  { value: "0", label: "Disabled" },
@@ -155,7 +164,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
155
164
  {
156
165
  value: "auto",
157
166
  label: "Auto",
158
- description: "Priority: Exa > Brave > Jina > Perplexity > Anthropic > Gemini > Codex > Z.AI",
167
+ description: "Priority: Exa > Brave > Jina > Perplexity > Anthropic > Gemini > Codex > Z.AI > Synthetic",
159
168
  },
160
169
  { value: "exa", label: "Exa", description: "Requires EXA_API_KEY" },
161
170
  { value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
@@ -163,6 +172,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
163
172
  { value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_API_KEY" },
164
173
  { value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
165
174
  { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
175
+ { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
166
176
  ],
167
177
  "providers.image": [
168
178
  { value: "auto", label: "Auto", description: "Priority: OpenRouter > Gemini" },
@@ -201,6 +201,10 @@ export class SelectorController {
201
201
  this.ctx.ui.setClearOnShrink(value as boolean);
202
202
  break;
203
203
 
204
+ case "autocompleteMaxVisible":
205
+ this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value));
206
+ break;
207
+
204
208
  // Settings with UI side effects
205
209
  case "showImages":
206
210
  for (const child of this.ctx.chatContainer.children) {
@@ -189,6 +189,7 @@ export class InteractiveMode implements InteractiveModeContext {
189
189
  this.todoContainer = new Container();
190
190
  this.editor = new CustomEditor(getEditorTheme());
191
191
  this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
192
+ this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
192
193
  this.editor.onAutocompleteCancel = () => {
193
194
  this.ui.requestRender(true);
194
195
  };
@@ -0,0 +1,28 @@
1
+ {
2
+ "smol": [
3
+ "cerebras/zai-glm-4.7",
4
+ "cerebras/zai-glm-4.6",
5
+ "cerebras/zai-glm",
6
+ "haiku-4-5",
7
+ "haiku-4.5",
8
+ "haiku",
9
+ "flash",
10
+ "mini"
11
+ ],
12
+ "slow": [
13
+ "gpt-5.3-codex",
14
+ "gpt-5.3",
15
+ "gpt-5.2-codex",
16
+ "gpt-5.2",
17
+ "gpt-5.1-codex",
18
+ "gpt-5.1",
19
+ "codex",
20
+ "opus-4.6",
21
+ "opus-4-6",
22
+ "opus-4.5",
23
+ "opus-4-5",
24
+ "opus-4.1",
25
+ "opus-4-1",
26
+ "pro"
27
+ ]
28
+ }
@@ -2,7 +2,7 @@
2
2
  name: explore
3
3
  description: Fast read-only codebase scout returning compressed context for handoff
4
4
  tools: read, grep, find, bash
5
- model: pi/smol, haiku-4.5, haiku-4-5, gemini-flash-latest, gemini-3-flash, zai-glm-4.7, glm-4.7-flash, glm-4.5-flash, gpt-5.1-codex-mini, haiku, flash, mini
5
+ model: pi/smol
6
6
  thinking-level: minimal
7
7
  output:
8
8
  properties:
@@ -3,7 +3,7 @@ name: plan
3
3
  description: Software architect for complex multi-file architectural decisions. NOT for simple tasks, single-file changes, or tasks completable in <5 tool calls.
4
4
  tools: read, grep, find, bash
5
5
  spawns: explore
6
- model: pi/plan, pi/slow, gpt-5.2-codex, gpt-5.2, codex, gpt, opus-4.5, opus-4-5, gemini-3-pro
6
+ model: pi/plan, pi/slow
7
7
  thinking-level: high
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ name: reviewer
3
3
  description: "Code review specialist for quality/security analysis"
4
4
  tools: read, grep, find, bash, report_finding
5
5
  spawns: explore, task
6
- model: pi/slow, gpt-5.2-codex, gpt-5.2, codex, gpt
6
+ model: pi/slow
7
7
  thinking-level: high
8
8
  output:
9
9
  properties:
@@ -16,7 +16,15 @@
16
16
  import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
18
 
19
- import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
19
+ import {
20
+ type Agent,
21
+ AgentBusyError,
22
+ type AgentEvent,
23
+ type AgentMessage,
24
+ type AgentState,
25
+ type AgentTool,
26
+ type ThinkingLevel,
27
+ } from "@oh-my-pi/pi-agent-core";
20
28
  import type {
21
29
  AssistantMessage,
22
30
  ImageContent,
@@ -304,6 +312,7 @@ export class AgentSession {
304
312
 
305
313
  // Handoff state
306
314
  #handoffAbortController: AbortController | undefined = undefined;
315
+ #skipPostTurnMaintenanceAssistantTimestamp: number | undefined = undefined;
307
316
 
308
317
  // Retry state
309
318
  #retryAbortController: AbortController | undefined = undefined;
@@ -579,6 +588,9 @@ export class AgentSession {
579
588
  this.#lastAssistantMessage = event.message;
580
589
  const assistantMsg = event.message as AssistantMessage;
581
590
  this.#queueDeferredTtsrInjectionIfNeeded(assistantMsg);
591
+ if (this.#handoffAbortController) {
592
+ this.#skipPostTurnMaintenanceAssistantTimestamp = assistantMsg.timestamp;
593
+ }
582
594
  if (
583
595
  assistantMsg.stopReason !== "error" &&
584
596
  assistantMsg.stopReason !== "aborted" &&
@@ -637,6 +649,11 @@ export class AgentSession {
637
649
  const msg = this.#lastAssistantMessage;
638
650
  this.#lastAssistantMessage = undefined;
639
651
 
652
+ if (this.#skipPostTurnMaintenanceAssistantTimestamp === msg.timestamp) {
653
+ this.#skipPostTurnMaintenanceAssistantTimestamp = undefined;
654
+ return;
655
+ }
656
+
640
657
  // Check for retryable errors first (overloaded, rate limit, server errors)
641
658
  if (this.#isRetryableError(msg)) {
642
659
  const didRetry = await this.#handleRetryableError(msg);
@@ -1607,9 +1624,7 @@ export class AgentSession {
1607
1624
  // If streaming, queue via steer() or followUp() based on option
1608
1625
  if (this.isStreaming) {
1609
1626
  if (!options?.streamingBehavior) {
1610
- throw new Error(
1611
- "Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.",
1612
- );
1627
+ throw new AgentBusyError();
1613
1628
  }
1614
1629
  if (options.streamingBehavior === "followUp") {
1615
1630
  await this.#queueFollowUp(expandedText, options?.images);
@@ -1650,9 +1665,7 @@ export class AgentSession {
1650
1665
 
1651
1666
  if (this.isStreaming) {
1652
1667
  if (!options?.streamingBehavior) {
1653
- throw new Error(
1654
- "Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.",
1655
- );
1668
+ throw new AgentBusyError();
1656
1669
  }
1657
1670
  await this.sendCustomMessage(message, { deliverAs: options.streamingBehavior });
1658
1671
  return;
@@ -1777,7 +1790,7 @@ export class AgentSession {
1777
1790
  }
1778
1791
 
1779
1792
  const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
1780
- await this.agent.prompt(messages, agentPromptOptions);
1793
+ await this.#promptAgentWithIdleRetry(messages, agentPromptOptions);
1781
1794
  await this.#waitForRetry();
1782
1795
  } finally {
1783
1796
  this.#promptInFlight = false;
@@ -2800,6 +2813,8 @@ export class AgentSession {
2800
2813
  throw new Error("Nothing to hand off (no messages yet)");
2801
2814
  }
2802
2815
 
2816
+ this.#skipPostTurnMaintenanceAssistantTimestamp = undefined;
2817
+
2803
2818
  this.#handoffAbortController = new AbortController();
2804
2819
 
2805
2820
  // Build the handoff prompt
@@ -3693,6 +3708,24 @@ Be thorough - include exact file paths, function names, error messages, and tech
3693
3708
  }
3694
3709
  }
3695
3710
 
3711
+ async #promptAgentWithIdleRetry(messages: AgentMessage[], options?: { toolChoice?: ToolChoice }): Promise<void> {
3712
+ const deadline = Date.now() + 30_000;
3713
+ for (;;) {
3714
+ try {
3715
+ await this.agent.prompt(messages, options);
3716
+ return;
3717
+ } catch (err) {
3718
+ if (!(err instanceof AgentBusyError)) {
3719
+ throw err;
3720
+ }
3721
+ if (Date.now() >= deadline) {
3722
+ throw new Error("Timed out waiting for prior agent run to finish before prompting.");
3723
+ }
3724
+ await this.agent.waitForIdle();
3725
+ }
3726
+ }
3727
+ }
3728
+
3696
3729
  /** Whether auto-retry is currently in progress */
3697
3730
  get isRetrying(): boolean {
3698
3731
  return this.#retryPromise !== undefined;
@@ -14,16 +14,28 @@ import {
14
14
  loginAnthropic,
15
15
  loginAntigravity,
16
16
  loginCerebras,
17
+ loginCloudflareAiGateway,
17
18
  loginCursor,
18
19
  loginGeminiCli,
19
20
  loginGitHubCopilot,
21
+ loginHuggingface,
20
22
  loginKimi,
23
+ loginLiteLLM,
21
24
  loginMiniMaxCode,
22
25
  loginMiniMaxCodeCn,
26
+ loginMoonshot,
27
+ loginNvidia,
28
+ loginOllama,
23
29
  loginOpenAICodex,
24
30
  loginOpenCode,
25
31
  loginPerplexity,
32
+ loginQianfan,
33
+ loginQwenPortal,
26
34
  loginSynthetic,
35
+ loginTogether,
36
+ loginVenice,
37
+ loginVllm,
38
+ loginXiaomi,
27
39
  loginZai,
28
40
  type OAuthController,
29
41
  type OAuthCredentials,
@@ -693,11 +705,24 @@ export class AuthStorage {
693
705
  case "perplexity":
694
706
  credentials = await loginPerplexity(ctrl);
695
707
  break;
708
+ case "huggingface": {
709
+ const apiKey = await loginHuggingface(ctrl);
710
+ await saveApiKeyCredential(apiKey);
711
+ return;
712
+ }
696
713
  case "opencode": {
697
714
  const apiKey = await loginOpenCode(ctrl);
698
715
  await saveApiKeyCredential(apiKey);
699
716
  return;
700
717
  }
718
+ case "ollama": {
719
+ const apiKey = await loginOllama(ctrl);
720
+ if (!apiKey) {
721
+ return;
722
+ }
723
+ await saveApiKeyCredential(apiKey);
724
+ return;
725
+ }
701
726
  case "cerebras": {
702
727
  const apiKey = await loginCerebras(ctrl);
703
728
  await saveApiKeyCredential(apiKey);
@@ -708,6 +733,11 @@ export class AuthStorage {
708
733
  await saveApiKeyCredential(apiKey);
709
734
  return;
710
735
  }
736
+ case "qianfan": {
737
+ const apiKey = await loginQianfan(ctrl);
738
+ await saveApiKeyCredential(apiKey);
739
+ return;
740
+ }
711
741
  case "minimax-code": {
712
742
  const apiKey = await loginMiniMaxCode(ctrl);
713
743
  await saveApiKeyCredential(apiKey);
@@ -723,6 +753,51 @@ export class AuthStorage {
723
753
  await saveApiKeyCredential(apiKey);
724
754
  return;
725
755
  }
756
+ case "venice": {
757
+ const apiKey = await loginVenice(ctrl);
758
+ await saveApiKeyCredential(apiKey);
759
+ return;
760
+ }
761
+ case "litellm": {
762
+ const apiKey = await loginLiteLLM(ctrl);
763
+ await saveApiKeyCredential(apiKey);
764
+ return;
765
+ }
766
+ case "moonshot": {
767
+ const apiKey = await loginMoonshot(ctrl);
768
+ await saveApiKeyCredential(apiKey);
769
+ return;
770
+ }
771
+ case "together": {
772
+ const apiKey = await loginTogether(ctrl);
773
+ await saveApiKeyCredential(apiKey);
774
+ return;
775
+ }
776
+ case "cloudflare-ai-gateway": {
777
+ const apiKey = await loginCloudflareAiGateway(ctrl);
778
+ await saveApiKeyCredential(apiKey);
779
+ return;
780
+ }
781
+ case "vllm": {
782
+ const apiKey = await loginVllm(ctrl);
783
+ await saveApiKeyCredential(apiKey);
784
+ return;
785
+ }
786
+ case "qwen-portal": {
787
+ const apiKey = await loginQwenPortal(ctrl);
788
+ await saveApiKeyCredential(apiKey);
789
+ return;
790
+ }
791
+ case "nvidia": {
792
+ const apiKey = await loginNvidia(ctrl);
793
+ await saveApiKeyCredential(apiKey);
794
+ return;
795
+ }
796
+ case "xiaomi": {
797
+ const apiKey = await loginXiaomi(ctrl);
798
+ await saveApiKeyCredential(apiKey);
799
+ return;
800
+ }
726
801
  default: {
727
802
  const customProvider = getOAuthProvider(provider);
728
803
  if (!customProvider) {
@@ -4,10 +4,18 @@ import { Readability } from "@mozilla/readability";
4
4
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
5
  import { StringEnum } from "@oh-my-pi/pi-ai";
6
6
  import { logger, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
7
+ import { getPuppeteerDir } from "@oh-my-pi/pi-utils/dirs";
7
8
  import { type Static, Type } from "@sinclair/typebox";
8
9
  import { type HTMLElement, parseHTML } from "linkedom";
9
- import type { Browser, CDPSession, ElementHandle, KeyInput, Page, SerializedAXNode } from "puppeteer";
10
- import puppeteer from "puppeteer";
10
+ import type {
11
+ Browser,
12
+ CDPSession,
13
+ ElementHandle,
14
+ KeyInput,
15
+ Page,
16
+ default as Puppeteer,
17
+ SerializedAXNode,
18
+ } from "puppeteer";
11
19
  import { renderPromptTemplate } from "../config/prompt-templates";
12
20
  import browserDescription from "../prompts/tools/browser.md" with { type: "text" };
13
21
  import type { ToolSession } from "../sdk";
@@ -31,6 +39,25 @@ import stealthWorkerScript from "./puppeteer/13_stealth_worker.txt" with { type:
31
39
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
32
40
  import { toolResult } from "./tool-result";
33
41
 
42
+ /**
43
+ * Lazy-import puppeteer from a safe CWD so cosmiconfig doesn't choke
44
+ * on malformed package.json files in the user's project tree.
45
+ */
46
+ let puppeteerModule: typeof Puppeteer | undefined;
47
+ async function loadPuppeteer(): Promise<typeof Puppeteer> {
48
+ if (puppeteerModule) return puppeteerModule;
49
+ const prev = process.cwd();
50
+ const safeDir = getPuppeteerDir();
51
+ await Bun.write(path.join(safeDir, "package.json"), "{}");
52
+ try {
53
+ process.chdir(safeDir);
54
+ puppeteerModule = (await import("puppeteer")).default;
55
+ return puppeteerModule;
56
+ } finally {
57
+ process.chdir(prev);
58
+ }
59
+ }
60
+
34
61
  const DEFAULT_TIMEOUT_SECONDS = 30;
35
62
  const MAX_TIMEOUT_SECONDS = 120;
36
63
  const DEFAULT_VIEWPORT = { width: 1365, height: 768, deviceScaleFactor: 1.25 };
@@ -488,6 +515,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
488
515
  await this.#closeBrowser();
489
516
  this.#currentHeadless = this.session.settings.get("browser.headless");
490
517
  const initialViewport = params?.viewport ?? DEFAULT_VIEWPORT;
518
+ const puppeteer = await loadPuppeteer();
491
519
  this.#browser = await puppeteer.launch({
492
520
  headless: this.#currentHeadless,
493
521
  defaultViewport: this.#currentHeadless ? initialViewport : null,
@@ -5,8 +5,9 @@ import type { Api, Model } from "@oh-my-pi/pi-ai";
5
5
  import { completeSimple } from "@oh-my-pi/pi-ai";
6
6
  import { logger } from "@oh-my-pi/pi-utils";
7
7
  import type { ModelRegistry } from "../config/model-registry";
8
- import { parseModelString, SMOL_MODEL_PRIORITY } from "../config/model-resolver";
8
+ import { parseModelString } from "../config/model-resolver";
9
9
  import { renderPromptTemplate } from "../config/prompt-templates";
10
+ import MODEL_PRIO from "../priority.json" with { type: "json" };
10
11
  import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
11
12
 
12
13
  const TITLE_SYSTEM_PROMPT = renderPromptTemplate(titleSystemPrompt);
@@ -34,7 +35,7 @@ function getTitleModelCandidates(registry: ModelRegistry, savedSmolModel?: strin
34
35
  }
35
36
  }
36
37
 
37
- for (const pattern of SMOL_MODEL_PRIORITY) {
38
+ for (const pattern of MODEL_PRIO.smol) {
38
39
  const needle = pattern.toLowerCase();
39
40
  const exactMatch = availableModels.find(model => model.id.toLowerCase() === needle);
40
41
  addCandidate(exactMatch);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Unified Web Search Tool
3
3
  *
4
- * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Gemini, Codex, and Z.AI
4
+ * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Gemini, Codex, Z.AI, and Synthetic
5
5
  * providers with provider-specific parameters exposed conditionally.
6
6
  *
7
7
  * When EXA_API_KEY is available, additional specialized tools are exposed:
@@ -33,7 +33,7 @@ import { SearchProviderError } from "./types";
33
33
  export const webSearchSchema = Type.Object({
34
34
  query: Type.String({ description: "Search query" }),
35
35
  provider: Type.Optional(
36
- StringEnum(["auto", "exa", "brave", "jina", "zai", "anthropic", "perplexity", "gemini", "codex"], {
36
+ StringEnum(["auto", "exa", "brave", "jina", "zai", "anthropic", "perplexity", "gemini", "codex", "synthetic"], {
37
37
  description: "Search provider (default: auto)",
38
38
  }),
39
39
  ),
@@ -47,7 +47,7 @@ export const webSearchSchema = Type.Object({
47
47
 
48
48
  export type SearchParams = {
49
49
  query: string;
50
- provider?: "auto" | "exa" | "brave" | "jina" | "zai" | "anthropic" | "perplexity" | "gemini" | "codex";
50
+ provider?: "auto" | "exa" | "brave" | "jina" | "zai" | "anthropic" | "perplexity" | "gemini" | "codex" | "synthetic";
51
51
  recency?: "day" | "week" | "month" | "year";
52
52
  limit?: number;
53
53
  /** Maximum output tokens. Defaults to 4096. */
@@ -236,7 +236,7 @@ export async function runSearchQuery(
236
236
  /**
237
237
  * Web search tool implementation.
238
238
  *
239
- * Supports Anthropic, Perplexity, Exa, Brave, Jina, Gemini, Codex, and Z.AI providers with automatic fallback.
239
+ * Supports Anthropic, Perplexity, Exa, Brave, Jina, Gemini, Codex, Z.AI, and Synthetic providers with automatic fallback.
240
240
  * Session is accepted for interface consistency but not used.
241
241
  */
242
242
  export class SearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
@@ -6,6 +6,7 @@ import { ExaProvider } from "./providers/exa";
6
6
  import { GeminiProvider } from "./providers/gemini";
7
7
  import { JinaProvider } from "./providers/jina";
8
8
  import { PerplexityProvider } from "./providers/perplexity";
9
+ import { SyntheticProvider } from "./providers/synthetic";
9
10
  import { ZaiProvider } from "./providers/zai";
10
11
  import type { SearchProviderId } from "./types";
11
12
 
@@ -21,6 +22,7 @@ const SEARCH_PROVIDERS: Record<SearchProviderId, SearchProvider> = {
21
22
  anthropic: new AnthropicProvider(),
22
23
  gemini: new GeminiProvider(),
23
24
  codex: new CodexProvider(),
25
+ synthetic: new SyntheticProvider(),
24
26
  } as const;
25
27
 
26
28
  const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
@@ -32,6 +34,7 @@ const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
32
34
  "gemini",
33
35
  "codex",
34
36
  "zai",
37
+ "synthetic",
35
38
  ];
36
39
 
37
40
  export function getSearchProvider(provider: SearchProviderId): SearchProvider {
@@ -46,7 +49,7 @@ export function setPreferredSearchProvider(provider: SearchProviderId | "auto"):
46
49
  preferredProvId = provider;
47
50
  }
48
51
 
49
- /** Determine which providers are configured (priority: Exa → Brave → Jina → Perplexity → Anthropic → Gemini → Codex → Z.AI) */
52
+ /** Determine which providers are configured (priority: Exa → Brave → Jina → Perplexity → Anthropic → Gemini → Codex → Z.AI → Synthetic) */
50
53
  export async function resolveProviderChain(
51
54
  preferredProvider: SearchProviderId | "auto" = preferredProvId,
52
55
  ): Promise<SearchProvider[]> {