@sigrun/provider-pareto 0.0.0-stage → 0.1.1
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/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +92 -2
- package/dist/index.d.ts +114 -0
- package/dist/index.js +251 -0
- package/package.json +50 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.1] - 2026-10-27
|
|
9
|
+
### Changed
|
|
10
|
+
- Re-released through CI trusted publishing; this version carries SLSA
|
|
11
|
+
provenance. No functional change from 0.1.0.
|
|
12
|
+
|
|
13
|
+
## [Unreleased]
|
|
14
|
+
|
|
15
|
+
## [0.1.0] - 2026-10-18
|
|
16
|
+
### Changed
|
|
17
|
+
- Renamed to `@sigrun/provider-pareto` for the 0.1.0 release (GitHub
|
|
18
|
+
repo `sigrun-ai/sigrun-provider-pareto`): published package metadata
|
|
19
|
+
(`publishConfig` access/provenance), host modules
|
|
20
|
+
(`@earendil-works/pi-ai`, `@earendil-works/pi-durable`) moved to
|
|
21
|
+
`peerDependencies` mirrored in `devDependencies` at exact `1.3.0`,
|
|
22
|
+
CHANGELOG.md joined the files whitelist, and a manifest hygiene check
|
|
23
|
+
wired into `npm test`.
|
|
24
|
+
### Added
|
|
25
|
+
- Sigrun provider plugin: Pareto paygo
|
|
26
|
+
(`https://api.paretoinference.com/v1`, live-verified 2026-10-18) as
|
|
27
|
+
one dynamic OpenAI-compatible provider (`provider: "pareto"` stamped
|
|
28
|
+
on every catalog entry so persisted catalogs survive cold-boot
|
|
29
|
+
restore).
|
|
30
|
+
- Provider-only contributor factory: `{ name: "provider-pareto",
|
|
31
|
+
providers: [{ provider, readiness }], health }` — no pi-durable
|
|
32
|
+
extension surface.
|
|
33
|
+
- Deployment-configurable base URL: the async factory reads `baseUrl`
|
|
34
|
+
from the plugin's scoped settings with the constant as fallback; a
|
|
35
|
+
malformed override fails the factory loudly instead of silently
|
|
36
|
+
falling back.
|
|
37
|
+
- Credential resolution through sigrun's auth.json bridge with
|
|
38
|
+
`PARETO_API_KEY` as the ambient fallback (bridge credential always
|
|
39
|
+
wins).
|
|
40
|
+
- Three-state readiness tracker (`not_configured` / `degraded` /
|
|
41
|
+
`ready`) seeded from credential resolution and updated per refresh
|
|
42
|
+
outcome; `console.warn` on state transitions only. `health()` maps
|
|
43
|
+
`not_configured` to `"ok"` and configured-and-failing to
|
|
44
|
+
`"degraded"`.
|
|
45
|
+
- `fetchModels` throws on unconfigured and on failure — never returns
|
|
46
|
+
`[]` (an empty array would publish and wipe the persisted catalog).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael MacLeod
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sigrun/provider-pareto
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A [sigrun](https://github.com/sigrun-ai) provider plugin:
|
|
4
|
+
[Pareto](https://paretoinference.com) paygo (OpenAI-compatible
|
|
5
|
+
inference) as one dynamic chat provider.
|
|
6
|
+
|
|
7
|
+
## What it contributes
|
|
8
|
+
|
|
9
|
+
A **provider-only contributor** — the default export is a factory
|
|
10
|
+
returning `{ name: "provider-pareto", providers: [{ provider, readiness }],
|
|
11
|
+
health }` with no pi-durable extension surface. Sigrun's loader merges
|
|
12
|
+
the provider into its `Models` store at boot.
|
|
13
|
+
|
|
14
|
+
- **Provider** `pareto` — dynamic catalog fetched from `<base>/models`
|
|
15
|
+
(OpenAI `[OI]` list shape; entries also carry a per-token dollar-string
|
|
16
|
+
`pricing` object this plugin does not map — cost stays zeroed and
|
|
17
|
+
structural) over `openai-completions`; boots with an empty baseline
|
|
18
|
+
and publishes the fetched catalog.
|
|
19
|
+
- **Readiness** (`not_configured` / `degraded` / `ready`, surfaced on
|
|
20
|
+
sigrun's `/v1/auth/check`) — seeded from credential resolution and
|
|
21
|
+
updated per refresh outcome; state transitions log via `console.warn`
|
|
22
|
+
(transitions only, never per failure). On the first configured boot
|
|
23
|
+
the provider announces `startup → ready` at credential *resolution* —
|
|
24
|
+
before any refresh has run (`ready` there means "credential resolved,
|
|
25
|
+
refresh not yet attempted", not "catalog verified").
|
|
26
|
+
- **Health** — `degraded` only when the provider is configured and its
|
|
27
|
+
last refresh failed; an unconfigured optional plugin reports `ok`.
|
|
28
|
+
|
|
29
|
+
## Base URL
|
|
30
|
+
|
|
31
|
+
The default endpoint constant is `https://api.paretoinference.com/v1`
|
|
32
|
+
(live-verified 2026-10-18). Pareto's documented base URL is a
|
|
33
|
+
deployment-specific value, so the factory reads the plugin's scoped
|
|
34
|
+
settings first — `baseUrl` (string) in `settings/provider-pareto.json`
|
|
35
|
+
overrides the constant:
|
|
36
|
+
|
|
37
|
+
```jsonc
|
|
38
|
+
// settings/provider-pareto.json (in the sigrun config dir)
|
|
39
|
+
{ "baseUrl": "https://your-pareto-endpoint.example/v1" }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
An absent `baseUrl` uses the constant; a present-but-malformed one
|
|
43
|
+
(empty or not a string) fails the factory loudly (the extension
|
|
44
|
+
quarantines with the reason) rather than silently falling back.
|
|
45
|
+
|
|
46
|
+
## Credentials
|
|
47
|
+
|
|
48
|
+
Resolved per request through sigrun's auth.json bridge, falling back to
|
|
49
|
+
the ambient environment:
|
|
50
|
+
|
|
51
|
+
1. the bridged auth.json credential (`"default"` entry, then the
|
|
52
|
+
`"pareto"` entry), then
|
|
53
|
+
2. `PARETO_API_KEY`.
|
|
54
|
+
|
|
55
|
+
The plugin never reads auth.json or `process.env` itself. Rotating the
|
|
56
|
+
key applies on the next resolution — no restart needed.
|
|
57
|
+
|
|
58
|
+
## Failure semantics
|
|
59
|
+
|
|
60
|
+
`fetchModels` **throws** when unconfigured or when the catalog fetch
|
|
61
|
+
fails — it never returns an empty array, which pi-ai would publish and
|
|
62
|
+
persist, wiping the catalog. A failed refresh retains the previous
|
|
63
|
+
catalog and records a per-provider error.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```jsonc
|
|
68
|
+
// settings.json (sigrun config dir) — the UNIFIED extensions list:
|
|
69
|
+
// install + load in one place; npm and git specifiers are the same list
|
|
70
|
+
// npm ("@sigrun/provider-pareto@^0.1.0"), git
|
|
71
|
+
// ("git:github.com/sigrun-ai/sigrun-provider-pareto", or any
|
|
72
|
+
// https git URL with an optional @ref), or the file: form below.
|
|
73
|
+
{
|
|
74
|
+
"extensions": ["file:/path/to/sigrun-pareto"]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The committed `dist/` is the load target for git installs; pi-ai and
|
|
79
|
+
pi-durable are peerDependencies (sigrun supplies them at runtime);
|
|
80
|
+
they are mirrored in devDependencies for this repo's own build and tests.
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
npm install # also builds dist/ via prepare
|
|
86
|
+
npm test # vitest — hermetic fixture HTTP server, no network
|
|
87
|
+
npm run typecheck
|
|
88
|
+
npm run build
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sigrun/provider-pareto — a sigrun provider plugin (provider plugins Task 9):
|
|
3
|
+
* [Pareto](https://paretoinference.com) paygo (OpenAI-compatible
|
|
4
|
+
* inference) as one dynamic provider.
|
|
5
|
+
*
|
|
6
|
+
* Structurally identical to sigrun-ollama (the Task 7 reference plugin):
|
|
7
|
+
* the default export is the sigrun extension-package factory returning a
|
|
8
|
+
* PROVIDER-ONLY contributor — `{ name, providers, health }` with no
|
|
9
|
+
* `extension` field — consumed structurally by sigrun's loader. The
|
|
10
|
+
* deltas from the reference are the provider id, the base URL, the
|
|
11
|
+
* ambient env key, and ONE addition: the base URL is
|
|
12
|
+
* deployment-configurable through the plugin's scoped settings
|
|
13
|
+
* (`baseUrl` in `settings/provider-pareto.json`, constant fallback),
|
|
14
|
+
* because Pareto's documented endpoint is a deployment-specific value.
|
|
15
|
+
* Credentials resolve through sigrun's auth.json bridge (the stored
|
|
16
|
+
* credential arrives as `credential` on the auth resolver, ambient env
|
|
17
|
+
* as `ctx.env`) with `PARETO_API_KEY` as the fallback; the plugin never
|
|
18
|
+
* reads auth.json or `process.env` itself.
|
|
19
|
+
*
|
|
20
|
+
* Runtime imports from pi-ai are deliberate (`createProvider` + the
|
|
21
|
+
* OpenAI-completions api); the package still declares pi-ai/pi-durable as
|
|
22
|
+
* devDependencies only, because sigrun (the host) ships them and
|
|
23
|
+
* `syncPackages` installs with `--omit=dev` — the provider-plugins spec's
|
|
24
|
+
* dependency posture.
|
|
25
|
+
*/
|
|
26
|
+
import { type Provider } from "@earendil-works/pi-ai/models";
|
|
27
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
28
|
+
/** The provider id models are registered (and stamped) under. */
|
|
29
|
+
export declare const PARETO_PROVIDER_ID = "pareto";
|
|
30
|
+
/**
|
|
31
|
+
* Pareto's OpenAI-compatible endpoint. Live-verified 2026-10-18; the
|
|
32
|
+
* operator can override per deployment via the plugin's scoped settings
|
|
33
|
+
* (`baseUrl`) — see {@link resolveParetoBaseUrl}.
|
|
34
|
+
*/
|
|
35
|
+
export declare const PARETO_BASE_URL = "https://api.paretoinference.com/v1";
|
|
36
|
+
/** Ambient fallback when auth.json holds no credential. */
|
|
37
|
+
export declare const PARETO_API_KEY_ENV = "PARETO_API_KEY";
|
|
38
|
+
/**
|
|
39
|
+
* Structural slice of sigrun's seam host (src/harness/seam-host.ts) a
|
|
40
|
+
* provider plugin may consume. Declared here — structurally, like the
|
|
41
|
+
* bridge's pi-ai re-declarations — because the plugin cannot import
|
|
42
|
+
* sigrun core. The scoped settings store is async-only, which is why the
|
|
43
|
+
* loader awaits factory results (and why this plugin's factory is async).
|
|
44
|
+
*/
|
|
45
|
+
export interface SigrunPluginHost {
|
|
46
|
+
readonly stores: {
|
|
47
|
+
/** This plugin's own `settings/<slug>.json`; absent file → `{}`. */
|
|
48
|
+
readonly settings: {
|
|
49
|
+
get(): Promise<Record<string, unknown>>;
|
|
50
|
+
set(value: Record<string, unknown>): Promise<void>;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Sigrun's three-state per-provider readiness vocabulary (Task 6). */
|
|
55
|
+
export type ProviderReadinessStatus = "not_configured" | "degraded" | "ready";
|
|
56
|
+
/** One tracker report: the status plus optional operator-facing detail. */
|
|
57
|
+
export interface ProviderReadiness {
|
|
58
|
+
readonly status: ProviderReadinessStatus;
|
|
59
|
+
readonly detail?: string;
|
|
60
|
+
}
|
|
61
|
+
/** Sigrun's §3 health vocabulary. */
|
|
62
|
+
export type HealthStatus = "ok" | "degraded";
|
|
63
|
+
/**
|
|
64
|
+
* The provider-plugin factory result (the loader's Task 1 contract):
|
|
65
|
+
* a provider-only contributor — `{ name, providers, health? }`, no
|
|
66
|
+
* `extension`. Structural, like the host slice above.
|
|
67
|
+
*/
|
|
68
|
+
export interface ProviderPluginResult {
|
|
69
|
+
readonly name: string;
|
|
70
|
+
readonly providers: readonly {
|
|
71
|
+
readonly provider: Provider;
|
|
72
|
+
readonly readiness?: () => ProviderReadiness;
|
|
73
|
+
}[];
|
|
74
|
+
readonly health?: () => HealthStatus;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the plugin's base URL from its scoped settings: `baseUrl`
|
|
78
|
+
* (string) overrides the {@link PARETO_BASE_URL} constant. Absent → the
|
|
79
|
+
* constant; present-but-not-a-non-empty-string → throw (a malformed
|
|
80
|
+
* override must quarantine loudly, not silently fall back to a possibly
|
|
81
|
+
* wrong constant).
|
|
82
|
+
*/
|
|
83
|
+
export declare function resolveParetoBaseUrl(settings: Record<string, unknown>): string;
|
|
84
|
+
/**
|
|
85
|
+
* Parse an OpenAI-compatible `[OI]` `/models` body into pi-ai chat
|
|
86
|
+
* models. Every entry is stamped `provider: "pareto"` — pi-ai's restore
|
|
87
|
+
* path drops stored models whose `provider` field differs from the
|
|
88
|
+
* registered id, so an unstamped catalog silently vanishes on cold boot.
|
|
89
|
+
* Malformed bodies throw (a refresh failure retains the previous
|
|
90
|
+
* catalog; garbage must never publish). Extra entry fields (the live
|
|
91
|
+
* endpoint also serves `owned_by` and a `pricing` object) are ignored.
|
|
92
|
+
*/
|
|
93
|
+
export declare function parseParetoCatalog(body: unknown, baseUrl: string): Model<"openai-completions">[];
|
|
94
|
+
/** One built plugin: the contribution's parts, sharing the tracker closure. */
|
|
95
|
+
export interface ParetoPlugin {
|
|
96
|
+
readonly provider: Provider;
|
|
97
|
+
/** Synchronous read over plugin-owned state (never network/storage). */
|
|
98
|
+
readonly readiness: () => ProviderReadiness;
|
|
99
|
+
readonly health: () => HealthStatus;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build the Pareto provider plus its readiness/health trackers.
|
|
103
|
+
* `baseUrl` is overridable (scoped settings in the default factory,
|
|
104
|
+
* fixture server in tests); the default is the live-verified endpoint.
|
|
105
|
+
*/
|
|
106
|
+
export declare function createParetoPlugin(baseUrl?: string): ParetoPlugin;
|
|
107
|
+
/**
|
|
108
|
+
* The sigrun extension-package factory (provider-only contributor).
|
|
109
|
+
* Arity 1 declares the host-taking signature — sigrun's loader passes the
|
|
110
|
+
* seam host only to factories that declare one (`factory.length > 0`) —
|
|
111
|
+
* and the factory is ASYNC because the scoped settings store is
|
|
112
|
+
* async-only; sigrun's loader awaits factory results.
|
|
113
|
+
*/
|
|
114
|
+
export default function sigrunPareto(host: SigrunPluginHost): Promise<ProviderPluginResult>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sigrun/provider-pareto — a sigrun provider plugin (provider plugins Task 9):
|
|
3
|
+
* [Pareto](https://paretoinference.com) paygo (OpenAI-compatible
|
|
4
|
+
* inference) as one dynamic provider.
|
|
5
|
+
*
|
|
6
|
+
* Structurally identical to sigrun-ollama (the Task 7 reference plugin):
|
|
7
|
+
* the default export is the sigrun extension-package factory returning a
|
|
8
|
+
* PROVIDER-ONLY contributor — `{ name, providers, health }` with no
|
|
9
|
+
* `extension` field — consumed structurally by sigrun's loader. The
|
|
10
|
+
* deltas from the reference are the provider id, the base URL, the
|
|
11
|
+
* ambient env key, and ONE addition: the base URL is
|
|
12
|
+
* deployment-configurable through the plugin's scoped settings
|
|
13
|
+
* (`baseUrl` in `settings/provider-pareto.json`, constant fallback),
|
|
14
|
+
* because Pareto's documented endpoint is a deployment-specific value.
|
|
15
|
+
* Credentials resolve through sigrun's auth.json bridge (the stored
|
|
16
|
+
* credential arrives as `credential` on the auth resolver, ambient env
|
|
17
|
+
* as `ctx.env`) with `PARETO_API_KEY` as the fallback; the plugin never
|
|
18
|
+
* reads auth.json or `process.env` itself.
|
|
19
|
+
*
|
|
20
|
+
* Runtime imports from pi-ai are deliberate (`createProvider` + the
|
|
21
|
+
* OpenAI-completions api); the package still declares pi-ai/pi-durable as
|
|
22
|
+
* devDependencies only, because sigrun (the host) ships them and
|
|
23
|
+
* `syncPackages` installs with `--omit=dev` — the provider-plugins spec's
|
|
24
|
+
* dependency posture.
|
|
25
|
+
*/
|
|
26
|
+
import { createProvider } from "@earendil-works/pi-ai/models";
|
|
27
|
+
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
28
|
+
/** The provider id models are registered (and stamped) under. */
|
|
29
|
+
export const PARETO_PROVIDER_ID = "pareto";
|
|
30
|
+
/**
|
|
31
|
+
* Pareto's OpenAI-compatible endpoint. Live-verified 2026-10-18; the
|
|
32
|
+
* operator can override per deployment via the plugin's scoped settings
|
|
33
|
+
* (`baseUrl`) — see {@link resolveParetoBaseUrl}.
|
|
34
|
+
*/
|
|
35
|
+
export const PARETO_BASE_URL = "https://api.paretoinference.com/v1";
|
|
36
|
+
/** Ambient fallback when auth.json holds no credential. */
|
|
37
|
+
export const PARETO_API_KEY_ENV = "PARETO_API_KEY";
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the plugin's base URL from its scoped settings: `baseUrl`
|
|
40
|
+
* (string) overrides the {@link PARETO_BASE_URL} constant. Absent → the
|
|
41
|
+
* constant; present-but-not-a-non-empty-string → throw (a malformed
|
|
42
|
+
* override must quarantine loudly, not silently fall back to a possibly
|
|
43
|
+
* wrong constant).
|
|
44
|
+
*/
|
|
45
|
+
export function resolveParetoBaseUrl(settings) {
|
|
46
|
+
if (!("baseUrl" in settings))
|
|
47
|
+
return PARETO_BASE_URL;
|
|
48
|
+
const value = settings.baseUrl;
|
|
49
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
50
|
+
throw new Error("provider-pareto: settings/provider-pareto.json baseUrl must be a non-empty string when present");
|
|
51
|
+
}
|
|
52
|
+
return value.trim();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Conservative placeholders for catalog entries Pareto's `/models` shape
|
|
56
|
+
* gives no usable metadata for (entries carry a per-token `pricing`
|
|
57
|
+
* object in dollar STRINGS, which this plugin does not map — cost stays
|
|
58
|
+
* zeroed and structural until sigrun defines a rate source of truth).
|
|
59
|
+
* Same values as the reference plugin and sigrun's degenerate env
|
|
60
|
+
* provider, keeping context management deterministic against unknown
|
|
61
|
+
* windows.
|
|
62
|
+
*/
|
|
63
|
+
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
64
|
+
const DEFAULT_MAX_TOKENS = 16_384;
|
|
65
|
+
/**
|
|
66
|
+
* Parse an OpenAI-compatible `[OI]` `/models` body into pi-ai chat
|
|
67
|
+
* models. Every entry is stamped `provider: "pareto"` — pi-ai's restore
|
|
68
|
+
* path drops stored models whose `provider` field differs from the
|
|
69
|
+
* registered id, so an unstamped catalog silently vanishes on cold boot.
|
|
70
|
+
* Malformed bodies throw (a refresh failure retains the previous
|
|
71
|
+
* catalog; garbage must never publish). Extra entry fields (the live
|
|
72
|
+
* endpoint also serves `owned_by` and a `pricing` object) are ignored.
|
|
73
|
+
*/
|
|
74
|
+
export function parseParetoCatalog(body, baseUrl) {
|
|
75
|
+
if (typeof body !== "object" || body === null) {
|
|
76
|
+
throw new Error("pareto /models body is not a JSON object");
|
|
77
|
+
}
|
|
78
|
+
const data = body.data;
|
|
79
|
+
if (!Array.isArray(data)) {
|
|
80
|
+
throw new Error("pareto /models body has no data array");
|
|
81
|
+
}
|
|
82
|
+
return data.map((entry) => {
|
|
83
|
+
const id = entry?.id;
|
|
84
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
85
|
+
throw new Error("pareto /models entry without a string id");
|
|
86
|
+
}
|
|
87
|
+
const model = {
|
|
88
|
+
id,
|
|
89
|
+
name: id,
|
|
90
|
+
api: "openai-completions",
|
|
91
|
+
// THE stamp — see the doc comment above.
|
|
92
|
+
provider: PARETO_PROVIDER_ID,
|
|
93
|
+
baseUrl,
|
|
94
|
+
// Pareto's /models carries no modality metadata; text is the
|
|
95
|
+
// safe floor (claiming image without evidence would ship image
|
|
96
|
+
// parts to models that cannot read them).
|
|
97
|
+
input: ["text"],
|
|
98
|
+
// Rates exist on the wire but are not mapped (see the
|
|
99
|
+
// placeholders comment); zeroed, usage tracking stays
|
|
100
|
+
// structural.
|
|
101
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
102
|
+
reasoning: false,
|
|
103
|
+
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
104
|
+
maxTokens: DEFAULT_MAX_TOKENS,
|
|
105
|
+
};
|
|
106
|
+
return model;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build the Pareto provider plus its readiness/health trackers.
|
|
111
|
+
* `baseUrl` is overridable (scoped settings in the default factory,
|
|
112
|
+
* fixture server in tests); the default is the live-verified endpoint.
|
|
113
|
+
*/
|
|
114
|
+
export function createParetoPlugin(baseUrl = PARETO_BASE_URL) {
|
|
115
|
+
// Tracker state: seeded from credential resolution (pi-ai never
|
|
116
|
+
// refreshes an unconfigured provider, so refresh outcomes alone cannot
|
|
117
|
+
// express not_configured), updated per refresh outcome. Readiness and
|
|
118
|
+
// health are synchronous derivations over this state.
|
|
119
|
+
let credentialResolved = false;
|
|
120
|
+
let lastRefreshFailed = false;
|
|
121
|
+
let lastRefreshDetail;
|
|
122
|
+
let lastAnnounced;
|
|
123
|
+
function status() {
|
|
124
|
+
if (!credentialResolved)
|
|
125
|
+
return "not_configured";
|
|
126
|
+
return lastRefreshFailed ? "degraded" : "ready";
|
|
127
|
+
}
|
|
128
|
+
/** Apply a state change; `console.warn` on transitions only. */
|
|
129
|
+
function announce() {
|
|
130
|
+
const next = status();
|
|
131
|
+
if (next === lastAnnounced)
|
|
132
|
+
return;
|
|
133
|
+
console.warn(`provider-pareto: pareto ${lastAnnounced ?? "startup"} → ${next}` +
|
|
134
|
+
(lastRefreshDetail !== undefined && next === "degraded"
|
|
135
|
+
? ` (${lastRefreshDetail})`
|
|
136
|
+
: ""));
|
|
137
|
+
lastAnnounced = next;
|
|
138
|
+
}
|
|
139
|
+
function noteCredential(resolved) {
|
|
140
|
+
credentialResolved = resolved;
|
|
141
|
+
announce();
|
|
142
|
+
}
|
|
143
|
+
function noteRefresh(ok, detail) {
|
|
144
|
+
lastRefreshFailed = !ok;
|
|
145
|
+
lastRefreshDetail = ok ? undefined : detail;
|
|
146
|
+
announce();
|
|
147
|
+
}
|
|
148
|
+
const provider = createProvider({
|
|
149
|
+
id: PARETO_PROVIDER_ID,
|
|
150
|
+
name: "Pareto",
|
|
151
|
+
baseUrl,
|
|
152
|
+
// REQUIRED baseline: createProvider iterates it (omitting throws
|
|
153
|
+
// "baselineModels is not iterable"); empty = purely dynamic.
|
|
154
|
+
models: [],
|
|
155
|
+
auth: {
|
|
156
|
+
apiKey: {
|
|
157
|
+
name: "Pareto API key (auth.json or PARETO_API_KEY)",
|
|
158
|
+
resolve: async ({ credential, ctx }) => {
|
|
159
|
+
// Bridged auth.json credential (sigrun's bridge: the
|
|
160
|
+
// "default" entry first, then the provider's own) wins;
|
|
161
|
+
// the env var is the ambient fallback.
|
|
162
|
+
const stored = credential?.key?.trim() || undefined;
|
|
163
|
+
if (stored !== undefined) {
|
|
164
|
+
noteCredential(true);
|
|
165
|
+
return { auth: { apiKey: stored }, source: "auth.json" };
|
|
166
|
+
}
|
|
167
|
+
const envKey = (await ctx.env(PARETO_API_KEY_ENV))?.trim() || undefined;
|
|
168
|
+
if (envKey !== undefined) {
|
|
169
|
+
noteCredential(true);
|
|
170
|
+
return { auth: { apiKey: envKey }, source: PARETO_API_KEY_ENV };
|
|
171
|
+
}
|
|
172
|
+
// Unconfigured: undefined makes pi-ai skip the refresh
|
|
173
|
+
// phase entirely and requests fail loud.
|
|
174
|
+
noteCredential(false);
|
|
175
|
+
return undefined;
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
fetchModels: async (context) => {
|
|
180
|
+
// The context credential is whatever auth resolution produced —
|
|
181
|
+
// for this plugin always the merged api key; narrow rather than
|
|
182
|
+
// trust the union.
|
|
183
|
+
const key = context.credential?.type === "api_key"
|
|
184
|
+
? context.credential.key?.trim() || undefined
|
|
185
|
+
: undefined;
|
|
186
|
+
if (key === undefined) {
|
|
187
|
+
noteCredential(false);
|
|
188
|
+
// NEVER return [] — the createProvider wrapper publishes +
|
|
189
|
+
// persists whatever fetchModels returns; an empty array
|
|
190
|
+
// would wipe the catalog.
|
|
191
|
+
throw new Error(`provider-pareto: Pareto is not configured ` +
|
|
192
|
+
`(no auth.json credential and no ${PARETO_API_KEY_ENV}); ` +
|
|
193
|
+
"refusing to publish an empty catalog");
|
|
194
|
+
}
|
|
195
|
+
const effectiveBase = context.baseUrl ?? baseUrl;
|
|
196
|
+
// URL-join needs the trailing slash ("models" against a
|
|
197
|
+
// slash-less base would resolve to the host root).
|
|
198
|
+
const root = effectiveBase.endsWith("/") ? effectiveBase : `${effectiveBase}/`;
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetch(new URL("models", root), {
|
|
201
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
202
|
+
signal: context.signal,
|
|
203
|
+
});
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
throw new Error(`HTTP ${response.status}`);
|
|
206
|
+
}
|
|
207
|
+
const models = parseParetoCatalog(await response.json(), effectiveBase);
|
|
208
|
+
noteRefresh(true);
|
|
209
|
+
return models;
|
|
210
|
+
}
|
|
211
|
+
catch (cause) {
|
|
212
|
+
// An aborted refresh is not a failed one: pi-ai discards
|
|
213
|
+
// the outcome anyway, so the tracker must not move.
|
|
214
|
+
if (context.signal.aborted)
|
|
215
|
+
throw cause;
|
|
216
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
217
|
+
noteRefresh(false, message);
|
|
218
|
+
throw new Error(`provider-pareto: Pareto catalog refresh failed: ${message}`);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
api: openAICompletionsApi(),
|
|
222
|
+
});
|
|
223
|
+
return {
|
|
224
|
+
provider,
|
|
225
|
+
readiness: () => ({
|
|
226
|
+
status: status(),
|
|
227
|
+
...(lastRefreshDetail !== undefined && status() === "degraded"
|
|
228
|
+
? { detail: lastRefreshDetail }
|
|
229
|
+
: {}),
|
|
230
|
+
}),
|
|
231
|
+
// not_configured → ok (an unconfigured optional plugin is not a
|
|
232
|
+
// degraded process); configured-and-failing → degraded; else ok.
|
|
233
|
+
health: () => (status() === "degraded" ? "degraded" : "ok"),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The sigrun extension-package factory (provider-only contributor).
|
|
238
|
+
* Arity 1 declares the host-taking signature — sigrun's loader passes the
|
|
239
|
+
* seam host only to factories that declare one (`factory.length > 0`) —
|
|
240
|
+
* and the factory is ASYNC because the scoped settings store is
|
|
241
|
+
* async-only; sigrun's loader awaits factory results.
|
|
242
|
+
*/
|
|
243
|
+
export default async function sigrunPareto(host) {
|
|
244
|
+
const settings = await host.stores.settings.get();
|
|
245
|
+
const plugin = createParetoPlugin(resolveParetoBaseUrl(settings));
|
|
246
|
+
return {
|
|
247
|
+
name: "provider-pareto",
|
|
248
|
+
providers: [{ provider: plugin.provider, readiness: plugin.readiness }],
|
|
249
|
+
health: plugin.health,
|
|
250
|
+
};
|
|
251
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@sigrun/provider-pareto",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Sigrun provider plugin: Pareto paygo (OpenAI-compatible inference) as a dynamic provider",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.19.0"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node scripts/check-manifest.mjs --expect-name @sigrun/provider-pareto && vitest run",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"build": "tsc -p tsconfig.build.json",
|
|
25
|
+
"prepare": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@earendil-works/pi-ai": "1.3.0",
|
|
29
|
+
"@earendil-works/pi-durable": "1.3.0",
|
|
30
|
+
"@types/node": "^26.6.4",
|
|
31
|
+
"typescript": "^7.0.2",
|
|
32
|
+
"vitest": "^5.0.3"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "github",
|
|
37
|
+
"url": "git+https://github.com/sigrun-ai/sigrun-provider-pareto.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/sigrun-ai/sigrun-provider-pareto/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/sigrun-ai/sigrun-provider-pareto#readme",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public",
|
|
45
|
+
"provenance": true
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@earendil-works/pi-ai": "1.3.0",
|
|
49
|
+
"@earendil-works/pi-durable": "1.3.0"
|
|
50
|
+
}
|
|
51
|
+
}
|