@aliou/pi-synthetic 0.13.5 → 0.15.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Pi Synthetic Extension
4
4
 
5
- A Pi extension that adds [Synthetic](https://synthetic.new) as a model provider, giving you access to open-source models through an OpenAI-compatible API.
5
+ A Pi extension that adds [Synthetic](https://synthetic.new) as a model provider, giving you access to open-source models through Synthetic's OpenAI-compatible API.
6
6
 
7
7
  ## Installation
8
8
 
@@ -52,6 +52,14 @@ Once installed, select `synthetic` as your provider and choose from available mo
52
52
  /model synthetic hf:moonshotai/Kimi-K2.5
53
53
  ```
54
54
 
55
+ ### Model Hosting
56
+
57
+ All models are accessed through Synthetic's API. Some models are hosted by Synthetic directly (`provider: "synthetic"` in the model config); others are proxied by Synthetic to upstream backends such as Fireworks or Together.
58
+
59
+ By default, new installs show only Synthetic-hosted models. You can enable proxied models in `/synthetic:settings` under **Models > Proxied Models**. Existing configurations keep proxied models enabled to preserve prior behavior.
60
+
61
+ The `provider` field in `src/extensions/provider/models.ts` is for maintenance only and is stripped before registering models with Pi, so users always select the `synthetic` provider.
62
+
55
63
  ### Web Search Tool
56
64
 
57
65
  The extension registers `synthetic_web_search` — a zero-data-retention web search tool. The tool is always visible; it fails with a clear message if credentials are missing or the account lacks a subscription.
@@ -89,21 +97,24 @@ The extension automatically notifies you when you approach or exceed your Synthe
89
97
 
90
98
  ## Disabling Features
91
99
 
92
- Each feature (provider, web search, quotas command, usage status, quota warnings) is a separate Pi extension. You can disable individual features using `pi config`:
100
+ Each feature (provider, web search, quotas command, sub bar integration, usage status, quota warnings) is a separate Pi extension. You can disable individual features using `pi config`:
93
101
 
94
102
  ```
95
103
  pi config extensions.disabled add @aliou/pi-synthetic/quota-warnings
96
104
  ```
97
105
 
98
- This prevents the quota-warnings extension from loading while keeping the rest of pi-synthetic active. Replace `quota-warnings` with `web-search`, `command-quotas`, or `provider` to disable other features.
106
+ This prevents the quota-warnings extension from loading while keeping the rest of pi-synthetic active. Replace `quota-warnings` with `web-search`, `command-quotas`, `sub-bar-integration`, `usage-status`, or `provider` to disable other features.
107
+
108
+ The **Proxied Models** setting is not a loadable extension feature. It is a regular setting controlled through `/synthetic:settings`.
99
109
 
100
110
  ## Adding or Updating Models
101
111
 
102
- Models are hardcoded in `src/providers/models.ts`. To add or update models:
112
+ Models are hardcoded in `src/extensions/provider/models.ts`. To add or update models:
103
113
 
104
- 1. Edit `src/providers/models.ts`
114
+ 1. Edit `src/extensions/provider/models.ts`
105
115
  2. Add the model configuration following the `SyntheticModelConfig` interface
106
- 3. Run `pnpm run typecheck` to verify
116
+ 3. Set `provider` to the upstream backend Synthetic uses for that model, such as `synthetic`, `fireworks`, or `together`
117
+ 4. Run `pnpm run typecheck` to verify
107
118
 
108
119
  ## Development
109
120
 
@@ -156,7 +167,7 @@ This repository uses [Changesets](https://github.com/changesets/changesets) for
156
167
 
157
168
  ## Requirements
158
169
 
159
- - Pi coding agent v0.50.0+
170
+ - Pi coding agent v0.72.0+
160
171
  - Synthetic API key (configured in `~/.pi/agent/auth.json` or via `SYNTHETIC_API_KEY`)
161
172
 
162
173
  ## Links
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-synthetic",
3
- "version": "0.13.5",
3
+ "version": "0.15.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -32,8 +32,8 @@
32
32
  "README.md"
33
33
  ],
34
34
  "peerDependencies": {
35
- "@mariozechner/pi-coding-agent": "0.61.0",
36
- "@mariozechner/pi-tui": "0.61.0"
35
+ "@mariozechner/pi-coding-agent": "0.72.0",
36
+ "@mariozechner/pi-tui": "0.72.0"
37
37
  },
38
38
  "dependencies": {
39
39
  "@aliou/pi-utils-settings": "^0.13.0",
@@ -43,8 +43,8 @@
43
43
  "@aliou/biome-plugins": "^0.7.0",
44
44
  "@biomejs/biome": "^2.4.2",
45
45
  "@changesets/cli": "^2.27.11",
46
- "@mariozechner/pi-coding-agent": "0.61.0",
47
- "@sinclair/typebox": "^0.34.48",
46
+ "@mariozechner/pi-coding-agent": "0.72.0",
47
+ "typebox": "^1.1.37",
48
48
  "@types/node": "^25.0.10",
49
49
  "husky": "^9.1.7",
50
50
  "typescript": "^5.9.3",
package/src/config.ts CHANGED
@@ -25,12 +25,7 @@ export interface SyntheticExtensionsRegisterPayload {
25
25
  feature: SyntheticFeatureId;
26
26
  }
27
27
 
28
- /**
29
- * Config schema version. Stamped on disk when the initial migration runs or
30
- * when the config is seeded. Uses the package version; bumping the package
31
- * does not retrigger migrations (we only run them when `configVersion` is
32
- * missing), but it records which release first created the file.
33
- */
28
+ /** Config schema version. Stamped on disk when config is seeded or migrated. */
34
29
  export const SYNTHETIC_CONFIG_VERSION: string = pkg.version;
35
30
 
36
31
  export interface SyntheticConfig {
@@ -40,6 +35,7 @@ export interface SyntheticConfig {
40
35
  usageStatus?: boolean;
41
36
  quotaWarnings?: boolean;
42
37
  subBarIntegration?: boolean;
38
+ proxiedModels?: boolean;
43
39
  }
44
40
 
45
41
  export interface ResolvedSyntheticConfig {
@@ -49,6 +45,7 @@ export interface ResolvedSyntheticConfig {
49
45
  usageStatus: boolean;
50
46
  quotaWarnings: boolean;
51
47
  subBarIntegration: boolean;
48
+ proxiedModels: boolean;
52
49
  }
53
50
 
54
51
  const DEFAULT_CONFIG: ResolvedSyntheticConfig = {
@@ -58,39 +55,33 @@ const DEFAULT_CONFIG: ResolvedSyntheticConfig = {
58
55
  usageStatus: false,
59
56
  quotaWarnings: false,
60
57
  subBarIntegration: true,
58
+ proxiedModels: false,
61
59
  };
62
60
 
63
- // Module-level flag set when the v1 migration runs or when the global config
64
- // is seeded for the first time. Consumed once by the provider extension to
65
- // display a one-time notice about the new settings UI.
66
- let pendingMigrationNotice = false;
67
-
68
- export function hasPendingMigrationNotice(): boolean {
69
- return pendingMigrationNotice;
70
- }
71
-
72
- export function clearPendingMigrationNotice(): void {
73
- pendingMigrationNotice = false;
74
- }
61
+ export const pendingMessages: string[] = [];
75
62
 
76
- function markMigrationNoticePending(): void {
77
- pendingMigrationNotice = true;
63
+ function needsProxiedModelsMigration(config: SyntheticConfig): boolean {
64
+ if (config.proxiedModels !== undefined) return false;
65
+ if (config.configVersion === undefined) return true;
66
+ return (
67
+ config.configVersion.localeCompare("0.13.5", undefined, {
68
+ numeric: true,
69
+ }) <= 0
70
+ );
78
71
  }
79
72
 
80
73
  const migrations: Migration<SyntheticConfig>[] = [
81
74
  {
82
- name: "seed-defaults",
83
- shouldRun: (config) => config.configVersion === undefined,
75
+ name: "seed-proxied-models",
76
+ shouldRun: needsProxiedModelsMigration,
84
77
  run: (config) => {
85
- markMigrationNoticePending();
78
+ pendingMessages.push(
79
+ "This provider now differentiates hosted models from proxied models and allows disabling them. Use `/synthetic:settings` to disable them.",
80
+ );
86
81
  return {
82
+ ...config,
87
83
  configVersion: SYNTHETIC_CONFIG_VERSION,
88
- webSearch: config.webSearch ?? DEFAULT_CONFIG.webSearch,
89
- quotasCommand: config.quotasCommand ?? DEFAULT_CONFIG.quotasCommand,
90
- usageStatus: config.usageStatus ?? DEFAULT_CONFIG.usageStatus,
91
- quotaWarnings: config.quotaWarnings ?? DEFAULT_CONFIG.quotaWarnings,
92
- subBarIntegration:
93
- config.subBarIntegration ?? DEFAULT_CONFIG.subBarIntegration,
84
+ proxiedModels: true,
94
85
  };
95
86
  },
96
87
  },
@@ -106,8 +97,7 @@ export const configLoader = new ConfigLoader<
106
97
 
107
98
  /**
108
99
  * Seed the global config file on first use. When no config file exists in
109
- * any scope, this writes the current defaults (with configVersion) to the
110
- * global scope and flags the migration notice as pending.
100
+ * any scope, this writes the current defaults with configVersion.
111
101
  *
112
102
  * Must be called after `configLoader.load()`.
113
103
  */
@@ -115,18 +105,10 @@ export async function seedSyntheticConfigIfMissing(): Promise<void> {
115
105
  if (configLoader.hasConfig("global") || configLoader.hasConfig("local")) {
116
106
  return;
117
107
  }
118
- markMigrationNoticePending();
119
108
  try {
120
- await configLoader.save("global", {
121
- configVersion: SYNTHETIC_CONFIG_VERSION,
122
- webSearch: DEFAULT_CONFIG.webSearch,
123
- quotasCommand: DEFAULT_CONFIG.quotasCommand,
124
- usageStatus: DEFAULT_CONFIG.usageStatus,
125
- quotaWarnings: DEFAULT_CONFIG.quotaWarnings,
126
- subBarIntegration: DEFAULT_CONFIG.subBarIntegration,
127
- });
109
+ await configLoader.save("global", DEFAULT_CONFIG);
128
110
  } catch {
129
- // If the write fails, keep the notice pending so the user still sees it.
111
+ // Ignore seed failures. Defaults still resolve in memory.
130
112
  }
131
113
  }
132
114
 
@@ -191,10 +173,24 @@ export function registerSyntheticSettings(
191
173
  const quotaWarnings = tabConfig?.quotaWarnings ?? resolved.quotaWarnings;
192
174
  const subBarIntegration =
193
175
  tabConfig?.subBarIntegration ?? resolved.subBarIntegration;
176
+ const proxiedModels = tabConfig?.proxiedModels ?? resolved.proxiedModels;
194
177
 
195
178
  const sections: SettingsSection[] = [];
196
179
 
197
180
  sections.push(
181
+ {
182
+ label: "Models",
183
+ items: [
184
+ {
185
+ id: "proxiedModels",
186
+ label: "Proxied Models",
187
+ description:
188
+ "Allow models that Synthetic proxies to upstream backends such as Fireworks or Together. Disable to show only models hosted directly by Synthetic.",
189
+ currentValue: proxiedModels ? "enabled" : "disabled",
190
+ values: ["enabled", "disabled"],
191
+ },
192
+ ],
193
+ },
198
194
  {
199
195
  label: "Tools",
200
196
  items: [
@@ -250,12 +246,25 @@ export function registerSyntheticSettings(
250
246
  return sections;
251
247
  },
252
248
  onSettingChange: (id, newValue, config) => {
253
- if (!getLoadedFeatures().has(id as SyntheticFeatureId)) {
249
+ const featureIds = new Set<string>([
250
+ "webSearch",
251
+ "quotasCommand",
252
+ "usageStatus",
253
+ "quotaWarnings",
254
+ "subBarIntegration",
255
+ ]);
256
+
257
+ if (
258
+ featureIds.has(id) &&
259
+ !getLoadedFeatures().has(id as SyntheticFeatureId)
260
+ ) {
254
261
  return null;
255
262
  }
256
263
 
257
264
  const enabled = newValue === "enabled";
258
265
  switch (id) {
266
+ case "proxiedModels":
267
+ return { ...config, proxiedModels: enabled };
259
268
  case "webSearch":
260
269
  return { ...config, webSearch: enabled };
261
270
  case "quotasCommand":
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildSyntheticProviderModels } from "./index";
3
+ import { SYNTHETIC_MODELS } from "./models";
4
+
5
+ describe("buildSyntheticProviderModels", () => {
6
+ it("excludes proxied models when includeProxiedModels is false", () => {
7
+ const models = buildSyntheticProviderModels(false);
8
+ for (const model of models) {
9
+ const source = SYNTHETIC_MODELS.find((m) => m.id === model.id);
10
+ expect(source).toBeDefined();
11
+ expect(source?.provider).toBe("synthetic");
12
+ }
13
+ });
14
+
15
+ it("includes all models when includeProxiedModels is true", () => {
16
+ const models = buildSyntheticProviderModels(true);
17
+ expect(models).toHaveLength(SYNTHETIC_MODELS.length);
18
+ });
19
+
20
+ it("does not expose the internal provider field", () => {
21
+ const models = buildSyntheticProviderModels(true);
22
+ for (const model of models) {
23
+ expect(model).not.toHaveProperty("provider");
24
+ }
25
+ });
26
+
27
+ it("sets default compat fields on every model", () => {
28
+ const models = buildSyntheticProviderModels(true);
29
+ for (const model of models) {
30
+ expect(model.compat).toMatchObject({
31
+ supportsDeveloperRole: false,
32
+ });
33
+ expect(model.compat).toHaveProperty("maxTokensField");
34
+ }
35
+ });
36
+
37
+ it("preserves model-specific compat overrides", () => {
38
+ const models = buildSyntheticProviderModels(true);
39
+ const miniMax = models.find((m) => m.id === "hf:MiniMaxAI/MiniMax-M2.5");
40
+ expect(miniMax).toBeDefined();
41
+ expect(miniMax?.compat).toMatchObject({
42
+ supportsDeveloperRole: false,
43
+ maxTokensField: "max_completion_tokens",
44
+ });
45
+ });
46
+ });
@@ -1,60 +1,40 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
3
2
  import {
4
- clearPendingMigrationNotice,
5
3
  configLoader,
6
4
  emitSyntheticConfigUpdated,
7
- hasPendingMigrationNotice,
5
+ pendingMessages,
8
6
  registerSyntheticSettings,
7
+ SYNTHETIC_CONFIG_UPDATED_EVENT,
9
8
  SYNTHETIC_EXTENSIONS_REGISTER_EVENT,
10
9
  SYNTHETIC_EXTENSIONS_REQUEST_EVENT,
10
+ type SyntheticConfigUpdatedPayload,
11
11
  type SyntheticExtensionsRegisterPayload,
12
12
  type SyntheticFeatureId,
13
13
  seedSyntheticConfigIfMissing,
14
14
  } from "../../config";
15
15
  import { SYNTHETIC_MODELS } from "./models";
16
16
 
17
- const MIGRATION_NOTICE_MESSAGE_TYPE = "synthetic:migration-notice";
18
- const MIGRATION_NOTICE_TITLE = "pi-synthetic";
19
- const MIGRATION_NOTICE_CONTENT = [
20
- "New optional features added to `pi-synthetic`:",
21
- "- Usage widget",
22
- "- Quotas warnings",
23
- "",
24
- "Enable them either with `pi config` or inside of `pi` with `/synthetic:settings`.",
25
- ].join("\n");
26
-
27
- /** Wrap lines in a rounded Unicode frame with 1-char inner padding. */
28
- function wrapInRoundedBorder(
29
- lines: string[],
30
- width: number,
31
- colorFn: (text: string) => string,
32
- ): string[] {
33
- const innerWidth = Math.max(1, width - 2);
34
- const hBar = "\u2500".repeat(innerWidth);
35
- const top = colorFn(`\u256D${hBar}\u256E`);
36
- const bottom = colorFn(`\u2570${hBar}\u256F`);
37
- const left = colorFn("\u2502");
38
- const right = colorFn("\u2502");
39
-
40
- const wrapped = lines.map((line) => {
41
- const contentWidth = visibleWidth(line);
42
- const fill = Math.max(0, innerWidth - contentWidth);
43
- return `${left}${line}${" ".repeat(fill)}${right}`;
44
- });
45
-
46
- return [top, ...wrapped, bottom];
17
+ export function buildSyntheticProviderModels(includeProxiedModels: boolean) {
18
+ return SYNTHETIC_MODELS.filter(
19
+ (model) => includeProxiedModels || model.provider === "synthetic",
20
+ ).map(({ provider: _provider, ...model }) => ({
21
+ ...model,
22
+ compat: {
23
+ supportsDeveloperRole: false,
24
+ maxTokensField: "max_tokens" as const,
25
+ ...model.compat,
26
+ },
27
+ }));
47
28
  }
48
29
 
49
- /** Highlight `backtick-wrapped` spans using the accent color. */
50
- function highlightInlineCode(
51
- text: string,
52
- colorFn: (text: string) => string,
53
- ): string {
54
- return text.replace(/`([^`]+)`/g, (_, code) => colorFn(code));
30
+ interface RegisterSyntheticProviderOptions {
31
+ includeProxiedModels: boolean;
55
32
  }
56
33
 
57
- export function registerSyntheticProvider(pi: ExtensionAPI): void {
34
+ export function registerSyntheticProvider(
35
+ pi: ExtensionAPI,
36
+ options: RegisterSyntheticProviderOptions,
37
+ ): void {
58
38
  pi.registerProvider("synthetic", {
59
39
  baseUrl: "https://api.synthetic.new/openai/v1",
60
40
  apiKey: "SYNTHETIC_API_KEY",
@@ -63,50 +43,22 @@ export function registerSyntheticProvider(pi: ExtensionAPI): void {
63
43
  Referer: "https://pi.dev",
64
44
  "X-Title": "npm:@aliou/pi-synthetic",
65
45
  },
66
- models: SYNTHETIC_MODELS.map(({ provider: _provider, ...model }) => ({
67
- ...model,
68
- compat: {
69
- supportsDeveloperRole: false,
70
- maxTokensField: "max_tokens",
71
- ...model.compat,
72
- },
73
- })),
46
+ models: buildSyntheticProviderModels(options.includeProxiedModels),
74
47
  });
75
48
  }
76
49
 
77
50
  export default async function (pi: ExtensionAPI) {
78
51
  await configLoader.load();
79
52
  await seedSyntheticConfigIfMissing();
80
- registerSyntheticProvider(pi);
81
53
 
82
- pi.registerMessageRenderer(
83
- MIGRATION_NOTICE_MESSAGE_TYPE,
84
- (message, _options, theme) => {
85
- const rawContent =
86
- typeof message.content === "string"
87
- ? message.content
88
- : MIGRATION_NOTICE_CONTENT;
89
- const accent = (t: string) => theme.fg("accent", t);
90
- const borderColor = accent;
91
- const title = theme.bold(accent(MIGRATION_NOTICE_TITLE));
92
- const body = highlightInlineCode(rawContent, accent);
54
+ const includeProxiedModels = configLoader.getConfig().proxiedModels;
55
+ registerSyntheticProvider(pi, { includeProxiedModels });
93
56
 
94
- return {
95
- render(width: number) {
96
- // border (2) + inner padding (2)
97
- const contentWidth = Math.max(1, width - 4);
98
- const bodyLines = wrapTextWithAnsi(body, contentWidth);
99
- const lines = [title, "", ...bodyLines];
100
- const padded = lines.map((line) => ` ${line} `);
101
- return wrapInRoundedBorder(padded, width, borderColor);
102
- },
103
- handleInput() {
104
- return false;
105
- },
106
- invalidate() {},
107
- };
108
- },
109
- );
57
+ pi.events.on(SYNTHETIC_CONFIG_UPDATED_EVENT, (data: unknown) => {
58
+ const includeProxiedModels = (data as SyntheticConfigUpdatedPayload).config
59
+ .proxiedModels;
60
+ registerSyntheticProvider(pi, { includeProxiedModels });
61
+ });
110
62
 
111
63
  const loadedFeatures = new Set<SyntheticFeatureId>();
112
64
 
@@ -119,21 +71,17 @@ export default async function (pi: ExtensionAPI) {
119
71
  getLoadedFeatures: () => loadedFeatures,
120
72
  });
121
73
 
122
- pi.on("session_start", async () => {
74
+ pi.on("session_start", async (_event, ctx) => {
75
+ const messages = pendingMessages.splice(0).map((m) => `- ${m}`);
76
+ if (messages.length > 0) {
77
+ ctx.ui.notify(
78
+ `[synthetic] Migration messages: \n ${messages.join("\n")}`,
79
+ "info",
80
+ );
81
+ }
82
+
123
83
  loadedFeatures.clear();
124
84
  pi.events.emit(SYNTHETIC_EXTENSIONS_REQUEST_EVENT, undefined);
125
85
  emitSyntheticConfigUpdated(pi);
126
-
127
- if (hasPendingMigrationNotice()) {
128
- clearPendingMigrationNotice();
129
- pi.sendMessage(
130
- {
131
- customType: MIGRATION_NOTICE_MESSAGE_TYPE,
132
- content: MIGRATION_NOTICE_CONTENT,
133
- display: true,
134
- },
135
- { triggerTurn: false },
136
- );
137
- }
138
86
  });
139
87
  }
@@ -9,14 +9,6 @@ export interface SyntheticModelConfig extends ProviderModelConfig {
9
9
  provider: string;
10
10
  }
11
11
 
12
- const SYNTHETIC_REASONING_EFFORT_MAP = {
13
- minimal: "low",
14
- low: "low",
15
- medium: "medium",
16
- high: "high",
17
- xhigh: "high",
18
- } as const;
19
-
20
12
  export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
21
13
  // API: hf:zai-org/GLM-4.7 → ctx=202752
22
14
  {
@@ -24,9 +16,9 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
24
16
  name: "zai-org/GLM-4.7",
25
17
  provider: "synthetic",
26
18
  reasoning: true,
19
+ thinkingLevelMap: { minimal: null, xhigh: null },
27
20
  compat: {
28
21
  supportsReasoningEffort: true,
29
- reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
30
22
  },
31
23
  input: ["text"],
32
24
  cost: {
@@ -44,9 +36,9 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
44
36
  name: "zai-org/GLM-5",
45
37
  provider: "synthetic",
46
38
  reasoning: true,
39
+ thinkingLevelMap: { minimal: null, xhigh: null },
47
40
  compat: {
48
41
  supportsReasoningEffort: true,
49
- reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
50
42
  },
51
43
  input: ["text"],
52
44
  cost: {
@@ -64,9 +56,9 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
64
56
  name: "zai-org/GLM-5.1",
65
57
  provider: "synthetic",
66
58
  reasoning: true,
59
+ thinkingLevelMap: { minimal: null, xhigh: null },
67
60
  compat: {
68
61
  supportsReasoningEffort: true,
69
- reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
70
62
  supportsDeveloperRole: false,
71
63
  },
72
64
  input: ["text"],
@@ -85,9 +77,9 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
85
77
  name: "zai-org/GLM-4.7-Flash",
86
78
  provider: "synthetic",
87
79
  reasoning: true,
80
+ thinkingLevelMap: { minimal: null, xhigh: null },
88
81
  compat: {
89
82
  supportsReasoningEffort: true,
90
- reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
91
83
  },
92
84
  input: ["text"],
93
85
  cost: {
@@ -104,7 +96,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
104
96
  id: "hf:meta-llama/Llama-3.3-70B-Instruct",
105
97
  name: "meta-llama/Llama-3.3-70B-Instruct",
106
98
  provider: "together",
107
- reasoning: true,
99
+ reasoning: false,
108
100
  input: ["text"],
109
101
  cost: {
110
102
  input: 0.88,
@@ -121,10 +113,6 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
121
113
  name: "deepseek-ai/DeepSeek-R1-0528",
122
114
  provider: "together",
123
115
  reasoning: true,
124
- compat: {
125
- supportsReasoningEffort: true,
126
- reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
127
- },
128
116
  input: ["text"],
129
117
  cost: {
130
118
  input: 3,
@@ -173,10 +161,6 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
173
161
  name: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
174
162
  provider: "together",
175
163
  reasoning: true,
176
- compat: {
177
- supportsReasoningEffort: true,
178
- reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
179
- },
180
164
  input: ["text"],
181
165
  cost: {
182
166
  input: 2,
@@ -193,9 +177,9 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
193
177
  name: "moonshotai/Kimi-K2.6",
194
178
  provider: "synthetic",
195
179
  reasoning: true,
180
+ thinkingLevelMap: { minimal: null, low: null, xhigh: null },
196
181
  compat: {
197
182
  supportsReasoningEffort: true,
198
- reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
199
183
  },
200
184
  input: ["text", "image"],
201
185
  cost: {
@@ -207,16 +191,28 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
207
191
  contextWindow: 262144,
208
192
  maxTokens: 65536,
209
193
  },
194
+ // API: hf:moonshotai/Kimi-K2.5 → ctx=262144, out=65536
195
+ {
196
+ id: "hf:moonshotai/Kimi-K2.5",
197
+ name: "moonshotai/Kimi-K2.5",
198
+ provider: "together",
199
+ reasoning: true,
200
+ input: ["text", "image"],
201
+ cost: {
202
+ input: 0.5,
203
+ output: 2.8,
204
+ cacheRead: 0.5,
205
+ cacheWrite: 0,
206
+ },
207
+ contextWindow: 262144,
208
+ maxTokens: 65536,
209
+ },
210
210
  // API: hf:nvidia/Kimi-K2.5-NVFP4 → ctx=262144; models.dev: out=65536 (NVFP4 quantized)
211
211
  {
212
212
  id: "hf:nvidia/Kimi-K2.5-NVFP4",
213
213
  name: "nvidia/Kimi-K2.5-NVFP4",
214
214
  provider: "together",
215
215
  reasoning: true,
216
- compat: {
217
- supportsReasoningEffort: true,
218
- reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
219
- },
220
216
  input: ["text", "image"],
221
217
  cost: {
222
218
  input: 0.5,
@@ -249,10 +245,6 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
249
245
  name: "Qwen/Qwen3-235B-A22B-Thinking-2507",
250
246
  provider: "together",
251
247
  reasoning: true,
252
- compat: {
253
- supportsReasoningEffort: true,
254
- reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
255
- },
256
248
  input: ["text"],
257
249
  cost: {
258
250
  input: 0.65,
@@ -269,10 +261,6 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
269
261
  name: "Qwen/Qwen3.5-397B-A17B",
270
262
  provider: "together",
271
263
  reasoning: true,
272
- compat: {
273
- supportsReasoningEffort: true,
274
- reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
275
- },
276
264
  input: ["text", "image"],
277
265
  cost: {
278
266
  input: 0.6,
@@ -289,6 +277,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
289
277
  name: "MiniMaxAI/MiniMax-M2.5",
290
278
  provider: "synthetic",
291
279
  reasoning: true,
280
+ thinkingLevelMap: { off: null, minimal: null, low: null, xhigh: null },
292
281
  input: ["text"],
293
282
  cost: {
294
283
  input: 0.4,
@@ -300,7 +289,6 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
300
289
  maxTokens: 65536,
301
290
  compat: {
302
291
  supportsReasoningEffort: true,
303
- reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
304
292
  maxTokensField: "max_completion_tokens",
305
293
  },
306
294
  },
@@ -310,9 +298,9 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
310
298
  name: "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
311
299
  provider: "synthetic",
312
300
  reasoning: true,
301
+ thinkingLevelMap: { minimal: null, low: null, xhigh: null },
313
302
  compat: {
314
303
  supportsReasoningEffort: true,
315
- reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
316
304
  },
317
305
  input: ["text"],
318
306
  cost: {
@@ -210,13 +210,20 @@ export default async function (pi: ExtensionAPI) {
210
210
  void refresher.refreshFor(ctx);
211
211
  });
212
212
 
213
- pi.on("session_switch", (_event, ctx) => {
214
- currentContext = ctx;
215
- currentProvider = ctx.model?.provider;
216
- if (enabled && ctx.model?.provider === "synthetic") {
217
- void refresher.refreshFor(ctx);
218
- } else {
219
- refresher.stopAutoRefresh(ctx);
213
+ pi.on("session_start", (event, ctx) => {
214
+ // Handle session switches (model_select handles mid-session provider changes)
215
+ if (
216
+ event.reason === "new" ||
217
+ event.reason === "resume" ||
218
+ event.reason === "fork"
219
+ ) {
220
+ currentContext = ctx;
221
+ currentProvider = ctx.model?.provider;
222
+ if (enabled && ctx.model?.provider === "synthetic") {
223
+ void refresher.refreshFor(ctx);
224
+ } else {
225
+ refresher.stopAutoRefresh(ctx);
226
+ }
220
227
  }
221
228
  });
222
229
 
@@ -8,7 +8,7 @@ import type {
8
8
  } from "@mariozechner/pi-coding-agent";
9
9
  import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
10
10
  import { Container, Markdown, Text } from "@mariozechner/pi-tui";
11
- import { type Static, Type } from "@sinclair/typebox";
11
+ import { type Static, Type } from "typebox";
12
12
  import { configLoader } from "../../config";
13
13
  import { getSyntheticApiKey } from "../../lib/env";
14
14