@sigrun/provider-bifrost 0.1.2 → 0.2.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 CHANGED
@@ -11,8 +11,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
  provenance. No functional change from 0.1.0.
12
12
 
13
13
  ## [Unreleased]
14
+
15
+ ## [0.2.1] - 2026-11-03
16
+ ### Fixed
17
+ - Replies from chain models persist the `chain/` display id, so clients can
18
+ show where a message came from. Outgoing history is replayed under the
19
+ wire name, so chain models keep their thinking blocks and signatures.
20
+ Usage is now keyed `bifrost/chain/<name>`; earlier usage stays under
21
+ `bifrost/<name>`.
22
+
23
+ ## [0.2.0] - 2026-10-30
14
24
  ### Changed
15
- - Forward-compatible with pi-durable 1.4.0 (widened peer range). No behaviour change.
25
+ - **Breaking:** chain model references change from `bifrost/bifrost/<chain>`
26
+ to `bifrost/chain/<chain>` (plugin id `chain/<chain>`). The gateway
27
+ receives the bare chain name.
28
+ - Member ids are unchanged; upstream catalog entries in the reserved
29
+ `chain/` display namespace are skipped with one warning per process.
30
+ - Forward-compatible with pi-durable 1.4.0 (widened peer range).
16
31
 
17
32
  ## [0.1.0] - 2026-10-18
18
33
  ### Changed
package/README.md CHANGED
@@ -26,11 +26,14 @@ the provider into its `Models` store at boot.
26
26
  queried too: the VK self-identifies against
27
27
  `/api/governance/virtual-keys` (the credential value matched to VK
28
28
  rows), then `/api/routing/rules?scope=virtual_key&scope_id=<vk>`
29
- yields the chain rules; every usable chain publishes as
30
- `bifrost/<chain>` (min-member context window, cost 0 by design —
31
- the gateway's own `/api/logs` is the billing system of record),
29
+ yields the chain rules; every usable chain publishes with plugin id
30
+ `chain/<name>` (displayed by sigrun as `bifrost/chain/<name>`, with
31
+ min-member context window and cost 0 by design). Requests to the
32
+ gateway carry the bare `<name>`, never the `chain/` display prefix.
33
+ The gateway's own `/api/logs` is the billing system of record,
32
34
  optionally followed by the upstream catalog (`publishUpstream`,
33
- default true).
35
+ default true). Upstream `/v1/models` entries under the reserved
36
+ `chain/` namespace are skipped as direct members.
34
37
  - **Readiness** (`not_configured` / `degraded` / `ready`, surfaced on
35
38
  sigrun's `/v1/auth/check`) — seeded from credential resolution and
36
39
  updated per refresh outcome; state transitions log via `console.warn`
@@ -126,7 +129,7 @@ and `not_configured` readiness.
126
129
  ```jsonc
127
130
  // settings.json (sigrun config dir) — the UNIFIED extensions list:
128
131
  // install + load in one place; npm and git specifiers are the same list
129
- // npm ("@sigrun/provider-bifrost@^0.1.0"), git
132
+ // npm ("@sigrun/provider-bifrost@^0.2.0"), git
130
133
  // ("git:github.com/sigrun-ai/sigrun-provider-bifrost", or any
131
134
  // https git URL with an optional @ref), or the file: form below.
132
135
  {
package/dist/catalog.d.ts CHANGED
@@ -14,6 +14,8 @@ import { type FetchJsonOptions } from "./http.js";
14
14
  import type { NativeProtocols } from "./config.js";
15
15
  /** The provider id models are registered (and stamped) under. */
16
16
  export declare const PROVIDER_ID = "bifrost";
17
+ /** Sigrun-side chain display namespace; never sent to the gateway. */
18
+ export declare const CHAIN_ID_PREFIX = "chain/";
17
19
  export declare const PROVIDER_DISPLAY_NAME = "Bifrost Gateway";
18
20
  /** Different relative to the configured /v1 base URL. */
19
21
  export declare const INFERENCE_PATH_MODELS = "models";
@@ -220,13 +222,15 @@ export declare function wireRulesToChains(rules: RoutingRule[]): Chain[];
220
222
  /** Convert a $/token wire string to pi's $/Mtok rate. */
221
223
  export declare function perMtok(dollarsPerToken: string): number | undefined;
222
224
  /**
223
- * Build one bifrost/<chain> chat model per chain. Cost is 0 by design. The
225
+ * Build one chain/<name> display model per chain. Cost is 0 by design. The
224
226
  * wire follows the chain's model-family (native protocol end to end): the
225
227
  * chain name decides — fallback legs of a chain are same-model by
226
228
  * construction, and the gateway translates any client wire for legs that
227
229
  * differ.
228
230
  */
229
231
  export declare function buildChainModels(chains: Chain[], catalog: CatalogModel[], providerSlug: string, baseUrl: string, native?: NativeProtocols): GatewayModel[];
232
+ /** Catalog entries that collide with the reserved chain display namespace. */
233
+ export declare function collidingMemberIds(catalog: readonly CatalogModel[]): string[];
230
234
  /**
231
235
  * Build direct-selectable models for the /v1/models catalog with real
232
236
  * per-member pricing, ordered with chain members first.
package/dist/catalog.js CHANGED
@@ -18,6 +18,8 @@ import { fetchJson } from "./http.js";
18
18
  /* ---------- gateway constants (pi port's src/bifrost.ts) ---------- */
19
19
  /** The provider id models are registered (and stamped) under. */
20
20
  export const PROVIDER_ID = "bifrost";
21
+ /** Sigrun-side chain display namespace; never sent to the gateway. */
22
+ export const CHAIN_ID_PREFIX = "chain/";
21
23
  export const PROVIDER_DISPLAY_NAME = "Bifrost Gateway";
22
24
  /** Different relative to the configured /v1 base URL. */
23
25
  export const INFERENCE_PATH_MODELS = "models";
@@ -78,7 +80,7 @@ function bareModelName(id) {
78
80
  // Strip legacy managed prefixes (gateway/, bifrost/) if a rule CEL ever
79
81
  // produces a prefixed bare name; MANAGED_PREFIXES itself is private to CEL
80
82
  // decoding, so this local strip keeps the denylist honest at both call sites.
81
- return noPrefix.replace(/^(gateway|bifrost)\//, "");
83
+ return noPrefix.replace(/^(gateway|bifrost|chain)\//, "");
82
84
  }
83
85
  function inputFor(id) {
84
86
  return TEXT_ONLY_MODELS.has(bareModelName(id)) ? [...TEXT_ONLY_MODALITIES] : [...INPUT_MODALITIES];
@@ -231,7 +233,7 @@ export async function wireDiscovery(managementToken, vkValue, baseUrl, scopeId,
231
233
  }
232
234
  /* ---------- chain decoding (pure) ---------- */
233
235
  /** Managed model-id prefixes that strip to the bare chain name. */
234
- const MANAGED_PREFIXES = ["bifrost", "gateway"];
236
+ const MANAGED_PREFIXES = ["bifrost", "gateway", "chain"];
235
237
  /**
236
238
  * Extract a chain model name from a routing rule's CEL expression.
237
239
  * Recognizes `model == "x"` arms and strips managed prefixes (`bifrost/`,
@@ -313,7 +315,7 @@ function thinkingLevelMap(api, levels) {
313
315
  return map;
314
316
  }
315
317
  /**
316
- * Build one bifrost/<chain> chat model per chain. Cost is 0 by design. The
318
+ * Build one chain/<name> display model per chain. Cost is 0 by design. The
317
319
  * wire follows the chain's model-family (native protocol end to end): the
318
320
  * chain name decides — fallback legs of a chain are same-model by
319
321
  * construction, and the gateway translates any client wire for legs that
@@ -328,7 +330,7 @@ export function buildChainModels(chains, catalog, providerSlug, baseUrl, native)
328
330
  .filter((v) => typeof v === "number");
329
331
  const contextWindow = contexts.length > 0 ? Math.min(...contexts) : CONTEXT_WINDOW_FALLBACK;
330
332
  return buildModel({
331
- id: `${providerSlug}/${chain.name}`,
333
+ id: `${CHAIN_ID_PREFIX}${chain.name}`,
332
334
  name: chain.name,
333
335
  familyId: chain.name,
334
336
  provider: providerSlug,
@@ -383,6 +385,10 @@ function buildModel(args) {
383
385
  };
384
386
  return model;
385
387
  }
388
+ /** Catalog entries that collide with the reserved chain display namespace. */
389
+ export function collidingMemberIds(catalog) {
390
+ return catalog.filter((entry) => entry.id.startsWith(CHAIN_ID_PREFIX)).map((entry) => entry.id);
391
+ }
386
392
  /**
387
393
  * Build direct-selectable models for the /v1/models catalog with real
388
394
  * per-member pricing, ordered with chain members first.
@@ -407,6 +413,8 @@ export function buildMemberModels(chains, catalog, providerSlug, baseUrl, thinki
407
413
  }
408
414
  const models = [];
409
415
  for (const memberId of memberIds) {
416
+ if (memberId.startsWith(CHAIN_ID_PREFIX))
417
+ continue;
410
418
  const entry = byId.get(memberId);
411
419
  if (!entry)
412
420
  continue; // chain member absent from /v1/models — skip rather than guess
package/dist/config.d.ts CHANGED
@@ -36,7 +36,7 @@ export interface NativeProtocols {
36
36
  messagesModels?: string[];
37
37
  }
38
38
  export interface BifrostSettings {
39
- /** Emit chain models (bifrost/<chain>) from the VK's routing rules. */
39
+ /** Emit chain models (chain/<name>) from the VK's routing rules. */
40
40
  publishChains: boolean;
41
41
  /** Emit all upstream models from /v1/models as direct-selectable entries. */
42
42
  publishUpstream: boolean;
package/dist/index.d.ts CHANGED
@@ -42,11 +42,12 @@
42
42
  */
43
43
  import { type Provider } from "@earendil-works/pi-ai/models";
44
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 };
45
+ import { PROVIDER_DISPLAY_NAME, PROVIDER_ID, UNRESOLVED_BASE_URL, CHAIN_ID_PREFIX, type GatewayModel } from "./catalog.js";
46
+ export { PROVIDER_ID, PROVIDER_DISPLAY_NAME, UNRESOLVED_BASE_URL, CHAIN_ID_PREFIX };
47
47
  export { parseBifrostSettings, BIFROST_BASE_URL_ENV } from "./config.js";
48
48
  export type { BifrostSettings, NativeProtocols } from "./config.js";
49
49
  export type { GatewayModel, SurfaceApi, CatalogModel, Chain, RoutingRule } from "./catalog.js";
50
+ export { chainWireName, withWireNames } from "./wire.js";
50
51
  /** Ambient fallback for the virtual key when auth.json holds no credential. */
51
52
  export declare const BIFROST_API_KEY_ENV = "BIFROST_API_KEY";
52
53
  /**
package/dist/index.js CHANGED
@@ -45,16 +45,25 @@ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completio
45
45
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
46
46
  import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
47
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";
48
+ import { PROVIDER_DISPLAY_NAME, PROVIDER_ID, UNRESOLVED_BASE_URL, CHAIN_ID_PREFIX, buildAllModels, collidingMemberIds, wireCatalog, wireDiscovery, wireRulesToChains, wireScopeId, } from "./catalog.js";
49
+ import { withWireNames } from "./wire.js";
49
50
  // Re-exported gateway constants (the pi port's src/bifrost.ts surface):
50
51
  // the provider id is part of this plugin's public contract — sigrun's
51
52
  // 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
+ export { PROVIDER_ID, PROVIDER_DISPLAY_NAME, UNRESOLVED_BASE_URL, CHAIN_ID_PREFIX };
53
54
  // Re-exported discovery/config types so consumers (and tests) need no
54
55
  // deep imports.
55
56
  export { parseBifrostSettings, BIFROST_BASE_URL_ENV } from "./config.js";
57
+ export { chainWireName, withWireNames } from "./wire.js";
56
58
  /** Ambient fallback for the virtual key when auth.json holds no credential. */
57
59
  export const BIFROST_API_KEY_ENV = "BIFROST_API_KEY";
60
+ let chainNamespaceWarned = false;
61
+ function warnOnChainNamespaceCollision(catalog) {
62
+ if (chainNamespaceWarned || collidingMemberIds(catalog).length === 0)
63
+ return;
64
+ console.warn('bifrost: upstream provider "chain" collides with the chain display namespace; its models are skipped');
65
+ chainNamespaceWarned = true;
66
+ }
58
67
  /**
59
68
  * Compose the gateway model list from live Bifrost state (the ported
60
69
  * `discoverModels` with sigrun's failure semantics — throws instead of
@@ -85,6 +94,7 @@ export async function discoverModels(token, baseUrl, options, signal) {
85
94
  throw new Error("provider-bifrost: credential does not match any virtual key in /api/governance/virtual-keys");
86
95
  }
87
96
  const { catalog, rules } = await wireDiscovery(managementToken, token, baseUrl, scopeId, signal);
97
+ warnOnChainNamespaceCollision(catalog);
88
98
  const chains = wireRulesToChains(rules);
89
99
  if (chains.length === 0) {
90
100
  throw new Error(`provider-bifrost: no chain routing rules found for ${scopeId} — is chains-first routing configured for this VK?`);
@@ -100,6 +110,7 @@ export async function discoverModels(token, baseUrl, options, signal) {
100
110
  throw new Error("provider-bifrost: publishChains and publishUpstream are both false — nothing to publish");
101
111
  }
102
112
  const catalog = await wireCatalog(token, baseUrl, signal);
113
+ warnOnChainNamespaceCollision(catalog);
103
114
  const models = buildAllModels([], catalog, PROVIDER_ID, baseUrl, false, true, options.thinkingOverrides, options.nativeProtocols);
104
115
  if (models.length === 0) {
105
116
  throw new Error("provider-bifrost: /v1/models returned no usable entries; refusing to publish an empty catalog");
@@ -124,6 +135,7 @@ export function createBifrostPlugin(options) {
124
135
  // The ambient base URL, captured at credential resolution (the only
125
136
  // place the env seam is available); the baked settings URL outranks it.
126
137
  let ambientBaseUrl;
138
+ const wireNames = new Map();
127
139
  function status() {
128
140
  if (!credentialResolved)
129
141
  return "not_configured";
@@ -199,9 +211,9 @@ export function createBifrostPlugin(options) {
199
211
  // default). The map always registers all three: the registry entry
200
212
  // is inert for models that never publish that api.
201
213
  api: {
202
- "openai-completions": openAICompletionsApi(),
203
- "openai-responses": openAIResponsesApi(),
204
- "anthropic-messages": anthropicMessagesApi(),
214
+ "openai-completions": withWireNames(openAICompletionsApi(), wireNames),
215
+ "openai-responses": withWireNames(openAIResponsesApi(), wireNames),
216
+ "anthropic-messages": withWireNames(anthropicMessagesApi(), wireNames),
205
217
  },
206
218
  fetchModels: async (context) => {
207
219
  const key = context.credential?.type === "api_key"
@@ -214,6 +226,11 @@ export function createBifrostPlugin(options) {
214
226
  const url = options.baseUrl ?? ambientBaseUrl;
215
227
  try {
216
228
  const models = await discoverModels(key, url, options, context.signal);
229
+ wireNames.clear();
230
+ for (const model of models) {
231
+ if (model.id.startsWith(CHAIN_ID_PREFIX))
232
+ wireNames.set(model.id, model.name);
233
+ }
217
234
  noteRefresh(true);
218
235
  return models;
219
236
  }
package/dist/wire.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { Api, AssistantMessageEventStream, Model, ProviderStreams } from "@earendil-works/pi-ai";
2
+ /** Resolve the gateway's model name from a sigrun-side display id. */
3
+ export declare function chainWireName(id: string, known: ReadonlyMap<string, string>): string;
4
+ /**
5
+ * Restore the sigrun-side display id on a reply.
6
+ *
7
+ * The gateway names its reply after the model it was asked for — the bare
8
+ * wire name (`acme-flash`) — and pi-ai copies that name onto the reply's
9
+ * messages, so a persisted message from a chain model would forget where it
10
+ * came from. Every event is forwarded, the terminal `done`/`error` included,
11
+ * because pi-ai derives `result()` (and `durationMs`) from that event.
12
+ *
13
+ * `model` is the request's model; it is optional only because the display id
14
+ * and wire name already describe the reply. Supply it so a stream that breaks
15
+ * outright can report an accurate `api`/`provider`.
16
+ */
17
+ export declare function withDisplayId(stream: AssistantMessageEventStream, displayId: string, wireName: string, model?: Model<Api>): AssistantMessageEventStream;
18
+ /** Keep display ids at the provider boundary and bare names on the gateway wire. */
19
+ export declare function withWireNames(streams: ProviderStreams, known: ReadonlyMap<string, string>): ProviderStreams;
package/dist/wire.js ADDED
@@ -0,0 +1,159 @@
1
+ import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
2
+ import { CHAIN_ID_PREFIX, PROVIDER_ID } from "./catalog.js";
3
+ /** Resolve the gateway's model name from a sigrun-side display id. */
4
+ export function chainWireName(id, known) {
5
+ const discovered = known.get(id);
6
+ if (discovered !== undefined)
7
+ return discovered;
8
+ return id.startsWith(CHAIN_ID_PREFIX) ? id.slice(CHAIN_ID_PREFIX.length) : id;
9
+ }
10
+ function wireModel(model, known) {
11
+ return { ...model, id: chainWireName(model.id, known) };
12
+ }
13
+ /**
14
+ * Fold this model's display id back to the wire name on outgoing history.
15
+ *
16
+ * pi-ai replays an assistant turn as "the same model" only when its provider,
17
+ * api and model all match the request (`transformMessages`), and that gate is
18
+ * what keeps thinking blocks with their signatures, tool-call thought
19
+ * signatures, and tool-call ids. The inner implementation replays against the
20
+ * WIRE model, so a persisted `chain/` reply would otherwise read as a foreign
21
+ * model's turn and lose every one of those. Only this provider's own turns for
22
+ * this exact model are renamed: another model's (or another provider's)
23
+ * history keeps its cross-model semantics.
24
+ */
25
+ function replayContext(context, model, wireName) {
26
+ let changed = false;
27
+ const messages = context.messages.map((message) => {
28
+ if (message.role !== "assistant")
29
+ return message;
30
+ if (message.provider !== model.provider || message.model !== model.id)
31
+ return message;
32
+ changed = true;
33
+ return { ...message, model: wireName };
34
+ });
35
+ // Untouched histories cross by reference, so a member request costs nothing.
36
+ return changed ? { ...context, messages } : context;
37
+ }
38
+ /** The message an event carries, whichever field its variant uses. */
39
+ function eventMessage(event) {
40
+ switch (event.type) {
41
+ case "done":
42
+ return event.message;
43
+ case "error":
44
+ return event.error;
45
+ default:
46
+ return event.partial;
47
+ }
48
+ }
49
+ /**
50
+ * The message a stream that broke outright reports.
51
+ *
52
+ * Mirrors pi-ai's own setup-error message (`api/lazy.js`): an empty assistant
53
+ * reply whose `stopReason` is `"error"` and whose text names the cause. pi-ai
54
+ * never replays an errored assistant turn, so this message only carries the
55
+ * failure to the consumer — but it MUST exist, because `EventStream.end()`
56
+ * without a result leaves `result()` unresolved forever.
57
+ */
58
+ function failureMessage(cause, displayId, model) {
59
+ return {
60
+ role: "assistant",
61
+ content: [],
62
+ api: model?.api ?? "unknown",
63
+ provider: model?.provider ?? PROVIDER_ID,
64
+ model: displayId,
65
+ usage: {
66
+ input: 0,
67
+ output: 0,
68
+ cacheRead: 0,
69
+ cacheWrite: 0,
70
+ totalTokens: 0,
71
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
72
+ },
73
+ stopReason: "error",
74
+ errorMessage: cause instanceof Error ? cause.message : String(cause),
75
+ timestamp: Date.now(),
76
+ };
77
+ }
78
+ /**
79
+ * Restore the sigrun-side display id on a reply.
80
+ *
81
+ * The gateway names its reply after the model it was asked for — the bare
82
+ * wire name (`acme-flash`) — and pi-ai copies that name onto the reply's
83
+ * messages, so a persisted message from a chain model would forget where it
84
+ * came from. Every event is forwarded, the terminal `done`/`error` included,
85
+ * because pi-ai derives `result()` (and `durationMs`) from that event.
86
+ *
87
+ * `model` is the request's model; it is optional only because the display id
88
+ * and wire name already describe the reply. Supply it so a stream that breaks
89
+ * outright can report an accurate `api`/`provider`.
90
+ */
91
+ export function withDisplayId(stream, displayId, wireName, model) {
92
+ const restored = createAssistantMessageEventStream();
93
+ void (async () => {
94
+ try {
95
+ let result;
96
+ for await (const event of stream) {
97
+ const message = eventMessage(event);
98
+ if (message.model === wireName)
99
+ message.model = displayId;
100
+ // pi-ai resolves result() from the terminal event, so forward it
101
+ // untouched (the id is already restored) and end() after it.
102
+ if (event.type === "done")
103
+ result = event.message;
104
+ else if (event.type === "error")
105
+ result = event.error;
106
+ restored.push(event);
107
+ }
108
+ // A stream can settle through end(result) without any event; pi-ai's
109
+ // own forwarder reads that result so result() never hangs.
110
+ if (result === undefined)
111
+ result = await stream.result();
112
+ if (result !== undefined && result.model === wireName)
113
+ result.model = displayId;
114
+ restored.end(result);
115
+ }
116
+ catch (cause) {
117
+ // pi-ai streams terminate with an error event and their iterator does
118
+ // not throw, so this is an upstream contract violation. Report it the
119
+ // way pi-ai reports a failed setup — a terminal error event — and
120
+ // settle with that message: ending without a result would leave every
121
+ // `await stream.result()` waiting forever, and the cause would be lost.
122
+ const failure = failureMessage(cause, displayId, model);
123
+ restored.push({ type: "error", reason: "error", error: failure });
124
+ restored.end(failure);
125
+ }
126
+ })();
127
+ return restored;
128
+ }
129
+ /** Keep display ids at the provider boundary and bare names on the gateway wire. */
130
+ export function withWireNames(streams, known) {
131
+ /** Replies are named after the request; a stripped id must come back whole. */
132
+ const restoreReply = (model, wireName, stream) => (wireName === model.id ? stream : withDisplayId(stream, model.id, wireName, model));
133
+ /**
134
+ * Cross the boundary once: the request names the wire model, the history
135
+ * replays under that same name, and the reply keeps the display id.
136
+ */
137
+ const delegate = (model, context, call) => {
138
+ const wireName = chainWireName(model.id, known);
139
+ const history = wireName === model.id ? context : replayContext(context, model, wireName);
140
+ return restoreReply(model, wireName, call(wireModel(model, known), history));
141
+ };
142
+ const wrapped = {
143
+ stream: (model, context, options) => delegate(model, context, (wire, history) => streams.stream(wire, history, options)),
144
+ streamSimple: (model, context, options) => delegate(model, context, (wire, history) => streams.streamSimple(wire, history, options)),
145
+ };
146
+ if (streams.fetchDeferred !== undefined) {
147
+ const fetchDeferred = streams.fetchDeferred;
148
+ // A deferred fetch replays no history: only the request and reply cross.
149
+ wrapped.fetchDeferred = (model, handle, options) => {
150
+ const wireName = chainWireName(model.id, known);
151
+ return restoreReply(model, wireName, fetchDeferred.call(streams, wireModel(model, known), handle, options));
152
+ };
153
+ }
154
+ if (streams.cancelDeferred !== undefined) {
155
+ const cancelDeferred = streams.cancelDeferred;
156
+ wrapped.cancelDeferred = (model, handle, options) => cancelDeferred.call(streams, wireModel(model, known), handle, options);
157
+ }
158
+ return wrapped;
159
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sigrun/provider-bifrost",
3
- "version": "0.1.2",
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",
3
+ "version": "0.2.1",
4
+ "description": "Sigrun provider plugin: a Bifrost LLM gateway as one dynamic provider — chain models from VK routing rules and/or the upstream catalogue, with optional per-family native protocol surfaces",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=22.19.0"
@@ -25,8 +25,8 @@
25
25
  "prepare": "npm run build"
26
26
  },
27
27
  "devDependencies": {
28
- "@earendil-works/pi-ai": "1.3.0",
29
- "@earendil-works/pi-durable": "1.3.0",
28
+ "@earendil-works/pi-ai": "1.4.0",
29
+ "@earendil-works/pi-durable": "1.4.0",
30
30
  "typebox": "1.3.27",
31
31
  "@types/node": "^26.6.4",
32
32
  "typescript": "^7.0.2",