@aliou/pi-synthetic 0.14.0 → 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
@@ -54,9 +54,11 @@ Once installed, select `synthetic` as your provider and choose from available mo
54
54
 
55
55
  ### Model Hosting
56
56
 
57
- All models are accessed through Synthetic's API. Some models are hosted by Synthetic directly; others are proxied by Synthetic to upstream backends such as Fireworks or Together.
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
58
 
59
- The code tracks this in `src/extensions/provider/models.ts` with each model's `provider` field. That field is for maintenance only and is stripped before registering models with Pi, so users always select the `synthetic` provider.
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.
60
62
 
61
63
  ### Web Search Tool
62
64
 
@@ -103,6 +105,8 @@ pi config extensions.disabled add @aliou/pi-synthetic/quota-warnings
103
105
 
104
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.
105
107
 
108
+ The **Proxied Models** setting is not a loadable extension feature. It is a regular setting controlled through `/synthetic:settings`.
109
+
106
110
  ## Adding or Updating Models
107
111
 
108
112
  Models are hardcoded in `src/extensions/provider/models.ts`. To add or update models:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-synthetic",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
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
  }