pi-fabric 0.110.3 → 0.111.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 +2 -0
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/dist/agents/provider-bridge.d.ts +22 -1
- package/dist/agents/provider-bridge.d.ts.map +1 -1
- package/dist/agents/provider-bridge.js +35 -4
- package/dist/agents/provider-bridge.js.map +2 -2
- package/docs/configuration.md +27 -5
- package/node_modules/pi-coralbricks-provider/LICENSE +21 -0
- package/node_modules/pi-coralbricks-provider/README.md +306 -0
- package/node_modules/pi-coralbricks-provider/auth.ts +30 -0
- package/node_modules/pi-coralbricks-provider/custom-models.json +1 -0
- package/node_modules/pi-coralbricks-provider/deprecated-models.json +1 -0
- package/node_modules/pi-coralbricks-provider/guard.ts +155 -0
- package/node_modules/pi-coralbricks-provider/index.ts +662 -0
- package/node_modules/pi-coralbricks-provider/models.json +88 -0
- package/node_modules/pi-coralbricks-provider/package.json +74 -0
- package/node_modules/pi-coralbricks-provider/park.ts +122 -0
- package/node_modules/pi-coralbricks-provider/patch.json +34 -0
- package/node_modules/pi-coralbricks-provider/scripts/update-models.js +658 -0
- package/node_modules/pi-coralbricks-provider/settings.ts +143 -0
- package/node_modules/pi-coralbricks-provider/sync-policy.ts +309 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -90,6 +90,8 @@ Requires Node.js 24+ and Pi 1.0.0+. Monty's optional native package installs on
|
|
|
90
90
|
pi install npm:pi-fabric
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
Fabric bundles the [Coral Bricks provider](docs/configuration.md#bundled-coral-bricks-provider): run `/login coralbricks` to use `coralbricks/*` models, or opt out with a package filter. Fabric's maintainers work at Coral Bricks.
|
|
94
|
+
|
|
93
95
|
<details>
|
|
94
96
|
<summary>Other install methods</summary>
|
|
95
97
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -8,6 +8,13 @@ Haydn Paterson, licensed under MIT. This private artifact is not loaded by the
|
|
|
8
8
|
Pi extension, which continues to use Pi's host-provided TypeBox. The complete
|
|
9
9
|
license is distributed at [docs/licenses/typebox-mit.txt](docs/licenses/typebox-mit.txt).
|
|
10
10
|
|
|
11
|
+
## pi-coralbricks-provider
|
|
12
|
+
|
|
13
|
+
The package bundles [`pi-coralbricks-provider`](https://github.com/monotykamary/pi-coralbricks-provider)
|
|
14
|
+
unmodified under `node_modules/pi-coralbricks-provider`, at the exact version
|
|
15
|
+
pinned in `package.json`. It is licensed under MIT, and its complete license is
|
|
16
|
+
distributed beside it at `node_modules/pi-coralbricks-provider/LICENSE`.
|
|
17
|
+
|
|
11
18
|
## Bend 2.0.36
|
|
12
19
|
|
|
13
20
|
The generated policy kernel includes Bend's emitted JavaScript trampoline and
|
|
@@ -38,7 +38,12 @@ export declare function orderProviderCandidates(paths: readonly string[], provid
|
|
|
38
38
|
named: string[];
|
|
39
39
|
rest: string[];
|
|
40
40
|
};
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Fabric's own manifest entries: they never register chat providers. Entries
|
|
43
|
+
* of bundled Pi packages (pi-coralbricks-provider) are not Fabric's own: they
|
|
44
|
+
* stay probe candidates, so the bundled provider resolves without loading
|
|
45
|
+
* Fabric's main extension.
|
|
46
|
+
*/
|
|
42
47
|
export declare function fabricExtensionEntries(moduleUrl?: string): string[];
|
|
43
48
|
/**
|
|
44
49
|
* Find the extension registrations that make `model` resolvable in a child
|
|
@@ -48,6 +53,22 @@ export declare function fabricExtensionEntries(moduleUrl?: string): string[];
|
|
|
48
53
|
* installed extension registers the provider.
|
|
49
54
|
*/
|
|
50
55
|
export declare function resolveExtensionProviders(options: ResolveExtensionProvidersOptions, sdk?: PiSdk): Promise<ExtensionProviderRegistrations | undefined>;
|
|
56
|
+
/**
|
|
57
|
+
* The SDK entry of the bundled Pi CLI running this process, if it is one. The
|
|
58
|
+
* published `pi` bin is `dist/bundle/cli.js`; its `dist/bundle/index.js` SDK
|
|
59
|
+
* entry is built from the same chunks the CLI has already evaluated.
|
|
60
|
+
*/
|
|
61
|
+
export declare function hostBundleSdkEntry(cliPath?: string | undefined): string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* The host's own SDK. Pi imports this bridge natively (it is ESM JavaScript),
|
|
64
|
+
* so jiti's virtual modules do not apply, and a bare package import from here
|
|
65
|
+
* evaluates the separate unbundled SDK: about 1,800 more modules in the
|
|
66
|
+
* bundled CLI. Besides the startup cost, that CLI enables Node's compile cache,
|
|
67
|
+
* which serializes every one of those modules on exit; with a cold cache that
|
|
68
|
+
* held a Windows child past the worker's exit grace after it had settled.
|
|
69
|
+
* The bundle's SDK entry shares the CLI's already loaded chunks instead.
|
|
70
|
+
*/
|
|
71
|
+
export declare function loadHostSdk(cliPath?: string | undefined): Promise<PiSdk>;
|
|
51
72
|
/**
|
|
52
73
|
* Loaded with `-e` into Pi children launched with `extensions: false`. It
|
|
53
74
|
* registers only the requested model's provider, as its extension registered
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-bridge.d.ts","sourceRoot":"","sources":["../../src/agents/provider-bridge.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAmB,MAAM,iCAAiC,CAAC;AAErG;;;GAGG;AACH,eAAO,MAAM,yBAAyB,8BAA8B,CAAC;AAErE,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,cAAc,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChF,OAAO,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,gCAAgC;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,KAAK,KAAK,GAAG,cAAc,iCAAiC,CAAC,CAAC;AAE9D,8DAA8D;AAC9D,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAKvF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,QAAQ,EAAE,MAAM,GACf;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CASrC;
|
|
1
|
+
{"version":3,"file":"provider-bridge.d.ts","sourceRoot":"","sources":["../../src/agents/provider-bridge.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAmB,MAAM,iCAAiC,CAAC;AAErG;;;GAGG;AACH,eAAO,MAAM,yBAAyB,8BAA8B,CAAC;AAErE,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,cAAc,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChF,OAAO,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,gCAAgC;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,KAAK,KAAK,GAAG,cAAc,iCAAiC,CAAC,CAAC;AAE9D,8DAA8D;AAC9D,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAKvF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,QAAQ,EAAE,MAAM,GACf;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CASrC;AAcD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,SAAkB,GAAG,MAAM,EAAE,CAgB5E;AA0CD;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,gCAAgC,EACzC,GAAG,CAAC,EAAE,KAAK,GACV,OAAO,CAAC,8BAA8B,GAAG,SAAS,CAAC,CA4CrD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,MAAM,GAAG,SAA2B,GAAG,MAAM,GAAG,SAAS,CAiBpG;AAED;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,OAAO,GAAE,MAAM,GAAG,SAA2B,GAAG,OAAO,CAAC,KAAK,CAAC,CAU/F;AAED;;;;;GAKG;AACH,wBAA8B,oBAAoB,CAAC,EAAE,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAQlF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/agents/provider-bridge.ts
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
var PROVIDER_BRIDGE_MODEL_ENV = "PI_FABRIC_EXTENSION_MODEL";
|
|
6
6
|
function parseModelKey(key) {
|
|
7
7
|
const value = key.trim();
|
|
@@ -26,13 +26,14 @@ var realPath = (file) => {
|
|
|
26
26
|
return path.resolve(file);
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
|
+
var isBundledPackageEntry = (entry) => entry.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").startsWith("node_modules/");
|
|
29
30
|
function fabricExtensionEntries(moduleUrl = import.meta.url) {
|
|
30
31
|
const root = path.resolve(path.dirname(fileURLToPath(moduleUrl)), "..", "..");
|
|
31
32
|
try {
|
|
32
33
|
const manifest = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
33
34
|
if (manifest.name !== "pi-fabric") return [];
|
|
34
35
|
const entries = [manifest.main, ...Array.isArray(manifest.pi?.extensions) ? manifest.pi.extensions : []];
|
|
35
|
-
return entries.filter((entry) => typeof entry === "string").map((entry) => path.resolve(root, entry));
|
|
36
|
+
return entries.filter((entry) => typeof entry === "string" && !isBundledPackageEntry(entry)).map((entry) => path.resolve(root, entry));
|
|
36
37
|
} catch {
|
|
37
38
|
return [];
|
|
38
39
|
}
|
|
@@ -92,11 +93,39 @@ async function resolveExtensionProviders(options, sdk) {
|
|
|
92
93
|
`Model "${options.model}" needs provider "${key.provider}", which is not built into Pi or defined in models.json, and no installed Pi extension registers it for this extensions: false child. Install the extension that provides it, or run the child with extensions: true.` + (failures.length > 0 ? ` Extensions that failed to load while searching: ${[...new Set(failures)].join(", ")}.` : "")
|
|
93
94
|
);
|
|
94
95
|
}
|
|
96
|
+
function hostBundleSdkEntry(cliPath = process.argv[1]) {
|
|
97
|
+
if (!cliPath) return void 0;
|
|
98
|
+
let cli;
|
|
99
|
+
try {
|
|
100
|
+
cli = fs.realpathSync(cliPath);
|
|
101
|
+
} catch {
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
const bundle = path.dirname(cli);
|
|
105
|
+
if (path.basename(cli) !== "cli.js" || path.basename(bundle) !== "bundle") return void 0;
|
|
106
|
+
const entry = path.join(bundle, "index.js");
|
|
107
|
+
try {
|
|
108
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(bundle, "..", "..", "package.json"), "utf8"));
|
|
109
|
+
return manifest.name === "@earendil-works/pi-coding-agent" && fs.existsSync(entry) ? entry : void 0;
|
|
110
|
+
} catch {
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function loadHostSdk(cliPath = process.argv[1]) {
|
|
115
|
+
const entry = hostBundleSdkEntry(cliPath);
|
|
116
|
+
if (entry) {
|
|
117
|
+
try {
|
|
118
|
+
return await import(pathToFileURL(entry).href);
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return import("@earendil-works/pi-coding-agent");
|
|
123
|
+
}
|
|
95
124
|
async function fabricProviderBridge(pi) {
|
|
96
125
|
const model = process.env[PROVIDER_BRIDGE_MODEL_ENV]?.trim();
|
|
97
126
|
if (!model) return;
|
|
98
|
-
const
|
|
99
|
-
const found = await resolveExtensionProviders({ model, cwd: process.cwd(), agentDir: getAgentDir() });
|
|
127
|
+
const sdk = await loadHostSdk();
|
|
128
|
+
const found = await resolveExtensionProviders({ model, cwd: process.cwd(), agentDir: sdk.getAgentDir() }, sdk);
|
|
100
129
|
if (!found) return;
|
|
101
130
|
for (const { name, config } of found.configs) pi.registerProvider(name, config);
|
|
102
131
|
for (const { provider } of found.natives) pi.registerProvider(provider);
|
|
@@ -105,6 +134,8 @@ export {
|
|
|
105
134
|
PROVIDER_BRIDGE_MODEL_ENV,
|
|
106
135
|
fabricProviderBridge as default,
|
|
107
136
|
fabricExtensionEntries,
|
|
137
|
+
hostBundleSdkEntry,
|
|
138
|
+
loadHostSdk,
|
|
108
139
|
orderProviderCandidates,
|
|
109
140
|
parseModelKey,
|
|
110
141
|
resolveExtensionProviders
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/agents/provider-bridge.ts"],
|
|
4
|
-
"sourcesContent": ["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Provider } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI, ProviderConfig, SettingsManager } from \"@earendil-works/pi-coding-agent\";\n\n/**\n * The `provider/id` key a Pi child launched with `extensions: false` must\n * resolve. The worker sets it only when it also loads this bridge with `-e`.\n */\nexport const PROVIDER_BRIDGE_MODEL_ENV = \"PI_FABRIC_EXTENSION_MODEL\";\n\nexport interface ExtensionProviderRegistrations {\n configs: Array<{ name: string; config: ProviderConfig; extensionPath: string }>;\n natives: Array<{ provider: Provider; extensionPath: string }>;\n}\n\nexport interface ResolveExtensionProvidersOptions {\n model: string;\n cwd: string;\n agentDir: string;\n /** Extension entry files never probed (Fabric's own entries by default). */\n exclude?: readonly string[];\n}\n\ntype PiSdk = typeof import(\"@earendil-works/pi-coding-agent\");\n\n/** Split a canonical `provider/id` key at its first slash. */\nexport function parseModelKey(key: string): { provider: string; id: string } | undefined {\n const value = key.trim();\n const slash = value.indexOf(\"/\");\n if (slash <= 0 || slash === value.length - 1) return undefined;\n return { provider: value.slice(0, slash), id: value.slice(slash + 1) };\n}\n\n/**\n * Probe extensions whose own path names the provider first (the usual\n * `pi-<provider>-provider` package), so the common case runs one registration\n * function instead of every installed extension.\n */\nexport function orderProviderCandidates(\n paths: readonly string[],\n provider: string,\n): { named: string[]; rest: string[] } {\n const needle = provider.toLowerCase();\n const named: string[] = [];\n const rest: string[] = [];\n for (const candidate of paths) {\n const tail = candidate.split(/[\\\\/]+/).filter(Boolean).slice(-3);\n (tail.some((segment) => segment.toLowerCase().includes(needle)) ? named : rest).push(candidate);\n }\n return { named, rest };\n}\n\nconst realPath = (file: string): string => {\n try {\n return fs.realpathSync(file);\n } catch {\n return path.resolve(file);\n }\n};\n\n/** Fabric's own manifest entries: they never register chat providers. */\nexport function fabricExtensionEntries(moduleUrl = import.meta.url): string[] {\n const root = path.resolve(path.dirname(fileURLToPath(moduleUrl)), \"..\", \"..\");\n try {\n const manifest = JSON.parse(fs.readFileSync(path.join(root, \"package.json\"), \"utf8\")) as {\n name?: unknown;\n main?: unknown;\n pi?: { extensions?: unknown };\n };\n if (manifest.name !== \"pi-fabric\") return [];\n const entries = [manifest.main, ...(Array.isArray(manifest.pi?.extensions) ? manifest.pi.extensions : [])];\n return entries.filter((entry): entry is string => typeof entry === \"string\").map((entry) => path.resolve(root, entry));\n } catch {\n return [];\n }\n}\n\nconst emptyRegistrations = (): ExtensionProviderRegistrations => ({ configs: [], natives: [] });\n\n/**\n * Load extension paths with Pi's own loader into an isolated runtime and keep\n * only the requested provider's registrations. The loaded extensions are\n * discarded: their tools, commands, flags, shortcuts, renderers, and event\n * handlers never bind to the child session, and their event bus is private.\n */\nasync function loadProviderRegistrations(\n sdk: PiSdk,\n paths: readonly string[],\n provider: string,\n options: { cwd: string; agentDir: string; settingsManager: SettingsManager },\n failures: string[],\n): Promise<ExtensionProviderRegistrations> {\n if (paths.length === 0) return emptyRegistrations();\n const loader = new sdk.DefaultResourceLoader({\n cwd: options.cwd,\n agentDir: options.agentDir,\n settingsManager: options.settingsManager,\n noExtensions: true,\n additionalExtensionPaths: [...paths],\n noSkills: true,\n noPromptTemplates: true,\n noThemes: true,\n noContextFiles: true,\n });\n await loader.reload();\n const { runtime, errors } = loader.getExtensions();\n failures.push(...errors.map((error) => error.path));\n return {\n configs: runtime.pendingProviderRegistrations\n .filter((registration) => registration.name === provider)\n .map(({ name, config, extensionPath }) => ({ name, config, extensionPath })),\n natives: runtime.pendingNativeProviderRegistrations\n .filter((registration) => registration.provider.id === provider)\n .map(({ provider: registered, extensionPath }) => ({ provider: registered, extensionPath })),\n };\n}\n\n/**\n * Find the extension registrations that make `model` resolvable in a child\n * without its other extensions. Returns `undefined` when Pi already knows the\n * model (built in, models.json, or a cached catalog), so those children keep\n * their exact extension-free behaviour. Throws an actionable error when no\n * installed extension registers the provider.\n */\nexport async function resolveExtensionProviders(\n options: ResolveExtensionProvidersOptions,\n sdk?: PiSdk,\n): Promise<ExtensionProviderRegistrations | undefined> {\n const key = parseModelKey(options.model);\n if (!key) return undefined;\n const pi = sdk ?? await import(\"@earendil-works/pi-coding-agent\");\n const models = await pi.ModelRuntime.create({\n authPath: path.join(options.agentDir, \"auth.json\"),\n modelsPath: path.join(options.agentDir, \"models.json\"),\n refreshOnCreate: false,\n });\n if (models.getModel(key.provider, key.id)) return undefined;\n await models.refresh({ allowNetwork: false, signal: AbortSignal.timeout(15_000) });\n if (models.getModel(key.provider, key.id)) return undefined;\n\n // Same trust a headless child applies without consulting extension\n // project_trust handlers: those belong to the extensions left out here.\n const untrusted = pi.SettingsManager.create(options.cwd, options.agentDir, { projectTrusted: false });\n const trusted = !pi.hasTrustRequiringProjectResources(options.cwd) ||\n (new pi.ProjectTrustStore(options.agentDir).get(options.cwd) ?? untrusted.getDefaultProjectTrust() === \"always\");\n const settingsManager = trusted\n ? pi.SettingsManager.create(options.cwd, options.agentDir, { projectTrusted: true })\n : untrusted;\n const resolved = await new pi.DefaultPackageManager({\n cwd: options.cwd,\n agentDir: options.agentDir,\n settingsManager,\n }).resolve();\n const excluded = new Set((options.exclude ?? fabricExtensionEntries()).map(realPath));\n const candidates = resolved.extensions\n .filter((resource) => resource.enabled && !resource.path.startsWith(\"builtin:\"))\n .map((resource) => resource.path)\n .filter((candidate) => !excluded.has(realPath(candidate)));\n const { named, rest } = orderProviderCandidates(candidates, key.provider);\n const context = { cwd: options.cwd, agentDir: options.agentDir, settingsManager };\n const failures: string[] = [];\n for (const group of [named, rest]) {\n const found = await loadProviderRegistrations(pi, group, key.provider, context, failures);\n if (found.configs.length > 0 || found.natives.length > 0) return found;\n }\n throw new Error(\n `Model \"${options.model}\" needs provider \"${key.provider}\", which is not built into Pi or defined in models.json, ` +\n \"and no installed Pi extension registers it for this extensions: false child. \" +\n \"Install the extension that provides it, or run the child with extensions: true.\" +\n (failures.length > 0 ? ` Extensions that failed to load while searching: ${[...new Set(failures)].join(\", \")}.` : \"\"),\n );\n}\n\n/**\n * Loaded with `-e` into Pi children launched with `extensions: false`. It\n * registers only the requested model's provider, as its extension registered\n * it, so credentials still resolve in the child through auth.json, the\n * environment, or the credential store. Nothing else from that extension loads.\n */\nexport default async function fabricProviderBridge(pi: ExtensionAPI): Promise<void> {\n const model = process.env[PROVIDER_BRIDGE_MODEL_ENV]?.trim();\n if (!model) return;\n const { getAgentDir } = await import(\"@earendil-works/pi-coding-agent\");\n const found = await resolveExtensionProviders({ model, cwd: process.cwd(), agentDir: getAgentDir() });\n if (!found) return;\n for (const { name, config } of found.configs) pi.registerProvider(name, config);\n for (const { provider } of found.natives) pi.registerProvider(provider);\n}\n"],
|
|
5
|
-
"mappings": ";AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport type { Provider } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI, ProviderConfig, SettingsManager } from \"@earendil-works/pi-coding-agent\";\n\n/**\n * The `provider/id` key a Pi child launched with `extensions: false` must\n * resolve. The worker sets it only when it also loads this bridge with `-e`.\n */\nexport const PROVIDER_BRIDGE_MODEL_ENV = \"PI_FABRIC_EXTENSION_MODEL\";\n\nexport interface ExtensionProviderRegistrations {\n configs: Array<{ name: string; config: ProviderConfig; extensionPath: string }>;\n natives: Array<{ provider: Provider; extensionPath: string }>;\n}\n\nexport interface ResolveExtensionProvidersOptions {\n model: string;\n cwd: string;\n agentDir: string;\n /** Extension entry files never probed (Fabric's own entries by default). */\n exclude?: readonly string[];\n}\n\ntype PiSdk = typeof import(\"@earendil-works/pi-coding-agent\");\n\n/** Split a canonical `provider/id` key at its first slash. */\nexport function parseModelKey(key: string): { provider: string; id: string } | undefined {\n const value = key.trim();\n const slash = value.indexOf(\"/\");\n if (slash <= 0 || slash === value.length - 1) return undefined;\n return { provider: value.slice(0, slash), id: value.slice(slash + 1) };\n}\n\n/**\n * Probe extensions whose own path names the provider first (the usual\n * `pi-<provider>-provider` package), so the common case runs one registration\n * function instead of every installed extension.\n */\nexport function orderProviderCandidates(\n paths: readonly string[],\n provider: string,\n): { named: string[]; rest: string[] } {\n const needle = provider.toLowerCase();\n const named: string[] = [];\n const rest: string[] = [];\n for (const candidate of paths) {\n const tail = candidate.split(/[\\\\/]+/).filter(Boolean).slice(-3);\n (tail.some((segment) => segment.toLowerCase().includes(needle)) ? named : rest).push(candidate);\n }\n return { named, rest };\n}\n\nconst realPath = (file: string): string => {\n try {\n return fs.realpathSync(file);\n } catch {\n return path.resolve(file);\n }\n};\n\n/** A manifest entry that loads a Pi package Fabric bundles (`node_modules/<package>/...`). */\nconst isBundledPackageEntry = (entry: string): boolean =>\n entry.replace(/\\\\/g, \"/\").replace(/^(?:\\.\\/)+/, \"\").startsWith(\"node_modules/\");\n\n/**\n * Fabric's own manifest entries: they never register chat providers. Entries\n * of bundled Pi packages (pi-coralbricks-provider) are not Fabric's own: they\n * stay probe candidates, so the bundled provider resolves without loading\n * Fabric's main extension.\n */\nexport function fabricExtensionEntries(moduleUrl = import.meta.url): string[] {\n const root = path.resolve(path.dirname(fileURLToPath(moduleUrl)), \"..\", \"..\");\n try {\n const manifest = JSON.parse(fs.readFileSync(path.join(root, \"package.json\"), \"utf8\")) as {\n name?: unknown;\n main?: unknown;\n pi?: { extensions?: unknown };\n };\n if (manifest.name !== \"pi-fabric\") return [];\n const entries = [manifest.main, ...(Array.isArray(manifest.pi?.extensions) ? manifest.pi.extensions : [])];\n return entries\n .filter((entry): entry is string => typeof entry === \"string\" && !isBundledPackageEntry(entry))\n .map((entry) => path.resolve(root, entry));\n } catch {\n return [];\n }\n}\n\nconst emptyRegistrations = (): ExtensionProviderRegistrations => ({ configs: [], natives: [] });\n\n/**\n * Load extension paths with Pi's own loader into an isolated runtime and keep\n * only the requested provider's registrations. The loaded extensions are\n * discarded: their tools, commands, flags, shortcuts, renderers, and event\n * handlers never bind to the child session, and their event bus is private.\n */\nasync function loadProviderRegistrations(\n sdk: PiSdk,\n paths: readonly string[],\n provider: string,\n options: { cwd: string; agentDir: string; settingsManager: SettingsManager },\n failures: string[],\n): Promise<ExtensionProviderRegistrations> {\n if (paths.length === 0) return emptyRegistrations();\n const loader = new sdk.DefaultResourceLoader({\n cwd: options.cwd,\n agentDir: options.agentDir,\n settingsManager: options.settingsManager,\n noExtensions: true,\n additionalExtensionPaths: [...paths],\n noSkills: true,\n noPromptTemplates: true,\n noThemes: true,\n noContextFiles: true,\n });\n await loader.reload();\n const { runtime, errors } = loader.getExtensions();\n failures.push(...errors.map((error) => error.path));\n return {\n configs: runtime.pendingProviderRegistrations\n .filter((registration) => registration.name === provider)\n .map(({ name, config, extensionPath }) => ({ name, config, extensionPath })),\n natives: runtime.pendingNativeProviderRegistrations\n .filter((registration) => registration.provider.id === provider)\n .map(({ provider: registered, extensionPath }) => ({ provider: registered, extensionPath })),\n };\n}\n\n/**\n * Find the extension registrations that make `model` resolvable in a child\n * without its other extensions. Returns `undefined` when Pi already knows the\n * model (built in, models.json, or a cached catalog), so those children keep\n * their exact extension-free behaviour. Throws an actionable error when no\n * installed extension registers the provider.\n */\nexport async function resolveExtensionProviders(\n options: ResolveExtensionProvidersOptions,\n sdk?: PiSdk,\n): Promise<ExtensionProviderRegistrations | undefined> {\n const key = parseModelKey(options.model);\n if (!key) return undefined;\n const pi = sdk ?? await import(\"@earendil-works/pi-coding-agent\");\n const models = await pi.ModelRuntime.create({\n authPath: path.join(options.agentDir, \"auth.json\"),\n modelsPath: path.join(options.agentDir, \"models.json\"),\n refreshOnCreate: false,\n });\n if (models.getModel(key.provider, key.id)) return undefined;\n await models.refresh({ allowNetwork: false, signal: AbortSignal.timeout(15_000) });\n if (models.getModel(key.provider, key.id)) return undefined;\n\n // Same trust a headless child applies without consulting extension\n // project_trust handlers: those belong to the extensions left out here.\n const untrusted = pi.SettingsManager.create(options.cwd, options.agentDir, { projectTrusted: false });\n const trusted = !pi.hasTrustRequiringProjectResources(options.cwd) ||\n (new pi.ProjectTrustStore(options.agentDir).get(options.cwd) ?? untrusted.getDefaultProjectTrust() === \"always\");\n const settingsManager = trusted\n ? pi.SettingsManager.create(options.cwd, options.agentDir, { projectTrusted: true })\n : untrusted;\n const resolved = await new pi.DefaultPackageManager({\n cwd: options.cwd,\n agentDir: options.agentDir,\n settingsManager,\n }).resolve();\n const excluded = new Set((options.exclude ?? fabricExtensionEntries()).map(realPath));\n const candidates = resolved.extensions\n .filter((resource) => resource.enabled && !resource.path.startsWith(\"builtin:\"))\n .map((resource) => resource.path)\n .filter((candidate) => !excluded.has(realPath(candidate)));\n const { named, rest } = orderProviderCandidates(candidates, key.provider);\n const context = { cwd: options.cwd, agentDir: options.agentDir, settingsManager };\n const failures: string[] = [];\n for (const group of [named, rest]) {\n const found = await loadProviderRegistrations(pi, group, key.provider, context, failures);\n if (found.configs.length > 0 || found.natives.length > 0) return found;\n }\n throw new Error(\n `Model \"${options.model}\" needs provider \"${key.provider}\", which is not built into Pi or defined in models.json, ` +\n \"and no installed Pi extension registers it for this extensions: false child. \" +\n \"Install the extension that provides it, or run the child with extensions: true.\" +\n (failures.length > 0 ? ` Extensions that failed to load while searching: ${[...new Set(failures)].join(\", \")}.` : \"\"),\n );\n}\n\n/**\n * The SDK entry of the bundled Pi CLI running this process, if it is one. The\n * published `pi` bin is `dist/bundle/cli.js`; its `dist/bundle/index.js` SDK\n * entry is built from the same chunks the CLI has already evaluated.\n */\nexport function hostBundleSdkEntry(cliPath: string | undefined = process.argv[1]): string | undefined {\n if (!cliPath) return undefined;\n let cli: string;\n try {\n cli = fs.realpathSync(cliPath);\n } catch {\n return undefined;\n }\n const bundle = path.dirname(cli);\n if (path.basename(cli) !== \"cli.js\" || path.basename(bundle) !== \"bundle\") return undefined;\n const entry = path.join(bundle, \"index.js\");\n try {\n const manifest = JSON.parse(fs.readFileSync(path.join(bundle, \"..\", \"..\", \"package.json\"), \"utf8\")) as { name?: unknown };\n return manifest.name === \"@earendil-works/pi-coding-agent\" && fs.existsSync(entry) ? entry : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * The host's own SDK. Pi imports this bridge natively (it is ESM JavaScript),\n * so jiti's virtual modules do not apply, and a bare package import from here\n * evaluates the separate unbundled SDK: about 1,800 more modules in the\n * bundled CLI. Besides the startup cost, that CLI enables Node's compile cache,\n * which serializes every one of those modules on exit; with a cold cache that\n * held a Windows child past the worker's exit grace after it had settled.\n * The bundle's SDK entry shares the CLI's already loaded chunks instead.\n */\nexport async function loadHostSdk(cliPath: string | undefined = process.argv[1]): Promise<PiSdk> {\n const entry = hostBundleSdkEntry(cliPath);\n if (entry) {\n try {\n return await import(pathToFileURL(entry).href) as PiSdk;\n } catch {\n // Unexpected bundle layout: fall back to the package entry.\n }\n }\n return import(\"@earendil-works/pi-coding-agent\");\n}\n\n/**\n * Loaded with `-e` into Pi children launched with `extensions: false`. It\n * registers only the requested model's provider, as its extension registered\n * it, so credentials still resolve in the child through auth.json, the\n * environment, or the credential store. Nothing else from that extension loads.\n */\nexport default async function fabricProviderBridge(pi: ExtensionAPI): Promise<void> {\n const model = process.env[PROVIDER_BRIDGE_MODEL_ENV]?.trim();\n if (!model) return;\n const sdk = await loadHostSdk();\n const found = await resolveExtensionProviders({ model, cwd: process.cwd(), agentDir: sdk.getAgentDir() }, sdk);\n if (!found) return;\n for (const { name, config } of found.configs) pi.registerProvider(name, config);\n for (const { provider } of found.natives) pi.registerProvider(provider);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/docs/configuration.md
CHANGED
|
@@ -338,16 +338,34 @@ Credentials stay with Pi. Fabric hands a child agent only the model key, and the
|
|
|
338
338
|
|
|
339
339
|
With `extensions: false` (per call, or as `agents.extensions`), a Pi child loads none of the user's extensions, so it gets the native tool surface without their tools, commands, or hooks. A model that comes from an extension still resolves. A small Fabric bridge loads the extension that registers the requested provider in an isolated runtime, keeps only that provider registration, and discards every tool, command, flag, renderer, and event handler the extension registered. Fabric first tries extensions whose path names the provider, then the other installed extensions; each tried extension's registration function runs once, and its event handlers never run. Project-local extensions count only in a project that Pi already trusts. Models built into Pi or defined in `models.json` skip the bridge entirely. When no installed extension registers the provider, the child fails before the task is sent, with an error that names the provider and suggests `extensions: true`. Strict model admission is unchanged. This covers both Pi runners (`pi-durable` and `pi`). Provider code that depends on its extension's session hooks, and virtual models routed with `pi.registerVirtualModel()`, still need `extensions: true`. Claude, Veda, and custom runners use their own model catalogs and are unaffected.
|
|
340
340
|
|
|
341
|
-
###
|
|
341
|
+
### Bundled Coral Bricks provider
|
|
342
342
|
|
|
343
|
-
|
|
343
|
+
Fabric bundles the Coral Bricks provider extension, [`pi-coralbricks-provider`](https://www.npmjs.com/package/pi-coralbricks-provider), so `coralbricks/*` models are available after installing Fabric alone. Disclosure: Fabric's maintainers work at Coral Bricks.
|
|
344
|
+
|
|
345
|
+
Add a key once, in either form:
|
|
344
346
|
|
|
345
347
|
```sh
|
|
346
|
-
|
|
347
|
-
export CORALBRICKS_API_KEY=...
|
|
348
|
+
/login coralbricks # inside Pi; stores the key in auth.json
|
|
349
|
+
export CORALBRICKS_API_KEY=... # or in the environment
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The provider registers keys such as `coralbricks/glm-5.3-fast` and `coralbricks/deepseek-v4.1-flash-fast`. Without a key it serves the model list shipped in the package and makes no network request. With a key it refreshes that list from Coral's API at session start and after `/login`. It loads as its own entry in Fabric's package (`node_modules/pi-coralbricks-provider/index.ts`), outside Fabric's startup graph, so `extensions: false` children resolve `coralbricks/*` models through the bridge above without loading Fabric's main extension.
|
|
353
|
+
|
|
354
|
+
You can also install `pi-coralbricks-provider` yourself, for example to pick up a newer release before Fabric bundles it. When both copies load, the newest version registers the provider and the other one stays idle, so `/model` lists each model once.
|
|
355
|
+
|
|
356
|
+
To leave the bundled provider out, replace the plain `npm:pi-fabric` entry in `~/.pi/agent/settings.json` (or a project's `.pi/settings.json`) with the package object form and exclude its path:
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
{
|
|
360
|
+
"packages": [
|
|
361
|
+
{ "source": "npm:pi-fabric", "extensions": ["!node_modules/pi-coralbricks-provider/**"] }
|
|
362
|
+
]
|
|
363
|
+
}
|
|
348
364
|
```
|
|
349
365
|
|
|
350
|
-
|
|
366
|
+
Fabric's own extension keeps loading. Use the same `source` you installed from (`git:github.com/fabric-runtime/pi-fabric` or a local path). After opting out, `coralbricks/*` models resolve only if you install the provider separately, and an `extensions: false` child that asks for one fails before the task is sent.
|
|
367
|
+
|
|
368
|
+
Use the Coral Bricks keys wherever Fabric takes a model, for example in aliases that mix providers:
|
|
351
369
|
|
|
352
370
|
```json
|
|
353
371
|
{
|
|
@@ -371,6 +389,10 @@ const [coral, gemini] = await Promise.all([
|
|
|
371
389
|
return { coral: coral.text, gemini: gemini.text };
|
|
372
390
|
```
|
|
373
391
|
|
|
392
|
+
### Other providers
|
|
393
|
+
|
|
394
|
+
Every other provider works the same way. Install its Pi extension with `pi install`, or define it in Pi's `models.json`, and its `provider/id` keys work in each place listed at the start of this section, including `extensions: false` children.
|
|
395
|
+
|
|
374
396
|
## Result formatting
|
|
375
397
|
|
|
376
398
|
`executor.resultFormat` sets the default for `fabric_exec` return values. Find it under `/fabric settings` → **Executor**. `"auto"` keeps strings as text and renders structured values as syntax-highlighted YAML. `"yaml"`, `"json"`, and `"text"` each force their named behavior. A call-level `resultFormat` parameter overrides the configured default.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🪸 pi-coralbricks-provider
|
|
4
|
+
|
|
5
|
+
**GLM 5.3 & DeepSeek V4.1 Flash through [Coral Bricks](https://www.coralbricks.ai)**
|
|
6
|
+
|
|
7
|
+
_A [pi](https://github.com/earendil-works/pi-coding-agent) provider extension for Coral's OpenAI-compatible inference gateway — up to **1M context** on open models._
|
|
8
|
+
|
|
9
|
+
[](https://github.com/earendil-works/pi-coding-agent)
|
|
10
|
+
[](https://www.npmjs.com/package/pi-coralbricks-provider)
|
|
11
|
+
[](./LICENSE)
|
|
12
|
+
[](https://github.com/synthetic-lab/synbad)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
## Pi 1.0 compatibility
|
|
17
|
+
|
|
18
|
+
Tested with Pi **1.0.0**. Host-provided packages remain wildcard peers; development uses exact SDK pins.
|
|
19
|
+
Run `bun run test:pi` for offline manifest, catalog, lifecycle and streaming checks. To test an installed host, set `PI1_HOST_PACKAGE` to its package directory; add `PI1_HOST_ENTRY=bundle` for its bundled CLI runtime. The probe stubs all network requests and never uses a live provider endpoint.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **2 reasoning models** from Coral's live catalog — GLM 5.3 and DeepSeek V4.1 Flash (the `-fp4` twin slugs Coral still serves are hidden as duplicates)
|
|
26
|
+
- **1M token context** on GLM and DeepSeek, with vision (image input) on DeepSeek V4.1 Flash
|
|
27
|
+
- **OpenAI-compatible API** — standard `/v1/chat/completions`, streaming, and tool calling
|
|
28
|
+
- **Per-family thinking levels** — zai-style `thinking` control for GLM (including a *real* off switch), `reasoning_effort` for DeepSeek V4.1 Flash
|
|
29
|
+
- **Accurate cost tracking** — input, cache-write and output rates mirror Coral's [published pricing](https://www.coralbricks.ai/pricing), and cached reads are **$0 on every model**
|
|
30
|
+
- **Self-healing model sync** — stale-while-revalidate from the authenticated `/v1/models`, hot-swapped at session start and after `/login`. Without a key it makes no network request and serves the cached or embedded catalog
|
|
31
|
+
- **Quiet startup sync** — startup never waits on the network, and the catalog is refreshed at most hourly, by one pi process at a time, so `/new`, `pi -p` and fan-out child agents share one cache (see [Model Sync](#model-sync))
|
|
32
|
+
- **Safe to install twice** — when a standalone install and a copy bundled in another Pi package (such as [pi-fabric](https://github.com/fabric-runtime/pi-fabric)) load together, only the newest copy registers the provider (see [Installed more than once](#installed-more-than-once))
|
|
33
|
+
- **synbad-validated** — [synbad](https://github.com/synthetic-lab/synbad) tool-calling and reasoning-parsing evals pass 13/13 in every run on GLM 5.3 in both unary and streaming modes
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
### Option 1: Using `pi install` (Recommended)
|
|
38
|
+
|
|
39
|
+
Install directly from GitHub:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pi install https://github.com/monotykamary/pi-coralbricks-provider
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
or from npm:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pi install npm:pi-coralbricks-provider
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then set your API key and run pi:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Recommended: run /login coralbricks inside pi (saves it to auth.json)
|
|
55
|
+
# See Authentication section below
|
|
56
|
+
|
|
57
|
+
# Or set as environment variable
|
|
58
|
+
export CORALBRICKS_API_KEY=cb_your-key-here
|
|
59
|
+
|
|
60
|
+
pi
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Option 2: Manual Clone
|
|
64
|
+
|
|
65
|
+
1. Clone this repository:
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/monotykamary/pi-coralbricks-provider.git
|
|
68
|
+
cd pi-coralbricks-provider
|
|
69
|
+
bun install
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
2. Set your Coral API key:
|
|
73
|
+
```bash
|
|
74
|
+
# Recommended: run /login coralbricks inside pi (saves it to auth.json)
|
|
75
|
+
# See Authentication section below
|
|
76
|
+
|
|
77
|
+
# Or set as environment variable
|
|
78
|
+
export CORALBRICKS_API_KEY=cb_your-key-here
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
3. Run pi with the extension:
|
|
82
|
+
```bash
|
|
83
|
+
pi -e /path/to/pi-coralbricks-provider
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Installed more than once
|
|
87
|
+
|
|
88
|
+
Another Pi package can bundle this extension; [pi-fabric](https://github.com/fabric-runtime/pi-fabric) does. You can still install it yourself, for example to get a newer version first. When two or more copies load into one Pi session, the newest version registers `coralbricks`, syncs models and serves `/coralbricks-settings`. The other copies register nothing and make no requests, so `/model` lists each model once and `/v1/models` is fetched once. Copies of the same version keep the one that loaded first.
|
|
89
|
+
|
|
90
|
+
The copies find each other through a process-global registry (`Symbol.for("pi-coralbricks-provider.registry.v1")`), scoped to the Pi extension runtime through the event bus Pi shares among that runtime's extensions. The newest copy wins whichever order Pi loads them in. If it loads second, it takes over before the session starts, and Pi serves its registration because a provider registered later under the same id replaces the earlier one. Ownership never moves back, and it ends with the session, so `/reload`, `/new`, `/resume` and `/fork` start from a clean slate. Versions before 1.0.32 do not take part and always register.
|
|
91
|
+
|
|
92
|
+
## Available Models
|
|
93
|
+
|
|
94
|
+
| Model | Context | Vision | Reasoning | Input $/M | Cache Read $/M | Cache Write $/M | Output $/M |
|
|
95
|
+
|-------|---------|--------|-----------|-----------|-----------------|------------------|------------|
|
|
96
|
+
| DeepSeek V4.1 Flash | 1.0M | ✅ | ✅ | $0.01 | — | $0.09 | $1.20 |
|
|
97
|
+
| GLM 5.3 | 1.0M | ❌ | ✅ | $1.12 | — | $1.68 | $4.40 |
|
|
98
|
+
|
|
99
|
+
*Costs are per million tokens. Cache Read shows — because Coral bills cached input at **$0** on every model. Prompt tokens Coral has not cached yet are billed once at the Cache Write rate, in place of the Input rate. Prices subject to change — check [Coral's live catalog](https://www.coralbricks.ai/api/public/models).*
|
|
100
|
+
|
|
101
|
+
## Usage
|
|
102
|
+
|
|
103
|
+
After loading the extension, use the `/model` command in pi to select your preferred model:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
/model coralbricks glm-5.3-fp4
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Or start pi directly with a Coral model:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pi -e /path/to/pi-coralbricks-provider --model coralbricks/deepseek-v4.1-flash-fast-fp4:high
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Thinking levels attach to the model id with `:<level>` — e.g. `:low`, `:high`, `:max`, or `:off` (GLM and DeepSeek).
|
|
116
|
+
|
|
117
|
+
## Provider Settings
|
|
118
|
+
|
|
119
|
+
Run `/coralbricks-settings` to choose the **API surface** and, for Responses,
|
|
120
|
+
whether to **park turns**. The searchable,
|
|
121
|
+
bordered settings list matches pi's settings UI; GUI/RPC clients get a selection
|
|
122
|
+
dialog. Changes are saved immediately and apply to the next request without a
|
|
123
|
+
restart, including when a CoralBricks model is already selected.
|
|
124
|
+
|
|
125
|
+
Settings live at `~/.pi/agent/extensions/coralbricks.json` (or under
|
|
126
|
+
`PI_CODING_AGENT_DIR` when set):
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{ "api": "responses", "park": false }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- **`chat-completions`** (default): uses `/v1/chat/completions`; existing behavior
|
|
133
|
+
is unchanged. Select this to opt back out.
|
|
134
|
+
- **`responses`** (opt-in): uses `/v1/responses` with pi's native text, reasoning,
|
|
135
|
+
tool-call, image, usage, and cancellation handling. The same model IDs and API
|
|
136
|
+
key work on both surfaces. Thinking levels map to `reasoning.effort` here.
|
|
137
|
+
|
|
138
|
+
By default, Responses requests replay the full local conversation and send
|
|
139
|
+
`store: false`. This integration does not use background jobs or resumable
|
|
140
|
+
streams. Coral's broader Responses API is documented at
|
|
141
|
+
[coralbricks.ai/docs](https://www.coralbricks.ai/docs).
|
|
142
|
+
|
|
143
|
+
### Parking (`"park": true`)
|
|
144
|
+
|
|
145
|
+
With parking on, every Responses request is sent with `store: true` and the
|
|
146
|
+
next request continues it with `previous_response_id`. pi still builds the full
|
|
147
|
+
transcript, but only the items after the parked response (tool results, the
|
|
148
|
+
next user message) go over the wire. Tools are re-sent every time because Coral
|
|
149
|
+
does not carry them across a chain.
|
|
150
|
+
|
|
151
|
+
A turn is chained only when the full input starts with exactly what was sent
|
|
152
|
+
for the parked response, followed by that response's own output. Compaction, a
|
|
153
|
+
model switch, an edited or branched history, or a pi restart falls back to a
|
|
154
|
+
full replay, and that request is parked again. If Coral no longer has the parent
|
|
155
|
+
(`previous_response_not_found`), the same request is re-sent once in full.
|
|
156
|
+
Parked ids live in memory only.
|
|
157
|
+
|
|
158
|
+
Things to know before turning it on:
|
|
159
|
+
|
|
160
|
+
- Coral keeps every parked response on its side. Deleting a parent breaks the
|
|
161
|
+
chain, so the extension does not delete them.
|
|
162
|
+
- Coral bills a chained request as the tool definitions plus the new items. The
|
|
163
|
+
first chained request in a session writes the tool block to cache once, and
|
|
164
|
+
later ones read it back. With a warm cache, a 7-request tool loop cost about
|
|
165
|
+
the same as a full replay on DeepSeek V4.1 Flash and about a third more on
|
|
166
|
+
GLM 5.3 Flash. The parked history itself is not billed again, which matters
|
|
167
|
+
when Coral evicts a long session's cache.
|
|
168
|
+
|
|
169
|
+
Live probes verified text, automatic tool calls, tool-result replay, and switching
|
|
170
|
+
back to Chat Completions on all current models. The gateway currently rejects
|
|
171
|
+
Responses requests that force a named tool (`tool_choice: {"type":"function",
|
|
172
|
+
"name":"..."}`); leave tool selection automatic (pi's default). Errors are surfaced
|
|
173
|
+
without silently falling back to a different API.
|
|
174
|
+
|
|
175
|
+
Missing, invalid, or unreadable settings default to Chat Completions. Unknown
|
|
176
|
+
JSON fields are preserved when toggling; malformed files must be fixed before
|
|
177
|
+
saving. If editing the file manually, run `/reload` to apply it.
|
|
178
|
+
|
|
179
|
+
## Authentication
|
|
180
|
+
|
|
181
|
+
The Coral API key can be configured in multiple ways (resolved in this order):
|
|
182
|
+
|
|
183
|
+
1. **Runtime override** — Use the `--api-key` CLI flag
|
|
184
|
+
2. **`auth.json`** (recommended) — Run `/login coralbricks` inside pi and paste your key; pi saves it to `~/.pi/agent/auth.json` and refreshes the model list from the authenticated `/v1/models`. Or add it by hand:
|
|
185
|
+
```json
|
|
186
|
+
{ "coralbricks": { "type": "api_key", "key": "cb_your-key" } }
|
|
187
|
+
```
|
|
188
|
+
The `key` field supports literal values, env var names, and shell commands (prefix with `!`). `/logout` (pick CoralBricks) removes the stored key; `CORALBRICKS_API_KEY` is left alone.
|
|
189
|
+
3. **Environment variable** — Set `CORALBRICKS_API_KEY`
|
|
190
|
+
|
|
191
|
+
With [localterm](https://www.npmjs.com/package/localterm), store it once and it's exposed everywhere:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
localterm secret set coralbricks_api_key
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Environment Variables
|
|
198
|
+
|
|
199
|
+
| Variable | Required | Description |
|
|
200
|
+
|----------|----------|-------------|
|
|
201
|
+
| `CORALBRICKS_API_KEY` | No | Your Coral API key (fallback if not in `auth.json` or `--api-key`) |
|
|
202
|
+
| `PI_PROVIDER_SYNC_TTL_MS` | No | Model sync freshness window in ms (default `3600000`; `0` = revalidate every session start) |
|
|
203
|
+
|
|
204
|
+
> Coral Inference is currently in a design-partner program — mint keys at [coralbricks.ai/api-keys](https://www.coralbricks.ai/api-keys). Newly-minted keys may take ~30 seconds to be honored, and `403 access_denied` means the account isn't on the allowlist yet.
|
|
205
|
+
|
|
206
|
+
## Thinking Levels
|
|
207
|
+
|
|
208
|
+
Verified against the live gateway:
|
|
209
|
+
|
|
210
|
+
| Model | Format | off | low | medium | high | max |
|
|
211
|
+
|-------|--------|-----|-----|--------|------|-----|
|
|
212
|
+
| GLM 5.3 FP4 | `thinking: {type}` + `reasoning_effort` | ✅ | ✅ | — | ✅ | ✅ |
|
|
213
|
+
| GLM 5.3 Flash (retired) | `thinking: {type}` + `reasoning_effort` | ✅ | ✅ | — | ✅ | ✅ |
|
|
214
|
+
| DeepSeek V4.1 Flash | `reasoning_effort` | ✅ | ✅ | — | ✅ | ✅ |
|
|
215
|
+
|
|
216
|
+
- **GLM** accepts zai-style `thinking: {type: "disabled"}` on Coral — pi's *off* level turns thinking off, though a short preamble of a few dozen tokens can still appear (the upstream Z.ai API has no off at all, so this differs from the canonical Z.ai map).
|
|
217
|
+
- **DeepSeek V4.1 Flash** reasons only when asked: a request without `reasoning_effort`, or with `"none"`, gets no reasoning, which is what pi's *off* sends. `thinking: {type}` has no effect on this model.
|
|
218
|
+
- Coral streams a duplicate `reasoning` field alongside `reasoning_content`; pi dedupes these automatically.
|
|
219
|
+
|
|
220
|
+
## Compat Settings
|
|
221
|
+
|
|
222
|
+
Coral's gateway follows the OpenAI Chat Completions API:
|
|
223
|
+
|
|
224
|
+
- **`supportsStore: false`** / **`supportsDeveloperRole: false`** — all models; Coral serves open models on the classic roles.
|
|
225
|
+
- **`maxTokensField: "max_tokens"`** — all models.
|
|
226
|
+
- **`thinkingFormat: "zai"`** — GLM 5.3: `thinking: {type: "enabled"|"disabled"}` toggles reasoning, `reasoning_effort` picks the depth.
|
|
227
|
+
- **`thinkingFormat: "openai"`** — DeepSeek V4.1 Flash: `reasoning_effort` drives thinking depth.
|
|
228
|
+
|
|
229
|
+
### Patch Overrides & Custom Models
|
|
230
|
+
|
|
231
|
+
- **`patch.json`** — per-model overrides applied on top of `models.json` (reasoning flags, pricing corrections, compat settings, thinking level maps, `"hidden"`). One entry per model Coral serves: GLM 5.3 and DeepSeek V4.1 Flash carry the reasoning and compat settings, and their `-fp4` twins set `"hidden": true` so `/model` and the table above list each model once.
|
|
232
|
+
- **`custom-models.json`** — full model definitions for models Coral doesn't list. Merged after patch.
|
|
233
|
+
|
|
234
|
+
Merge order: `[live|cache|embedded] → patch.json → custom-models.json`
|
|
235
|
+
|
|
236
|
+
## Inference-Quality Testing
|
|
237
|
+
|
|
238
|
+
Validated with [synbad](https://github.com/synthetic-lab/synbad) — Synthetic's tool-calling and reasoning-parsing eval suite for LLM inference providers (current 13-eval suite, `--reasoning-effort high`, five `--count 1` runs per mode on 2026-09-15, DeepSeek on 2026-09-22; a cell shows the evals that passed in every run):
|
|
239
|
+
|
|
240
|
+
| Model | Unary | Stream | Notes |
|
|
241
|
+
|-------|-------|--------|-------|
|
|
242
|
+
| GLM 5.3 FP4 | 13/13 ✅ | 13/13 ✅ | 5/5 runs |
|
|
243
|
+
| GLM 5.3 Flash (retired) | 12/13 ⚠️ | 12/13 ⚠️ | `reasoning/reasoning-parsing` passes 4 of 5 runs; see below |
|
|
244
|
+
| GPT-OSS 120B (retired) | 12/13 ⚠️ | 12/13 ⚠️ | `tools/parallel-tool` 0 of 5 runs, before Coral retired the model |
|
|
245
|
+
| DeepSeek V4.1 Flash | 12/13 ⚠️ | 12/13 ⚠️ | `tools/octo-list-no-optional-args` passes 1 of 5 runs; see below |
|
|
246
|
+
| Kimi K3 (retired) | 13/13 ✅ | 13/13 ✅ | 5/5 runs, before Coral retired the model |
|
|
247
|
+
| GLM 5.2 FP4 (retired) | 15/15 ✅ | 15/15 ✅ | 15-eval suite, single run, before Coral retired the model |
|
|
248
|
+
|
|
249
|
+
The misses are model-side, not transport: gpt-oss answered "Paris and London" with one call after another instead of two parallel calls even when `parallel_tool_calls: true`, in every run; on the trivial prompt `reasoning-parsing` uses, GLM 5.3 Flash answers without a think block about one run in five, in both modes, with the answer itself unaffected; and asked to "call the list tool with no args", DeepSeek V4.1 Flash usually fills in the optional `dirPath` as `"."`.
|
|
250
|
+
|
|
251
|
+
Reproduce:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
CORALBRICKS_API_KEY=cb_... node ../synbad/dist/source/index.js eval \
|
|
255
|
+
--env-var CORALBRICKS_API_KEY \
|
|
256
|
+
--base-url https://inference.coralbricks.ai/v1 \
|
|
257
|
+
--model glm-5.3-fp4 --count 1 --reasoning-effort high
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Model Sync
|
|
261
|
+
|
|
262
|
+
At startup the extension serves the last catalog it cached (`~/.pi/agent/cache/coralbricks-models.json`), or the catalog embedded in the package. Once Pi resolves a key (`/login coralbricks`, `auth.json`, `--api-key` or `CORALBRICKS_API_KEY`), it refreshes from the authenticated `/v1/models` at session start and after `/login`, caches the result and swaps it in. With no key it makes no network request at all.
|
|
263
|
+
|
|
264
|
+
Startup never waits on the network: the session-start refresh runs in the background, and only when:
|
|
265
|
+
|
|
266
|
+
- a Coral API key is configured (no key, no request),
|
|
267
|
+
- the cache is older than 1 hour, missing or unreadable, was fetched with a different API key, or was written by a version before this sync policy, and
|
|
268
|
+
- no other pi process is already refreshing it (a `coralbricks-models.lock` file next to the cache).
|
|
269
|
+
|
|
270
|
+
So `/new`, `/reload`, `pi -p` and fan-out child agents share one cache instead of each calling the API; a session that skips the fetch still picks up a cache another process refreshed, and every cache write is atomic. `/login coralbricks` and a changed key always refresh, whatever the cache's age. A failed refresh is retried after 5 minutes at the earliest. `PI_PROVIDER_SYNC_TTL_MS` changes the window (`0` revalidates on every session start). Only a hash fingerprint of the key is stored (`coralbricks-models.sync.json`), never the key itself. When several copies of the extension load (see [Installed more than once](#installed-more-than-once)), only the owning copy touches the lock or the network.
|
|
271
|
+
|
|
272
|
+
## Updating Models
|
|
273
|
+
|
|
274
|
+
Run the update script to fetch the latest models from Coral's API:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
export CORALBRICKS_API_KEY=cb_your-key
|
|
278
|
+
node scripts/update-models.js
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
This will:
|
|
282
|
+
1. Fetch models from `https://inference.coralbricks.ai/v1/models` (falls back to the unauthenticated [public catalog](https://www.coralbricks.ai/api/public/models) without a key)
|
|
283
|
+
2. Preserve existing model data (pricing, compat, thinking maps) for known models
|
|
284
|
+
3. Apply overrides from `patch.json`
|
|
285
|
+
4. Update `models.json` and the README model table
|
|
286
|
+
|
|
287
|
+
To regenerate just the README model table from local data — no API key needed:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
node scripts/update-models.js --readme-only
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Troubleshooting
|
|
294
|
+
|
|
295
|
+
| Symptom | Meaning |
|
|
296
|
+
|---------|---------|
|
|
297
|
+
| `403 access_denied` | Account not on the Coral Inference allowlist yet |
|
|
298
|
+
| `404 model_not_accepted` | Model id not enabled for your key |
|
|
299
|
+
| `401 invalid_api_key` | Re-mint at [coralbricks.ai/api-keys](https://www.coralbricks.ai/api-keys); fresh keys take ~30s to activate |
|
|
300
|
+
| `429 rate_limit_exceeded` | Per-key rate limit — retry with backoff |
|
|
301
|
+
| `502 upstream_error` / `503 backend_unconfigured` | Transient — retry |
|
|
302
|
+
| `504 timeout` | Sync request waited too long; re-issue smaller or use Coral's background Responses API |
|
|
303
|
+
|
|
304
|
+
## License
|
|
305
|
+
|
|
306
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { envApiKeyAuth, type ApiKeyAuth } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
export const CORALBRICKS_ENV_VAR = "CORALBRICKS_API_KEY";
|
|
4
|
+
export const CORALBRICKS_KEYS_URL = "https://www.coralbricks.ai/api-keys";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* API-key auth for `/login coralbricks`, the pattern Pi's built-in OpenRouter and
|
|
8
|
+
* Vercel AI Gateway providers use (pi-ai `envApiKeyAuth`): a stored auth.json
|
|
9
|
+
* `coralbricks` api_key wins, then CORALBRICKS_API_KEY. Login stores
|
|
10
|
+
* `{ "type": "api_key", "key": ... }` under `coralbricks`, the entry this
|
|
11
|
+
* extension has always read, and `/logout` removes it.
|
|
12
|
+
*/
|
|
13
|
+
export function coralApiKeyAuth(): ApiKeyAuth {
|
|
14
|
+
const base = envApiKeyAuth("CoralBricks API key", [CORALBRICKS_ENV_VAR]);
|
|
15
|
+
return {
|
|
16
|
+
...base,
|
|
17
|
+
login: async (interaction) => {
|
|
18
|
+
interaction.signal.throwIfAborted();
|
|
19
|
+
interaction.notify({
|
|
20
|
+
type: "info",
|
|
21
|
+
message: "Create a Coral Inference API key at https://www.coralbricks.ai (API keys page). New keys can take ~30 seconds to be honored.",
|
|
22
|
+
links: [{ url: CORALBRICKS_KEYS_URL, label: "coralbricks.ai/api-keys" }],
|
|
23
|
+
});
|
|
24
|
+
const key = (await interaction.prompt({ type: "secret", message: "Enter CoralBricks API key", placeholder: "cb_..." })).trim();
|
|
25
|
+
interaction.signal.throwIfAborted();
|
|
26
|
+
if (!key) throw new Error("No CoralBricks API key entered");
|
|
27
|
+
return { type: "api_key", key };
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|