@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 +6 -2
- package/package.json +1 -1
- package/src/config.ts +51 -42
- package/src/extensions/provider/index.test.ts +46 -0
- package/src/extensions/provider/index.ts +37 -89
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
|
-
|
|
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
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
|
-
|
|
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
|
|
77
|
-
|
|
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-
|
|
83
|
-
shouldRun:
|
|
75
|
+
name: "seed-proxied-models",
|
|
76
|
+
shouldRun: needsProxiedModelsMigration,
|
|
84
77
|
run: (config) => {
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
}
|