@aliou/pi-neuralwatt 0.16.3 → 0.18.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.
@@ -30,7 +30,10 @@ import {
30
30
  parseRateLimitHeaders,
31
31
  } from "./rate-limit-error";
32
32
  import { updateQuotasFromSseComment } from "./sse-quotas";
33
- import { wrapNeuralwattStreamSimple } from "./stream-simple";
33
+ import {
34
+ type NeuralwattStreamCallbacks,
35
+ wrapNeuralwattStreamSimple,
36
+ } from "./stream-simple";
34
37
 
35
38
  const HEADER_EMIT_THROTTLE_MS = 5_000;
36
39
 
@@ -42,7 +45,7 @@ function emitConfigUpdated(pi: ExtensionAPI): void {
42
45
 
43
46
  function registerNeuralwattProvider(
44
47
  pi: ExtensionAPI,
45
- onSseQuota: (line: string) => void,
48
+ streamCallbacks: NeuralwattStreamCallbacks,
46
49
  ): void {
47
50
  const staticModels = buildNeuralwattProviderModels();
48
51
 
@@ -51,7 +54,7 @@ function registerNeuralwattProvider(
51
54
  const streamSimple = baseStreamSimple
52
55
  ? (wrapNeuralwattStreamSimple(
53
56
  baseStreamSimple as never,
54
- onSseQuota,
57
+ streamCallbacks,
55
58
  ) as never)
56
59
  : undefined;
57
60
 
@@ -60,7 +63,7 @@ function registerNeuralwattProvider(
60
63
  const messagesStreamSimple = messagesBaseStreamSimple
61
64
  ? (wrapNeuralwattStreamSimple(
62
65
  messagesBaseStreamSimple as never,
63
- onSseQuota,
66
+ streamCallbacks,
64
67
  ) as never)
65
68
  : undefined;
66
69
 
@@ -101,7 +104,17 @@ export default async function (pi: ExtensionAPI) {
101
104
  emitQuotas(quotas, "sse");
102
105
  };
103
106
 
104
- registerNeuralwattProvider(pi, handleSseQuota);
107
+ // Rate-limit info captured by the per-request fetch wrapper from the most
108
+ // recent 429 response. Used in message_end to rewrite the generic error
109
+ // text with actionable details from Neuralwatt's response headers.
110
+ let pendingRateLimitInfo: NeuralwattRateLimitInfo | undefined;
111
+
112
+ registerNeuralwattProvider(pi, {
113
+ onSseQuota: handleSseQuota,
114
+ onRateLimit: (info) => {
115
+ pendingRateLimitInfo = info;
116
+ },
117
+ });
105
118
 
106
119
  const loadedFeatures = new Set<NeuralwattFeatureId>();
107
120
 
@@ -125,10 +138,6 @@ export default async function (pi: ExtensionAPI) {
125
138
  pi.events.emit(NEURALWATT_QUOTAS_UPDATED_EVENT, { quotas, source });
126
139
  }
127
140
 
128
- // Stored rate-limit info from the most recent 429 response.
129
- // Used in message_end to rewrite the generic error text with
130
- // actionable details from Neuralwatt's response headers.
131
- let pendingRateLimitInfo: NeuralwattRateLimitInfo | undefined;
132
141
  let currentModelRegistry: ModelRegistry | undefined;
133
142
 
134
143
  pi.on("message_end", (event, ctx) => {
@@ -148,14 +157,14 @@ export default async function (pi: ExtensionAPI) {
148
157
  return { message };
149
158
  }
150
159
 
151
- // Fallback for 429s where no layer-specific headers were captured. The
152
- // streamSimple wrap (wrapNeuralwattStreamSimple) already formats a
153
- // detailed message via formatRateLimitError when it captures headers;
154
- // detect that case by the `"429 rate limit:"` prefix it emits and leave
155
- // it untouched. This branch only fires for genuinely headerless 429s
156
- // (e.g. anonymous playground limits, or a 429 from infra in front of
157
- // Neuralwatt), since after_provider_response cannot observe 429s — the
158
- // OpenAI SDK throws before Pi's onResponse hook runs.
160
+ // Fallback for 429s where no layer-specific headers were captured (the
161
+ // fetch wrapper reported `undefined` via onRateLimit, or the 429 came
162
+ // from infra in front of Neuralwatt). The `"429 rate limit:"` prefix
163
+ // guard skips messages the first branch already rewrote. On the
164
+ // openai-completions surface after_provider_response cannot observe
165
+ // 429s — the OpenAI SDK throws before Pi's onResponse hook runs — so
166
+ // this is the only repair path there; on anthropic-messages
167
+ // after_provider_response also feeds pendingRateLimitInfo directly.
159
168
  if (
160
169
  event.message.role === "assistant" &&
161
170
  event.message.stopReason === "error" &&
@@ -4,8 +4,24 @@ import type {
4
4
  NeuralwattReasoningEffort,
5
5
  } from "../../../src/types/models-api";
6
6
 
7
+ export type ProviderChatModelConfig = Extract<
8
+ ProviderModelConfig,
9
+ { type?: "chat" }
10
+ >;
11
+
12
+ export type ProviderClassifierModelConfig = Extract<
13
+ ProviderModelConfig,
14
+ { type: "classifier" }
15
+ >;
16
+
17
+ export const NEURALWATT_SYSTEM_ONE_API = "typesafe-system-one" as const;
18
+
19
+ export type NeuralwattClassifierModel = ProviderClassifierModelConfig & {
20
+ api: typeof NEURALWATT_SYSTEM_ONE_API;
21
+ };
22
+
7
23
  export type ThinkingLevelMap = NonNullable<
8
- ProviderModelConfig["thinkingLevelMap"]
24
+ ProviderChatModelConfig["thinkingLevelMap"]
9
25
  >;
10
26
 
11
27
  /**
@@ -13,7 +29,7 @@ export type ThinkingLevelMap = NonNullable<
13
29
  * retained for anthropic-messages map derivation. Rides the models store
14
30
  * (JSON passthrough); stripped from stamped runtime models.
15
31
  */
16
- export type NeuralwattCompiledModel = ProviderModelConfig & {
32
+ export type NeuralwattCompiledModel = ProviderChatModelConfig & {
17
33
  reasoningContract?: NeuralwattReasoningMapSource;
18
34
  };
19
35
 
@@ -157,7 +173,7 @@ export function buildNeuralwattModel(
157
173
  ): NeuralwattCompiledModel {
158
174
  const vision = variant.vision ?? family.vision;
159
175
 
160
- const compat: NonNullable<ProviderModelConfig["compat"]> = {
176
+ const compat: NonNullable<ProviderChatModelConfig["compat"]> = {
161
177
  supportsDeveloperRole: false,
162
178
  maxTokensField: "max_tokens",
163
179
  };
@@ -1,19 +1,45 @@
1
- import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
2
1
  import type { NeuralwattApiModel } from "../../../src/types/models-api";
3
2
  import {
4
3
  buildThinkingLevelMap,
4
+ NEURALWATT_SYSTEM_ONE_API,
5
+ type NeuralwattClassifierModel,
5
6
  type NeuralwattCompiledModel,
7
+ type ProviderChatModelConfig,
6
8
  resolveMaxTokens,
7
9
  type ThinkingLevelMap,
8
10
  } from "./build";
9
11
  import { NEURALWATT_MODELS } from "./public-models";
10
12
 
11
- export type NeuralwattModel = NeuralwattCompiledModel;
13
+ export type NeuralwattChatModel = NeuralwattCompiledModel;
14
+
15
+ export type NeuralwattModel = NeuralwattChatModel | NeuralwattClassifierModel;
16
+
17
+ export function isNeuralwattClassifierModel(
18
+ model: NeuralwattModel,
19
+ ): model is NeuralwattClassifierModel {
20
+ return model.type === "classifier";
21
+ }
22
+
23
+ export function partitionNeuralwattModels(models: readonly NeuralwattModel[]): {
24
+ chat: NeuralwattChatModel[];
25
+ classifiers: NeuralwattClassifierModel[];
26
+ } {
27
+ const chat: NeuralwattChatModel[] = [];
28
+ const classifiers: NeuralwattClassifierModel[] = [];
29
+ for (const model of models) {
30
+ if (isNeuralwattClassifierModel(model)) {
31
+ classifiers.push(model);
32
+ } else {
33
+ chat.push(model);
34
+ }
35
+ }
36
+ return { chat, classifiers };
37
+ }
12
38
 
13
39
  // Chat-template thinking: the API exposes a `reasoning` block, but the
14
40
  // underlying mechanism is chat_template_kwargs, so Pi needs the mapping.
15
41
  const COMPAT_OVERRIDES: Partial<
16
- Record<string, Partial<NonNullable<ProviderModelConfig["compat"]>>>
42
+ Record<string, Partial<NonNullable<ProviderChatModelConfig["compat"]>>>
17
43
  > = {
18
44
  "Qwen/Qwen3.8-27B-FP8": {
19
45
  thinkingFormat: "chat-template",
@@ -23,7 +49,36 @@ const COMPAT_OVERRIDES: Partial<
23
49
  },
24
50
  };
25
51
 
26
- function apiModelToProviderModel(model: NeuralwattApiModel): NeuralwattModel {
52
+ function apiModelToClassifierModel(
53
+ model: NeuralwattApiModel,
54
+ ): NeuralwattClassifierModel {
55
+ const meta = model.metadata;
56
+ if (!meta)
57
+ throw new Error(
58
+ `Neuralwatt API returned model "${model.id}" without metadata`,
59
+ );
60
+
61
+ return {
62
+ type: "classifier",
63
+ api: NEURALWATT_SYSTEM_ONE_API,
64
+ id: model.id,
65
+ name: meta.display_name ?? model.id,
66
+ input: meta.capabilities.vision
67
+ ? (["text", "image"] as const)
68
+ : (["text"] as const),
69
+ cost: {
70
+ input: meta.pricing.input_per_million,
71
+ output: meta.pricing.output_per_million,
72
+ cacheRead: meta.pricing.cached_input_per_million ?? 0,
73
+ cacheWrite: meta.pricing.cached_output_per_million ?? 0,
74
+ },
75
+ contextWindow: model.max_model_len,
76
+ };
77
+ }
78
+
79
+ function apiModelToProviderModel(
80
+ model: NeuralwattApiModel,
81
+ ): NeuralwattChatModel {
27
82
  const meta = model.metadata;
28
83
  if (!meta)
29
84
  throw new Error(
@@ -32,7 +87,7 @@ function apiModelToProviderModel(model: NeuralwattApiModel): NeuralwattModel {
32
87
 
33
88
  const reasoning = meta.capabilities.reasoning;
34
89
 
35
- const compat: NonNullable<ProviderModelConfig["compat"]> = {
90
+ const compat: NonNullable<ProviderChatModelConfig["compat"]> = {
36
91
  supportsDeveloperRole: meta.capabilities.developer_role,
37
92
  maxTokensField: "max_tokens",
38
93
  ...COMPAT_OVERRIDES[model.id],
@@ -83,13 +138,18 @@ export function buildNeuralwattProviderModelsFromApi(
83
138
  m.metadata &&
84
139
  !m.metadata.deprecated &&
85
140
  !m.metadata.pricing.pricing_tbd &&
86
- // Exclude non-chat models (e.g. embeddings) by task
87
141
  !(
88
142
  m.metadata.capabilities.task &&
89
- !["chat", "completions"].includes(m.metadata.capabilities.task)
143
+ !["chat", "completions", "decision"].includes(
144
+ m.metadata.capabilities.task,
145
+ )
90
146
  ),
91
147
  )
92
- .map(apiModelToProviderModel);
148
+ .map((m) =>
149
+ m.metadata?.capabilities.task === "decision"
150
+ ? apiModelToClassifierModel(m)
151
+ : apiModelToProviderModel(m),
152
+ );
93
153
  return models;
94
154
  }
95
155
 
@@ -1,8 +1,8 @@
1
- import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
2
1
  import {
3
2
  buildNeuralwattFamily,
4
3
  type NeuralwattModelFamily,
5
4
  type NeuralwattVariantSpec,
5
+ type ProviderChatModelConfig,
6
6
  } from "./build";
7
7
 
8
8
  // Public models returned by https://api.neuralwatt.com/v1/models.
@@ -105,17 +105,6 @@ const KIMI_K3: NeuralwattModelFamily = {
105
105
  },
106
106
  };
107
107
 
108
- // MoonshotAI. K2.7 Code has mandatory reasoning with no selectable efforts
109
- // (`supported_efforts: []`), so `buildThinkingLevelMap` nulls out every level.
110
- const KIMI_K2_7_CODE: NeuralwattModelFamily = {
111
- cost: { input: 0.95, output: 4.0, cacheRead: 0.095 },
112
- vision: true,
113
- reasoningMetadata: {
114
- supported_efforts: [],
115
- mandatory: true,
116
- },
117
- };
118
-
119
108
  // Xiaomi. MiMo-V2.6-Pro only has two reasoning behaviors: `high` (default)
120
109
  // and `none`; the API aliases every other effort onto those, with `minimal`
121
110
  // landing on `none`.
@@ -336,35 +325,6 @@ const FAMILIES: [NeuralwattModelFamily, NeuralwattVariantSpec[]][] = [
336
325
  },
337
326
  ],
338
327
  ],
339
- [
340
- KIMI_K2_7_CODE,
341
- [
342
- {
343
- id: "kimi-k2.7-code",
344
- name: "Kimi K2.7 Code",
345
- contextWindow: 262128,
346
- maxOutputTokens: null,
347
- reasoning: true,
348
- },
349
- {
350
- // K2.7 Code cannot disable thinking; the -fast variant caps the
351
- // reasoning budget (~64 tokens) rather than turning it off.
352
- id: "kimi-k2.7-code-fast",
353
- name: "Kimi K2.7 Code Fast",
354
- contextWindow: 262128,
355
- maxOutputTokens: null,
356
- reasoning: true,
357
- },
358
- {
359
- id: "kimi-k2.7-code-flex",
360
- name: "Kimi K2.7 Code (flex)",
361
- contextWindow: 262128,
362
- maxOutputTokens: null,
363
- reasoning: true,
364
- costMultiplier: 0.65,
365
- },
366
- ],
367
- ],
368
328
  [
369
329
  MIMO_V2_6_PRO,
370
330
  [
@@ -375,6 +335,14 @@ const FAMILIES: [NeuralwattModelFamily, NeuralwattVariantSpec[]][] = [
375
335
  maxOutputTokens: 393216,
376
336
  reasoning: true,
377
337
  },
338
+ {
339
+ id: "mimo-v2.6-pro-flex",
340
+ name: "MiMo-V2.6-Pro (flex)",
341
+ contextWindow: 1048560,
342
+ maxOutputTokens: 393216,
343
+ reasoning: true,
344
+ costMultiplier: 0.65,
345
+ },
378
346
  ],
379
347
  ],
380
348
  [
@@ -492,6 +460,6 @@ const FAMILIES: [NeuralwattModelFamily, NeuralwattVariantSpec[]][] = [
492
460
  // `costMultiplier: 0.65` per variant.
493
461
  // https://docs.neuralwatt.com/guides/flex-tier.md
494
462
 
495
- export const NEURALWATT_MODELS: ProviderModelConfig[] = FAMILIES.flatMap(
463
+ export const NEURALWATT_MODELS: ProviderChatModelConfig[] = FAMILIES.flatMap(
496
464
  ([family, variants]) => buildNeuralwattFamily(family, variants),
497
465
  );
@@ -1,7 +1,9 @@
1
1
  import type {
2
+ AnyModel,
2
3
  ModelsStoreEntry,
3
4
  RefreshModelsContext,
4
5
  } from "@earendil-works/pi-ai";
6
+ import { configuredApiBaseUrl } from "../../../src/config/loader";
5
7
  import type { NeuralwattApiModel } from "../../../src/types/models-api";
6
8
  import type {
7
9
  buildNeuralwattProviderModels,
@@ -17,7 +19,8 @@ export const MODEL_STORE_TTL_MS = 4 * 60 * 60 * 1000;
17
19
  * key-scoped catalog (preview, grant-gated, private models), so an entry
18
20
  * stamped "public" must not shadow a keyed refresh — and a key-scoped entry
19
21
  * must not be replayed for an anonymous user. Matches the anonymous-key
20
- * convention in src/lib/neuralwatt-api.ts (authHeaders).
22
+ * convention in src/lib/neuralwatt-api.ts (authHeaders). The base URL is part
23
+ * of the key because a gateway can serve a different catalog than upstream.
21
24
  */
22
25
  const CATALOG_SCOPE_VERSION = "v2";
23
26
  type CatalogScope = "public" | "key";
@@ -30,18 +33,15 @@ function catalogScope(apiKey: string | undefined): CatalogScope {
30
33
  : "public";
31
34
  }
32
35
 
33
- function storedCatalogKey(entry: ScopedModelsStoreEntry): string | undefined {
34
- return entry.catalogKey;
36
+ function catalogKey(scope: CatalogScope): string {
37
+ return `${scope} ${CATALOG_SCOPE_VERSION} ${configuredApiBaseUrl()}`;
35
38
  }
36
39
 
37
40
  function catalogKeyMatches(
38
41
  entry: ScopedModelsStoreEntry | undefined,
39
42
  scope: CatalogScope,
40
43
  ): boolean {
41
- return (
42
- entry !== undefined &&
43
- storedCatalogKey(entry) === `${scope} ${CATALOG_SCOPE_VERSION}`
44
- );
44
+ return entry !== undefined && entry.catalogKey === catalogKey(scope);
45
45
  }
46
46
 
47
47
  export type FetchNeuralwattApiModels = (
@@ -49,6 +49,10 @@ export type FetchNeuralwattApiModels = (
49
49
  signal?: AbortSignal,
50
50
  ) => Promise<readonly NeuralwattApiModel[]>;
51
51
 
52
+ function asStoredChatModels(models: readonly AnyModel[]): NeuralwattModel[] {
53
+ return models as unknown as NeuralwattModel[];
54
+ }
55
+
52
56
  function isFreshStoreEntry(
53
57
  entry: Readonly<ModelsStoreEntry> | undefined,
54
58
  ): entry is ModelsStoreEntry {
@@ -82,11 +86,11 @@ export function createNeuralwattRefreshModels(
82
86
  const stored = context.stored as ScopedModelsStoreEntry | undefined;
83
87
  if (!context.allowNetwork) {
84
88
  return stored !== undefined && catalogKeyMatches(stored, scope)
85
- ? buildFromStore(stored.models)
89
+ ? buildFromStore(asStoredChatModels(stored.models))
86
90
  : fallback;
87
91
  }
88
92
  if (!context.force && isUsableStoreEntry(stored, scope)) {
89
- return buildFromStore(stored.models);
93
+ return buildFromStore(asStoredChatModels(stored.models));
90
94
  }
91
95
  const apiModels = await fetchApiModels(apiKey, context.signal);
92
96
  context.signal.throwIfAborted();
@@ -94,7 +98,7 @@ export function createNeuralwattRefreshModels(
94
98
  const entry: ScopedModelsStoreEntry = {
95
99
  models: models as unknown as ModelsStoreEntry["models"],
96
100
  checkedAt: Date.now(),
97
- catalogKey: `${scope} ${CATALOG_SCOPE_VERSION}`,
101
+ catalogKey: catalogKey(scope),
98
102
  };
99
103
  await context.publish({ persist: entry }).catch(() => undefined);
100
104
  context.signal.throwIfAborted();
@@ -2,6 +2,7 @@ import type { Provider } from "@earendil-works/pi-ai";
2
2
  import type { NeuralwattApi } from "../../src/config";
3
3
  import { createAnthropicMessagesApi } from "./api/anthropic-messages";
4
4
  import { createOpenAiCompletionsApi } from "./api/openai-completions";
5
+ import { classify, stampClassifierModels } from "./api/system-one";
5
6
  import type { NeuralwattApiHandler } from "./api/types";
6
7
  import {
7
8
  NEURALWATT_API_KEY_ENV,
@@ -13,6 +14,7 @@ import type { NeuralwattModel } from "./models/catalog";
13
14
  import {
14
15
  buildNeuralwattProviderModelsFromApi,
15
16
  buildNeuralwattProviderModelsFromStore,
17
+ partitionNeuralwattModels,
16
18
  } from "./models/catalog";
17
19
  import {
18
20
  createNeuralwattRefreshModels,
@@ -98,11 +100,22 @@ export function createNeuralwattProvider(
98
100
  if (envKey) {
99
101
  return { auth: { apiKey: envKey }, source: NEURALWATT_API_KEY_ENV };
100
102
  }
101
- return { auth: { apiKey: "" }, source: "anonymous" };
103
+ // pi-ai rejects empty keys before dispatch; "-" is the neutral
104
+ // placeholder that gateways strip or replace server-side.
105
+ return { auth: { apiKey: "-" }, source: "anonymous" };
102
106
  },
103
107
  },
104
108
  },
105
- getModels: () => handler.stampModels(canonicalModels),
109
+ getModels: () =>
110
+ handler.stampModels(partitionNeuralwattModels(canonicalModels).chat),
111
+ getAllModels: () => {
112
+ const { chat, classifiers } = partitionNeuralwattModels(canonicalModels);
113
+ return [
114
+ ...handler.stampModels(chat),
115
+ ...stampClassifierModels(classifiers),
116
+ ];
117
+ },
118
+ classify: (model, context, options) => classify(model, context, options),
106
119
  refreshModels: async (context) => {
107
120
  const models = await refreshCatalog(context);
108
121
  await context.publish({
@@ -1,32 +1,39 @@
1
1
  // Neuralwatt is OpenAI-compatible, but the OpenAI SDK throws on non-2xx
2
2
  // responses before Pi's after_provider_response hook can see the raw headers.
3
- // We wrap the built-in openai-completions streamSimple so 429 rate-limit
4
- // headers can be captured before the SDK turns them into a generic error, while
5
- // still delegating normal streaming behavior to Pi's provider implementation.
3
+ // We inject a per-request `options.fetch` wrapper so 429
4
+ // rate-limit headers can be captured before the SDK turns them into a generic
5
+ // error, and tee successful SSE bodies for live quota comments, while still
6
+ // delegating normal streaming behavior to Pi's provider implementation.
7
+ // `globalThis.fetch` is never touched.
6
8
  //
7
9
  // The SSE tee used for live quota comments is inspired by:
8
10
  // https://github.com/monotykamary/pi-neuralwatt-provider
9
11
 
10
- import {
11
- type AssistantMessageEventStream,
12
- type Context,
13
- createAssistantMessageEventStream,
14
- type Model,
15
- type SimpleStreamOptions,
12
+ import type {
13
+ AssistantMessageEventStream,
14
+ FetchFunction,
15
+ Model,
16
+ SimpleStreamOptions,
17
+ TranscriptContext,
16
18
  } from "@earendil-works/pi-ai";
19
+ import { configuredApiBaseUrl } from "../../src/config/loader";
17
20
  import {
18
21
  type NeuralwattRateLimitInfo,
19
- normalizeNeuralwattRateLimitError,
20
22
  parseRateLimitHeaders,
21
23
  } from "./rate-limit-error";
22
24
  import { readQuotaCommentsFromTee } from "./sse-quotas";
23
25
 
24
26
  export type AnyStreamSimple = (
25
27
  model: Model<string>,
26
- context: Context,
28
+ context: TranscriptContext,
27
29
  options?: SimpleStreamOptions,
28
30
  ) => AssistantMessageEventStream;
29
31
 
32
+ export interface NeuralwattStreamCallbacks {
33
+ onSseQuota: (line: string) => void;
34
+ onRateLimit: (info: NeuralwattRateLimitInfo | undefined) => void;
35
+ }
36
+
30
37
  function headersToRecord(headers: Headers): Record<string, string> {
31
38
  const record: Record<string, string> = {};
32
39
  headers.forEach((value, key) => {
@@ -58,56 +65,34 @@ function isProviderStreamUrl(
58
65
  }
59
66
  }
60
67
 
61
- async function forwardStream(
62
- stream: AssistantMessageEventStream,
63
- outer: AssistantMessageEventStream,
64
- getRateLimitInfo: () => NeuralwattRateLimitInfo | undefined,
65
- restoreFetch: () => void,
66
- ): Promise<void> {
67
- try {
68
- for await (const event of stream) {
69
- const rateLimitInfo = getRateLimitInfo();
70
- if (event.type === "error" && rateLimitInfo) {
71
- outer.push({
72
- ...event,
73
- error: normalizeNeuralwattRateLimitError(event.error, rateLimitInfo),
74
- });
75
- } else {
76
- outer.push(event);
77
- }
78
- }
79
- } finally {
80
- restoreFetch();
81
- outer.end();
82
- }
83
- }
84
-
85
68
  export function wrapNeuralwattStreamSimple(
86
69
  base: AnyStreamSimple,
87
- onSseQuota: (line: string) => void,
70
+ callbacks: NeuralwattStreamCallbacks,
88
71
  ): AnyStreamSimple {
89
72
  return (model, context, options = {}) => {
90
- let rateLimitInfo: NeuralwattRateLimitInfo | undefined;
91
- let sseQuotaTask: Promise<void> | undefined;
92
- const outer = createAssistantMessageEventStream();
93
- const providerOrigin = new URL(
94
- model.baseUrl ?? "https://api.neuralwatt.com/v1",
95
- ).origin;
96
- const originalFetch = globalThis.fetch;
97
- const wrappedFetch: typeof fetch = async (input, init) => {
98
- const response = await originalFetch(input, init);
73
+ const providerOrigin = new URL(model.baseUrl ?? configuredApiBaseUrl())
74
+ .origin;
75
+ const callerFetch = options.fetch;
76
+
77
+ const neuralwattFetch: FetchFunction = async (input, init) => {
78
+ // Chain onto a caller-provided fetch when present; otherwise resolve
79
+ // the global at request time so late host/test overrides still apply.
80
+ const response = await (callerFetch ?? globalThis.fetch)(input, init);
99
81
 
100
82
  if (!isProviderStreamUrl(input, providerOrigin)) return response;
101
83
 
102
- const headers = headersToRecord(response.headers);
103
84
  if (response.status === 429) {
104
- rateLimitInfo = parseRateLimitHeaders(headers);
85
+ callbacks.onRateLimit(
86
+ parseRateLimitHeaders(headersToRecord(response.headers)),
87
+ );
105
88
  return response;
106
89
  }
107
90
 
108
91
  if (response.ok && response.body) {
109
92
  const [sdkBody, quotaBody] = response.body.tee();
110
- sseQuotaTask = readQuotaCommentsFromTee(quotaBody, onSseQuota);
93
+ // Best-effort side channel; failures inside are swallowed by the
94
+ // reader so quota comments can never break the SDK stream.
95
+ void readQuotaCommentsFromTee(quotaBody, callbacks.onSseQuota);
111
96
  return new Response(sdkBody, {
112
97
  headers: response.headers,
113
98
  status: response.status,
@@ -118,22 +103,6 @@ export function wrapNeuralwattStreamSimple(
118
103
  return response;
119
104
  };
120
105
 
121
- globalThis.fetch = wrappedFetch;
122
-
123
- const restoreFetch = () => {
124
- if (globalThis.fetch === wrappedFetch) globalThis.fetch = originalFetch;
125
- sseQuotaTask?.catch(() => {});
126
- };
127
-
128
- const stream = base(model, context, options);
129
- const originalOuterEnd = outer.end.bind(outer);
130
- outer.end = (result?: Parameters<typeof originalOuterEnd>[0]) => {
131
- restoreFetch();
132
- originalOuterEnd(result);
133
- };
134
-
135
- void forwardStream(stream, outer, () => rateLimitInfo, restoreFetch);
136
-
137
- return outer;
106
+ return base(model, context, { ...options, fetch: neuralwattFetch });
138
107
  };
139
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-neuralwatt",
3
- "version": "0.16.3",
3
+ "version": "0.18.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -17,8 +17,7 @@
17
17
  "extensions": [
18
18
  "./extensions/provider/index.ts",
19
19
  "./extensions/command-quotas/index.ts",
20
- "./extensions/quota-warnings/index.ts",
21
- "./extensions/sub-bar-integration/index.ts"
20
+ "./extensions/quota-warnings/index.ts"
22
21
  ],
23
22
  "video": "https://assets.aliou.me/github/aliou/pi-neuralwatt/demo.mp4"
24
23
  },
@@ -38,21 +37,22 @@
38
37
  "@aliou/pi-utils-ui": "^0.5.0"
39
38
  },
40
39
  "peerDependencies": {
41
- "@earendil-works/pi-ai": ">=0.84.0",
42
- "@earendil-works/pi-coding-agent": ">=0.84.0",
40
+ "@earendil-works/pi-ai": ">=1.0.0",
41
+ "@earendil-works/pi-coding-agent": ">=1.0.0",
43
42
  "@earendil-works/pi-tui": "*"
44
43
  },
45
44
  "devDependencies": {
46
45
  "@aliou/biome-plugins": "^0.11.0",
47
46
  "@biomejs/biome": "^2.4.15",
48
47
  "@changesets/cli": "^2.27.11",
49
- "@earendil-works/pi-ai": "0.84.0",
50
- "@earendil-works/pi-coding-agent": "0.84.0",
51
- "@earendil-works/pi-tui": "0.84.0",
48
+ "@earendil-works/pi-ai": "1.1.0",
49
+ "@earendil-works/pi-coding-agent": "1.1.0",
50
+ "@earendil-works/pi-tui": "1.1.0",
52
51
  "@types/node": "^25.0.10",
53
52
  "husky": "^9.1.7",
54
53
  "ts-json-schema-generator": "^2.4.0",
55
54
  "tsx": "4.23.12",
55
+ "typebox": "1.3.27",
56
56
  "typescript": "^5.9.3",
57
57
  "vitest": "^4.0.18"
58
58
  },
@@ -70,8 +70,8 @@
70
70
  "test:watch": "vitest",
71
71
  "check:models": "tsx scripts/check-models.ts",
72
72
  "check:changesets": "tsx scripts/check-changesets.ts",
73
- "gen:schema": "pi-settings-schema -p src/config/types.ts -t NeuralwattConfig -o schema.json --version 0.12.0",
74
- "check:schema": "pi-settings-schema -p src/config/types.ts -t NeuralwattConfig -o schema.json --version 0.12.0 --check",
73
+ "gen:schema": "pi-settings-schema -p src/config/types.ts -t NeuralwattConfig -o schema.json",
74
+ "check:schema": "pi-settings-schema -p src/config/types.ts -t NeuralwattConfig -o schema.json --check",
75
75
  "check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts",
76
76
  "prepare": "[ -d .git ] && husky || true",
77
77
  "changeset": "changeset",