@sigrun/provider-bifrost 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/dist/http.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Transport only (ported from pi-bifrost-gateway 0.3.0 @ 5d15a28, module
3
+ * unchanged except the User-Agent name): GET with Bearer auth + typebox
4
+ * validation, a hard 15 s deadline so a silent peer can never hang the
5
+ * refresh.
6
+ */
7
+ import type { TProperties, TSchema } from "typebox";
8
+ import type { Validator } from "typebox/compile";
9
+ export declare class HttpError extends Error {
10
+ readonly status: number;
11
+ readonly code: string;
12
+ constructor(status: number, code: string, message: string);
13
+ }
14
+ export interface FetchJsonOptions {
15
+ fetchImpl?: typeof fetch;
16
+ }
17
+ export declare function fetchJson<T>(url: string, token: string, validator: Validator<TProperties, TSchema>, signal?: AbortSignal, opts?: FetchJsonOptions): Promise<T>;
package/dist/http.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Transport only (ported from pi-bifrost-gateway 0.3.0 @ 5d15a28, module
3
+ * unchanged except the User-Agent name): GET with Bearer auth + typebox
4
+ * validation, a hard 15 s deadline so a silent peer can never hang the
5
+ * refresh.
6
+ */
7
+ import { createRequire } from "node:module";
8
+ const require = createRequire(import.meta.url);
9
+ function packageVersion() {
10
+ const payload = require("../package.json");
11
+ if (typeof payload === "object" && payload !== null) {
12
+ const version = payload.version;
13
+ if (typeof version === "string" && version.length > 0)
14
+ return version;
15
+ }
16
+ return "0.0.0";
17
+ }
18
+ // The wire identity: the repo/product name (the npm name minus its
19
+ // scope), stable across a package rename — upstreams key logs on it.
20
+ const USER_AGENT = `sigrun-provider-bifrost/${packageVersion()}`;
21
+ export class HttpError extends Error {
22
+ status;
23
+ code;
24
+ constructor(status, code, message) {
25
+ super(message);
26
+ this.status = status;
27
+ this.code = code;
28
+ this.name = HttpError.name;
29
+ }
30
+ }
31
+ /**
32
+ * GET `url` with Bearer auth and validate the JSON body with `validator`.
33
+ * Non-2xx responses throw HttpError carrying the upstream
34
+ * `error.code`/`error.message` when present. Abort errors propagate untouched.
35
+ *
36
+ * Each GET also carries a default 15 s deadline (AbortSignal.timeout,
37
+ * combined with any caller signal — the caller's abort still wins). Without
38
+ * it a peer that accepts the connection and then goes silent hangs the
39
+ * refresh until the process dies; refresh MUST terminate.
40
+ */
41
+ const DEFAULT_TIMEOUT_MS = 15_000;
42
+ export async function fetchJson(url, token, validator, signal, opts) {
43
+ const doFetch = opts?.fetchImpl ?? globalThis.fetch;
44
+ const deadline = AbortSignal.any([AbortSignal.timeout(DEFAULT_TIMEOUT_MS), ...(signal ? [signal] : [])]);
45
+ const response = await doFetch(url, {
46
+ method: "GET",
47
+ headers: {
48
+ Authorization: `Bearer ${token}`,
49
+ "User-Agent": USER_AGENT,
50
+ Accept: "application/json",
51
+ },
52
+ signal: deadline,
53
+ });
54
+ const body = await response.text();
55
+ if (!response.ok) {
56
+ const { code, message } = parseErrorBody(body);
57
+ throw new HttpError(response.status, code === "http_error" ? `http_${response.status}` : code, message.length > 0 ? message : `${response.status} from ${url}`);
58
+ }
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(body);
62
+ }
63
+ catch (error) {
64
+ throw new HttpError(response.status, "schema_mismatch", `invalid JSON from ${url}: ${String(error)}`);
65
+ }
66
+ if (!validator.Check(parsed)) {
67
+ const detail = validator
68
+ .Errors(parsed)
69
+ .map((e) => `${e.instancePath}: ${e.message}`)
70
+ .join("; ");
71
+ throw new HttpError(response.status, "schema_mismatch", `schema mismatch from ${url}: ${detail}`);
72
+ }
73
+ return validator.Decode(parsed);
74
+ }
75
+ function parseErrorBody(body) {
76
+ try {
77
+ const payload = JSON.parse(body);
78
+ const error = payload?.error;
79
+ if (typeof error === "object" && error !== null && typeof error.message === "string") {
80
+ const code = typeof error.code === "string" && error.code.length > 0 ? error.code : "http_error";
81
+ return { code, message: error.message };
82
+ }
83
+ }
84
+ catch {
85
+ // non-JSON error body — fall through
86
+ }
87
+ return { code: "http_error", message: body.slice(0, 300) };
88
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @sigrun/provider-bifrost — the big port (provider plugins Task 10):
3
+ * [Bifrost](https://github.com/maximhq/bifrost) LLM gateway as one dynamic
4
+ * provider, ported from pi-bifrost-gateway 0.3.0 @ 5d15a28. All failover,
5
+ * key selection, and routing happens in Bifrost — this plugin never routes
6
+ * anything itself.
7
+ *
8
+ * The default export is the sigrun extension-package factory returning a
9
+ * PROVIDER-ONLY contributor — `{ name, providers, health }` with no
10
+ * `extension` field — consumed structurally by sigrun's loader (Task 1
11
+ * pathway). The port's two deliberate adaptations:
12
+ *
13
+ * - CONFIG: pi's config file becomes the plugin's SCOPED SETTINGS
14
+ * (`settings/provider-bifrost.json`), read ONCE at factory time —
15
+ * restart-required (sigrun's loader pre-keys the factory host on the
16
+ * provisional slug so the factory-time read resolves this package's
17
+ * file). No /login flow; no pi config file.
18
+ * - CREDENTIALS: the VK comes from sigrun's auth.json bridge (the stored
19
+ * credential arrives as `credential` on the auth resolver) with
20
+ * `BIFROST_API_KEY` as the ambient fallback; the ambient base URL
21
+ * (`BIFROST_BASE_URL`) resolves through the same env seam. The plugin
22
+ * never reads auth.json or `process.env` itself.
23
+ *
24
+ * Two discovery modes (settings `publishChains`, default false):
25
+ * - chains mode: the management plane is queried — VK self-identification
26
+ * (`/api/governance/virtual-keys`) + routing rules
27
+ * (`/api/routing/rules`) — and a rules set with no usable chains fails
28
+ * discovery loud.
29
+ * - member-only mode (default): the inference plane alone (`GET /v1/models`);
30
+ * ZERO management calls, so it works against a stock gateway with
31
+ * governance disabled entirely.
32
+ *
33
+ * `fetchModels` THROWS on unconfigured (no key or no base URL), on a bad
34
+ * credential/VK mismatch, and on zero chains — it NEVER returns `[]`,
35
+ * which pi-ai would publish and persist, wiping the catalog.
36
+ *
37
+ * Runtime imports from pi-ai are deliberate (`createProvider` + the three
38
+ * api surfaces); the package declares pi-ai/pi-durable as devDependencies
39
+ * only, because sigrun (the host) ships them and `syncPackages` installs
40
+ * without dev dependencies — the provider-plugins spec's dependency
41
+ * posture.
42
+ */
43
+ import { type Provider } from "@earendil-works/pi-ai/models";
44
+ import { type NativeProtocols } from "./config.js";
45
+ import { PROVIDER_DISPLAY_NAME, PROVIDER_ID, UNRESOLVED_BASE_URL, type GatewayModel } from "./catalog.js";
46
+ export { PROVIDER_ID, PROVIDER_DISPLAY_NAME, UNRESOLVED_BASE_URL };
47
+ export { parseBifrostSettings, BIFROST_BASE_URL_ENV } from "./config.js";
48
+ export type { BifrostSettings, NativeProtocols } from "./config.js";
49
+ export type { GatewayModel, SurfaceApi, CatalogModel, Chain, RoutingRule } from "./catalog.js";
50
+ /** Ambient fallback for the virtual key when auth.json holds no credential. */
51
+ export declare const BIFROST_API_KEY_ENV = "BIFROST_API_KEY";
52
+ /**
53
+ * Structural slice of sigrun's seam host (src/harness/seam-host.ts) a
54
+ * provider plugin may consume. Declared here — structurally, like the
55
+ * bridge's pi-ai re-declarations — because the plugin cannot import
56
+ * sigrun core. The scoped settings store is async-only, which is why the
57
+ * loader awaits factory results (and why this plugin's factory is async).
58
+ */
59
+ export interface SigrunPluginHost {
60
+ readonly stores: {
61
+ /** This plugin's own `settings/<slug>.json`; absent file → `{}`. */
62
+ readonly settings: {
63
+ get(): Promise<Record<string, unknown>>;
64
+ set(value: Record<string, unknown>): Promise<void>;
65
+ };
66
+ };
67
+ }
68
+ /** Sigrun's three-state per-provider readiness vocabulary (Task 6). */
69
+ export type ProviderReadinessStatus = "not_configured" | "degraded" | "ready";
70
+ /** One tracker report: the status plus optional operator-facing detail. */
71
+ export interface ProviderReadiness {
72
+ readonly status: ProviderReadinessStatus;
73
+ readonly detail?: string;
74
+ }
75
+ /** Sigrun's §3 health vocabulary. */
76
+ export type HealthStatus = "ok" | "degraded";
77
+ /**
78
+ * The provider-plugin factory result (the loader's Task 1 contract):
79
+ * a provider-only contributor — `{ name, providers, health? }`, no
80
+ * `extension`. Structural, like the host slice above.
81
+ */
82
+ export interface ProviderPluginResult {
83
+ readonly name: string;
84
+ readonly providers: readonly {
85
+ readonly provider: Provider;
86
+ readonly readiness?: () => ProviderReadiness;
87
+ }[];
88
+ readonly health?: () => HealthStatus;
89
+ }
90
+ /** The options `createBifrostPlugin` bakes from factory-time settings. */
91
+ export interface BifrostPluginOptions {
92
+ publishChains: boolean;
93
+ publishUpstream: boolean;
94
+ /** Settings-pinned base URL; ambient env fallback applies when absent. */
95
+ baseUrl?: string;
96
+ managementApiKey?: string;
97
+ nativeProtocols?: NativeProtocols;
98
+ thinkingOverrides?: Record<string, string[]>;
99
+ }
100
+ /**
101
+ * Compose the gateway model list from live Bifrost state (the ported
102
+ * `discoverModels` with sigrun's failure semantics — throws instead of
103
+ * returning `[]` on unconfigured, so a misconfigured deployment surfaces
104
+ * instead of silently wiping the catalog):
105
+ *
106
+ * - chains mode (`publishChains`): VK self-identification + routing rules
107
+ * via the MANAGEMENT token (`managementApiKey` ?? the VK), then catalog +
108
+ * rules in parallel (catalog with the VK itself). Zero usable chains
109
+ * throws — is chains-first routing configured for this VK?
110
+ * - member-only mode: inference plane alone; both publish flags off throws
111
+ * (a configured provider publishing nothing is a settings error, and
112
+ * `fetchModels` never returns `[]`).
113
+ *
114
+ * Throws when the credential is not a recognized VK (chains mode) or when
115
+ * discovery itself fails (unreachable gateway, schema mismatch).
116
+ */
117
+ export declare function discoverModels(token: string | undefined, baseUrl: string | undefined, options: BifrostPluginOptions, signal?: AbortSignal): Promise<GatewayModel[]>;
118
+ /** One built plugin: the contribution's parts, sharing the tracker closure. */
119
+ export interface BifrostPlugin {
120
+ readonly provider: Provider;
121
+ /** Synchronous read over plugin-owned state (never network/storage). */
122
+ readonly readiness: () => ProviderReadiness;
123
+ readonly health: () => HealthStatus;
124
+ }
125
+ /**
126
+ * Build the Bifrost provider plus its readiness/health trackers.
127
+ * `options` are the FACTORY-BAKED settings (restart-required); the ambient
128
+ * `BIFROST_BASE_URL` resolves at credential-resolution time through the
129
+ * env seam and falls below the baked `baseUrl` in precedence.
130
+ */
131
+ export declare function createBifrostPlugin(options: BifrostPluginOptions): BifrostPlugin;
132
+ /**
133
+ * The sigrun extension-package factory (provider-only contributor).
134
+ * Arity 1 declares the host-taking signature — sigrun's loader passes the
135
+ * seam host only to factories that declare one (`factory.length > 0`) —
136
+ * and the factory is ASYNC because the scoped settings store is
137
+ * async-only; sigrun's loader awaits factory results.
138
+ *
139
+ * Settings are read ONCE here and BAKED into the provider (the
140
+ * restart-required semantics: the loader pre-keys the host on the
141
+ * provisional slug — this package's manifest name — so the factory-time
142
+ * read resolves `settings/provider-bifrost.json`). Parse issues log via
143
+ * `console.warn` and never quarantine (a wrong-typed key falls back to
144
+ * its default, exactly like the pi port's issue list).
145
+ */
146
+ export default function sigrunBifrost(host: SigrunPluginHost): Promise<ProviderPluginResult>;
package/dist/index.js ADDED
@@ -0,0 +1,280 @@
1
+ /**
2
+ * @sigrun/provider-bifrost — the big port (provider plugins Task 10):
3
+ * [Bifrost](https://github.com/maximhq/bifrost) LLM gateway as one dynamic
4
+ * provider, ported from pi-bifrost-gateway 0.3.0 @ 5d15a28. All failover,
5
+ * key selection, and routing happens in Bifrost — this plugin never routes
6
+ * anything itself.
7
+ *
8
+ * The default export is the sigrun extension-package factory returning a
9
+ * PROVIDER-ONLY contributor — `{ name, providers, health }` with no
10
+ * `extension` field — consumed structurally by sigrun's loader (Task 1
11
+ * pathway). The port's two deliberate adaptations:
12
+ *
13
+ * - CONFIG: pi's config file becomes the plugin's SCOPED SETTINGS
14
+ * (`settings/provider-bifrost.json`), read ONCE at factory time —
15
+ * restart-required (sigrun's loader pre-keys the factory host on the
16
+ * provisional slug so the factory-time read resolves this package's
17
+ * file). No /login flow; no pi config file.
18
+ * - CREDENTIALS: the VK comes from sigrun's auth.json bridge (the stored
19
+ * credential arrives as `credential` on the auth resolver) with
20
+ * `BIFROST_API_KEY` as the ambient fallback; the ambient base URL
21
+ * (`BIFROST_BASE_URL`) resolves through the same env seam. The plugin
22
+ * never reads auth.json or `process.env` itself.
23
+ *
24
+ * Two discovery modes (settings `publishChains`, default false):
25
+ * - chains mode: the management plane is queried — VK self-identification
26
+ * (`/api/governance/virtual-keys`) + routing rules
27
+ * (`/api/routing/rules`) — and a rules set with no usable chains fails
28
+ * discovery loud.
29
+ * - member-only mode (default): the inference plane alone (`GET /v1/models`);
30
+ * ZERO management calls, so it works against a stock gateway with
31
+ * governance disabled entirely.
32
+ *
33
+ * `fetchModels` THROWS on unconfigured (no key or no base URL), on a bad
34
+ * credential/VK mismatch, and on zero chains — it NEVER returns `[]`,
35
+ * which pi-ai would publish and persist, wiping the catalog.
36
+ *
37
+ * Runtime imports from pi-ai are deliberate (`createProvider` + the three
38
+ * api surfaces); the package declares pi-ai/pi-durable as devDependencies
39
+ * only, because sigrun (the host) ships them and `syncPackages` installs
40
+ * without dev dependencies — the provider-plugins spec's dependency
41
+ * posture.
42
+ */
43
+ import { createProvider } from "@earendil-works/pi-ai/models";
44
+ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
45
+ import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
46
+ import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
47
+ import { BIFROST_BASE_URL_ENV, parseBifrostSettings, } from "./config.js";
48
+ import { PROVIDER_DISPLAY_NAME, PROVIDER_ID, UNRESOLVED_BASE_URL, buildAllModels, wireCatalog, wireDiscovery, wireRulesToChains, wireScopeId, } from "./catalog.js";
49
+ // Re-exported gateway constants (the pi port's src/bifrost.ts surface):
50
+ // the provider id is part of this plugin's public contract — sigrun's
51
+ // auth-check names providers by id and tests pin the model stamp to it.
52
+ export { PROVIDER_ID, PROVIDER_DISPLAY_NAME, UNRESOLVED_BASE_URL };
53
+ // Re-exported discovery/config types so consumers (and tests) need no
54
+ // deep imports.
55
+ export { parseBifrostSettings, BIFROST_BASE_URL_ENV } from "./config.js";
56
+ /** Ambient fallback for the virtual key when auth.json holds no credential. */
57
+ export const BIFROST_API_KEY_ENV = "BIFROST_API_KEY";
58
+ /**
59
+ * Compose the gateway model list from live Bifrost state (the ported
60
+ * `discoverModels` with sigrun's failure semantics — throws instead of
61
+ * returning `[]` on unconfigured, so a misconfigured deployment surfaces
62
+ * instead of silently wiping the catalog):
63
+ *
64
+ * - chains mode (`publishChains`): VK self-identification + routing rules
65
+ * via the MANAGEMENT token (`managementApiKey` ?? the VK), then catalog +
66
+ * rules in parallel (catalog with the VK itself). Zero usable chains
67
+ * throws — is chains-first routing configured for this VK?
68
+ * - member-only mode: inference plane alone; both publish flags off throws
69
+ * (a configured provider publishing nothing is a settings error, and
70
+ * `fetchModels` never returns `[]`).
71
+ *
72
+ * Throws when the credential is not a recognized VK (chains mode) or when
73
+ * discovery itself fails (unreachable gateway, schema mismatch).
74
+ */
75
+ export async function discoverModels(token, baseUrl, options, signal) {
76
+ if (!token || !baseUrl) {
77
+ throw new Error("provider-bifrost: the gateway is not configured " +
78
+ `(need a virtual key (auth.json or ${BIFROST_API_KEY_ENV}) and a base URL ` +
79
+ `(settings baseUrl or ${BIFROST_BASE_URL_ENV})); refusing to publish an empty catalog`);
80
+ }
81
+ if (options.publishChains) {
82
+ const managementToken = options.managementApiKey ?? token;
83
+ const scopeId = await wireScopeId(managementToken, token, baseUrl, signal);
84
+ if (!scopeId) {
85
+ throw new Error("provider-bifrost: credential does not match any virtual key in /api/governance/virtual-keys");
86
+ }
87
+ const { catalog, rules } = await wireDiscovery(managementToken, token, baseUrl, scopeId, signal);
88
+ const chains = wireRulesToChains(rules);
89
+ if (chains.length === 0) {
90
+ throw new Error(`provider-bifrost: no chain routing rules found for ${scopeId} — is chains-first routing configured for this VK?`);
91
+ }
92
+ const models = buildAllModels(chains, catalog, PROVIDER_ID, baseUrl, true, options.publishUpstream, options.thinkingOverrides, options.nativeProtocols);
93
+ if (models.length === 0) {
94
+ throw new Error("provider-bifrost: chains discovery produced no models; refusing to publish an empty catalog");
95
+ }
96
+ return models;
97
+ }
98
+ // Member-only mode: inference plane only, ZERO management calls.
99
+ if (!options.publishUpstream) {
100
+ throw new Error("provider-bifrost: publishChains and publishUpstream are both false — nothing to publish");
101
+ }
102
+ const catalog = await wireCatalog(token, baseUrl, signal);
103
+ const models = buildAllModels([], catalog, PROVIDER_ID, baseUrl, false, true, options.thinkingOverrides, options.nativeProtocols);
104
+ if (models.length === 0) {
105
+ throw new Error("provider-bifrost: /v1/models returned no usable entries; refusing to publish an empty catalog");
106
+ }
107
+ return models;
108
+ }
109
+ /**
110
+ * Build the Bifrost provider plus its readiness/health trackers.
111
+ * `options` are the FACTORY-BAKED settings (restart-required); the ambient
112
+ * `BIFROST_BASE_URL` resolves at credential-resolution time through the
113
+ * env seam and falls below the baked `baseUrl` in precedence.
114
+ */
115
+ export function createBifrostPlugin(options) {
116
+ // Tracker state: seeded from credential resolution (pi-ai never
117
+ // refreshes an unconfigured provider, so refresh outcomes alone cannot
118
+ // express not_configured), updated per refresh outcome. Readiness and
119
+ // health are synchronous derivations over this state.
120
+ let credentialResolved = false;
121
+ let lastRefreshFailed = false;
122
+ let lastRefreshDetail;
123
+ let lastAnnounced;
124
+ // The ambient base URL, captured at credential resolution (the only
125
+ // place the env seam is available); the baked settings URL outranks it.
126
+ let ambientBaseUrl;
127
+ function status() {
128
+ if (!credentialResolved)
129
+ return "not_configured";
130
+ return lastRefreshFailed ? "degraded" : "ready";
131
+ }
132
+ /** Apply a state change; `console.warn` on transitions only. */
133
+ function announce() {
134
+ const next = status();
135
+ if (next === lastAnnounced)
136
+ return;
137
+ console.warn(`provider-bifrost: bifrost ${lastAnnounced ?? "startup"} → ${next}` +
138
+ (lastRefreshDetail !== undefined && next === "degraded"
139
+ ? ` (${lastRefreshDetail})`
140
+ : ""));
141
+ lastAnnounced = next;
142
+ }
143
+ function noteCredential(resolved) {
144
+ credentialResolved = resolved;
145
+ announce();
146
+ }
147
+ function noteRefresh(ok, detail) {
148
+ lastRefreshFailed = !ok;
149
+ lastRefreshDetail = ok ? undefined : detail;
150
+ announce();
151
+ }
152
+ const provider = createProvider({
153
+ id: PROVIDER_ID,
154
+ name: PROVIDER_DISPLAY_NAME,
155
+ // UNRESOLVED_BASE_URL satisfies createProvider's shape before any
156
+ // URL exists; it is never requested — discovery refuses to run
157
+ // without a real URL, and every published model carries its own
158
+ // discovery-time baseUrl.
159
+ baseUrl: options.baseUrl ?? UNRESOLVED_BASE_URL,
160
+ // REQUIRED baseline: createProvider iterates it (omitting throws
161
+ // "baselineModels is not iterable"); empty = purely dynamic. An
162
+ // unconfigured provider boots with zero models.
163
+ models: [],
164
+ auth: {
165
+ apiKey: {
166
+ name: `Bifrost virtual key (auth.json or ${BIFROST_API_KEY_ENV})`,
167
+ resolve: async ({ credential, ctx }) => {
168
+ // Ambient base-URL capture happens on every resolution —
169
+ // the env seam is only available here, and like the key
170
+ // itself, rotating it applies on the next resolution.
171
+ const envUrl = (await ctx.env(BIFROST_BASE_URL_ENV))?.trim() || undefined;
172
+ if (envUrl !== undefined)
173
+ ambientBaseUrl = envUrl;
174
+ // Bridged auth.json credential (sigrun's bridge: the
175
+ // "default" entry first, then the provider's own) wins;
176
+ // the env var is the ambient fallback.
177
+ const stored = credential?.key?.trim() || undefined;
178
+ if (stored !== undefined) {
179
+ noteCredential(true);
180
+ return { auth: { apiKey: stored }, source: "auth.json" };
181
+ }
182
+ const envKey = (await ctx.env(BIFROST_API_KEY_ENV))?.trim() || undefined;
183
+ if (envKey !== undefined) {
184
+ noteCredential(true);
185
+ return { auth: { apiKey: envKey }, source: BIFROST_API_KEY_ENV };
186
+ }
187
+ // Unconfigured: undefined makes pi-ai skip the refresh
188
+ // phase entirely and requests fail loud.
189
+ noteCredential(false);
190
+ return undefined;
191
+ },
192
+ },
193
+ },
194
+ // Native-protocol surfaces: createProvider dispatches on model.api —
195
+ // anthropic-messages for claude-family models (wired to the gateway's
196
+ // /anthropic integration route), openai-responses for gpt-family
197
+ // models, chat completions for everything else. Which models get a
198
+ // native wire is settings-gated (nativeProtocols; both off by
199
+ // default). The map always registers all three: the registry entry
200
+ // is inert for models that never publish that api.
201
+ api: {
202
+ "openai-completions": openAICompletionsApi(),
203
+ "openai-responses": openAIResponsesApi(),
204
+ "anthropic-messages": anthropicMessagesApi(),
205
+ },
206
+ fetchModels: async (context) => {
207
+ const key = context.credential?.type === "api_key"
208
+ ? context.credential.key?.trim() || undefined
209
+ : undefined;
210
+ // URL precedence: the factory-baked settings URL wins, then the
211
+ // ambient BIFROST_BASE_URL captured at credential resolution.
212
+ // context.baseUrl is IGNORED — it echoes this provider's
213
+ // registration URL, which may be the never-requested placeholder.
214
+ const url = options.baseUrl ?? ambientBaseUrl;
215
+ try {
216
+ const models = await discoverModels(key, url, options, context.signal);
217
+ noteRefresh(true);
218
+ return models;
219
+ }
220
+ catch (cause) {
221
+ // An aborted refresh is not a failed one: pi-ai discards
222
+ // the outcome anyway, so the tracker must not move.
223
+ if (context.signal.aborted)
224
+ throw cause;
225
+ if (key === undefined)
226
+ noteCredential(false);
227
+ const message = cause instanceof Error ? cause.message : String(cause);
228
+ noteRefresh(false, message);
229
+ throw cause;
230
+ }
231
+ },
232
+ });
233
+ return {
234
+ provider,
235
+ readiness: () => ({
236
+ status: status(),
237
+ ...(lastRefreshDetail !== undefined && status() === "degraded"
238
+ ? { detail: lastRefreshDetail }
239
+ : {}),
240
+ }),
241
+ // not_configured → ok (an unconfigured optional plugin is not a
242
+ // degraded process); configured-and-failing → degraded; else ok.
243
+ health: () => (status() === "degraded" ? "degraded" : "ok"),
244
+ };
245
+ }
246
+ /**
247
+ * The sigrun extension-package factory (provider-only contributor).
248
+ * Arity 1 declares the host-taking signature — sigrun's loader passes the
249
+ * seam host only to factories that declare one (`factory.length > 0`) —
250
+ * and the factory is ASYNC because the scoped settings store is
251
+ * async-only; sigrun's loader awaits factory results.
252
+ *
253
+ * Settings are read ONCE here and BAKED into the provider (the
254
+ * restart-required semantics: the loader pre-keys the host on the
255
+ * provisional slug — this package's manifest name — so the factory-time
256
+ * read resolves `settings/provider-bifrost.json`). Parse issues log via
257
+ * `console.warn` and never quarantine (a wrong-typed key falls back to
258
+ * its default, exactly like the pi port's issue list).
259
+ */
260
+ export default async function sigrunBifrost(host) {
261
+ const raw = await host.stores.settings.get();
262
+ const issues = [];
263
+ const settings = parseBifrostSettings(raw, issues);
264
+ for (const issue of issues) {
265
+ console.warn(`provider-bifrost: settings/provider-bifrost.json: ${issue}`);
266
+ }
267
+ const plugin = createBifrostPlugin({
268
+ publishChains: settings.publishChains,
269
+ publishUpstream: settings.publishUpstream,
270
+ baseUrl: settings.baseUrl,
271
+ managementApiKey: settings.managementApiKey,
272
+ nativeProtocols: settings.nativeProtocols,
273
+ thinkingOverrides: settings.thinkingOverrides,
274
+ });
275
+ return {
276
+ name: "provider-bifrost",
277
+ providers: [{ provider: plugin.provider, readiness: plugin.readiness }],
278
+ health: plugin.health,
279
+ };
280
+ }
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
- "name": "@sigrun/provider-bifrost",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@sigrun/provider-bifrost",
3
+ "version": "0.1.1",
4
+ "description": "Sigrun provider plugin: a Bifrost LLM gateway as one dynamic provider \u2014 chain models from VK routing rules and/or the upstream catalogue, with optional per-family native protocol surfaces",
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-bifrost && 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
+ "typebox": "1.3.27",
31
+ "@types/node": "^26.6.4",
32
+ "typescript": "^7.0.2",
33
+ "vitest": "^5.0.3"
34
+ },
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "github",
38
+ "url": "git+https://github.com/sigrun-ai/sigrun-provider-bifrost.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/sigrun-ai/sigrun-provider-bifrost/issues"
42
+ },
43
+ "homepage": "https://github.com/sigrun-ai/sigrun-provider-bifrost#readme",
44
+ "publishConfig": {
45
+ "access": "public",
46
+ "provenance": true
47
+ },
48
+ "peerDependencies": {
49
+ "@earendil-works/pi-ai": "1.3.0",
50
+ "@earendil-works/pi-durable": "1.3.0",
51
+ "typebox": "1.3.27"
52
+ }
53
+ }