@aliou/pi-neuralwatt 0.13.0 → 0.14.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.
@@ -18,7 +18,8 @@ import type { NeuralwattQuotas } from "../../src/types/quota-api";
18
18
  import { getNeuralwattApiKey } from "../_shared/auth";
19
19
  import { registerNeuralwattSettings } from "./commands/settings";
20
20
  import { normalizeNeuralwattContextOverflowError } from "./context-overflow";
21
- import { getNeuralwattModels, refreshNeuralwattModels } from "./models";
21
+ import { getNeuralwattModels } from "./models";
22
+ import { createNeuralwattProvider } from "./provider";
22
23
  import { buildQuotasFromHeaders, fetchRequestedQuotas } from "./quota-store";
23
24
  import {
24
25
  type NeuralwattRateLimitInfo,
@@ -42,24 +43,24 @@ function registerNeuralwattProvider(
42
43
  ): void {
43
44
  const { provider: providerConfig } = configLoader.getConfig();
44
45
 
45
- const models = getNeuralwattModels({
46
+ const staticModels = getNeuralwattModels({
46
47
  includeLegacyModelIds: providerConfig.includeLegacyModelIds,
47
48
  includeAliasedModelIds: providerConfig.includeAliasedModelIds,
48
49
  });
49
50
 
50
- const config: Parameters<ExtensionAPI["registerProvider"]>[1] = {
51
- name: "Neuralwatt",
52
- baseUrl: "https://api.neuralwatt.com/v1",
53
- apiKey: "$NEURALWATT_API_KEY",
54
- api: "openai-completions",
55
- authHeader: true,
56
- headers: {
57
- Referer: "https://pi.dev",
58
- "X-Title": "npm:@aliou/pi-neuralwatt",
59
- },
60
- models,
61
- refreshModels: (context) =>
62
- refreshNeuralwattModels(context, {
51
+ const apiProvider = getApiProvider("openai-completions");
52
+ const baseStreamSimple = apiProvider?.streamSimple;
53
+ const streamSimple = baseStreamSimple
54
+ ? (wrapNeuralwattStreamSimple(
55
+ baseStreamSimple as never,
56
+ onSseQuota,
57
+ ) as never)
58
+ : undefined;
59
+
60
+ pi.registerProvider(
61
+ createNeuralwattProvider(
62
+ staticModels,
63
+ () => ({
63
64
  includeLegacyModelIds:
64
65
  configLoader.getConfig().provider.includeLegacyModelIds,
65
66
  includeAliasedModelIds:
@@ -67,18 +68,9 @@ function registerNeuralwattProvider(
67
68
  includeEarlyAccessModels:
68
69
  configLoader.getConfig().provider.includeEarlyAccessModels,
69
70
  }),
70
- };
71
-
72
- const provider = getApiProvider("openai-completions");
73
- const baseStreamSimple = provider?.streamSimple;
74
- if (baseStreamSimple) {
75
- config.streamSimple = wrapNeuralwattStreamSimple(
76
- baseStreamSimple as never,
77
- onSseQuota,
78
- ) as never;
79
- }
80
-
81
- pi.registerProvider("neuralwatt", config);
71
+ streamSimple,
72
+ ),
73
+ );
82
74
  }
83
75
 
84
76
  export default async function (pi: ExtensionAPI) {
@@ -180,28 +180,37 @@ const FAMILIES: [NeuralwattModelFamily, NeuralwattVariantSpec[]][] = [
180
180
  },
181
181
  ],
182
182
  ],
183
+ // The kimi-k3 endpoint rejects anything above 327,680 total tokens with
184
+ // `400: max_completion_tokens is too large … supports at most 327680
185
+ // completion tokens` (verified at runtime), even though the API advertises
186
+ // `max_model_len: 1048560` with a null output cap for the whole family.
187
+ // The -fast/-flex endpoints don't enforce any cap server-side yet (they
188
+ // accept max_completion_tokens beyond the advertised window), but they are
189
+ // the same K3 deployment and are expected to share the 327,680 limit, so
190
+ // all three variants are pinned to it. The drift check in models.test.ts
191
+ // whitelists this divergence via CONTEXT_WINDOW_OVERRIDES.
183
192
  [
184
193
  KIMI_K3,
185
194
  [
186
195
  {
187
196
  id: "kimi-k3",
188
197
  name: "Kimi K3",
189
- contextWindow: 1048560,
190
- maxOutputTokens: null,
198
+ contextWindow: 327680,
199
+ maxOutputTokens: 327680,
191
200
  reasoning: true,
192
201
  },
193
202
  {
194
203
  id: "kimi-k3-fast",
195
204
  name: "Kimi K3 Fast",
196
- contextWindow: 1048560,
197
- maxOutputTokens: null,
205
+ contextWindow: 327680,
206
+ maxOutputTokens: 327680,
198
207
  reasoning: false,
199
208
  },
200
209
  {
201
210
  id: "kimi-k3-flex",
202
211
  name: "Kimi K3 (flex)",
203
- contextWindow: 1048560,
204
- maxOutputTokens: null,
212
+ contextWindow: 327680,
213
+ maxOutputTokens: 327680,
205
214
  reasoning: true,
206
215
  costMultiplier: FLEX_COST_MULTIPLIER,
207
216
  },
@@ -5,10 +5,6 @@ import type {
5
5
  RefreshModelsContext,
6
6
  } from "@earendil-works/pi-ai";
7
7
  import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
8
- import {
9
- persistModels,
10
- readStoredModels,
11
- } from "../../../src/refresh-store-compat";
12
8
  import {
13
9
  ALIAS_NEURALWATT_MODEL_IDS,
14
10
  buildAliasNeuralwattModels,
@@ -24,6 +20,8 @@ const PROVIDER_ID = "neuralwatt";
24
20
  const BASE_URL = "https://api.neuralwatt.com/v1";
25
21
  const API = "openai-completions" as const;
26
22
 
23
+ export type StoredProviderModels = readonly Model<Api>[];
24
+
27
25
  export interface RefreshNeuralwattModelsOptions {
28
26
  includeLegacyModelIds: boolean;
29
27
  includeAliasedModelIds: boolean;
@@ -129,25 +127,27 @@ function persistCatalog(
129
127
  context: RefreshModelsContext,
130
128
  models: ProviderModelConfig[],
131
129
  ): Promise<boolean> {
132
- return persistModels(context, {
133
- models: models.map(toStoredModel),
134
- checkedAt: Date.now(),
130
+ return context.publish({
131
+ persist: {
132
+ models: models.map(toStoredModel),
133
+ checkedAt: Date.now(),
134
+ },
135
135
  });
136
136
  }
137
137
 
138
- /** Refresh the complete Neuralwatt catalog with Pi-managed persistence. */
138
+ /** Refresh the complete Neuralwatt catalog; undefined = failed (stale store kept). */
139
139
  export async function refreshNeuralwattModels(
140
140
  context: RefreshModelsContext,
141
141
  options: RefreshNeuralwattModelsOptions,
142
- ): Promise<ProviderModelConfig[]> {
142
+ ): Promise<ProviderModelConfig[] | undefined> {
143
143
  const baseline = configuredModels(
144
144
  options.includeLegacyModelIds,
145
145
  options.includeAliasedModelIds,
146
146
  );
147
- const stored = await readStoredModels(context);
147
+ const stored = context.stored;
148
148
 
149
149
  if (!options.includeEarlyAccessModels) {
150
- await persistCatalog(context, baseline);
150
+ await persistCatalog(context, baseline).catch(() => false);
151
151
  return baseline;
152
152
  }
153
153
 
@@ -161,29 +161,31 @@ export async function refreshNeuralwattModels(
161
161
  cachedEarlyAccess,
162
162
  );
163
163
 
164
- if (!context.allowNetwork || context.signal?.aborted) {
164
+ if (!context.allowNetwork || context.signal.aborted) {
165
165
  return cachedCatalog;
166
166
  }
167
167
 
168
+ // Anonymous credential (empty or missing key): keep the public catalog and
169
+ // skip discovery, which requires a real key.
168
170
  const apiKey =
169
- context.credential?.type === "api_key" ? context.credential.key : undefined;
171
+ context.credential?.type === "api_key" && context.credential.key
172
+ ? context.credential.key
173
+ : undefined;
170
174
  if (!apiKey) return cachedCatalog;
171
175
 
172
176
  const earlyAccess = await (options.loadEarlyAccess ?? loadEarlyAccessModels)(
173
177
  apiKey,
174
178
  context.signal,
175
179
  );
176
- if (context.signal?.aborted) return cachedCatalog;
177
- if (!earlyAccess) {
178
- throw new Error("Neuralwatt model catalog refresh failed");
179
- }
180
+ if (context.signal.aborted) return cachedCatalog;
181
+ if (!earlyAccess) return undefined;
180
182
 
181
183
  const catalog = configuredModels(
182
184
  options.includeLegacyModelIds,
183
185
  options.includeAliasedModelIds,
184
186
  configuredEarlyAccessModels(earlyAccess, baseline),
185
187
  );
186
- context.signal?.throwIfAborted();
187
- await persistCatalog(context, catalog);
188
+ context.signal.throwIfAborted();
189
+ await persistCatalog(context, catalog).catch(() => false);
188
190
  return catalog;
189
191
  }
@@ -0,0 +1,113 @@
1
+ import type {
2
+ Api,
3
+ Model,
4
+ Provider,
5
+ ProviderStreamOptions,
6
+ } from "@earendil-works/pi-ai";
7
+ import { stream, streamSimple } from "@earendil-works/pi-ai/compat";
8
+ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
9
+ import type {
10
+ RefreshNeuralwattModelsOptions,
11
+ StoredProviderModels,
12
+ } from "./models/refresh";
13
+ import { refreshNeuralwattModels } from "./models/refresh";
14
+ import type { AnyStreamSimple } from "./stream-simple";
15
+
16
+ export const NEURALWATT_PROVIDER_ID = "neuralwatt";
17
+ export const NEURALWATT_BASE_URL = "https://api.neuralwatt.com/v1";
18
+ export const NEURALWATT_API_KEY_ENV = "NEURALWATT_API_KEY";
19
+
20
+ const NEURALWATT_REQUEST_HEADERS = {
21
+ Referer: "https://pi.dev",
22
+ "X-Title": "npm:@aliou/pi-neuralwatt",
23
+ };
24
+
25
+ const API = "openai-completions" as const;
26
+
27
+ function toProviderModels(
28
+ models: readonly ProviderModelConfig[],
29
+ ): Model<Api>[] {
30
+ return models.map((model) => ({
31
+ ...model,
32
+ api: model.api ?? API,
33
+ provider: NEURALWATT_PROVIDER_ID,
34
+ baseUrl: model.baseUrl ?? NEURALWATT_BASE_URL,
35
+ headers: NEURALWATT_REQUEST_HEADERS,
36
+ }));
37
+ }
38
+
39
+ export function createNeuralwattProvider(
40
+ staticModels: ProviderModelConfig[],
41
+ refreshOptions: () => RefreshNeuralwattModelsOptions,
42
+ streamSimpleOverride?: AnyStreamSimple,
43
+ ): Provider {
44
+ let liveModels = toProviderModels(staticModels);
45
+
46
+ return {
47
+ id: NEURALWATT_PROVIDER_ID,
48
+ name: "Neuralwatt",
49
+ baseUrl: NEURALWATT_BASE_URL,
50
+ headers: NEURALWATT_REQUEST_HEADERS,
51
+ auth: {
52
+ apiKey: {
53
+ name: "Neuralwatt API key",
54
+ login: async (interaction) => ({
55
+ type: "api_key",
56
+ key: await interaction.prompt({
57
+ type: "secret",
58
+ message: "Enter Neuralwatt API key",
59
+ }),
60
+ }),
61
+ check: async ({ ctx, credential }) => {
62
+ if (credential?.type === "api_key" && credential.key) {
63
+ return { type: "api_key", source: "stored credential" };
64
+ }
65
+ if (await ctx.env(NEURALWATT_API_KEY_ENV)) {
66
+ return { type: "api_key", source: NEURALWATT_API_KEY_ENV };
67
+ }
68
+ return undefined;
69
+ },
70
+ resolve: async ({ ctx, credential, signal }) => {
71
+ signal.throwIfAborted();
72
+ if (credential?.type === "api_key" && credential.key) {
73
+ return {
74
+ auth: { apiKey: credential.key },
75
+ env: credential.env,
76
+ source: "stored credential",
77
+ };
78
+ }
79
+ const envKey = await ctx.env(NEURALWATT_API_KEY_ENV);
80
+ signal.throwIfAborted();
81
+ if (envKey) {
82
+ return { auth: { apiKey: envKey }, source: NEURALWATT_API_KEY_ENV };
83
+ }
84
+ // Resolve never fails: without a key the catalog is the hardcoded
85
+ // one and early-access discovery is skipped (anonymous playground
86
+ // traffic authenticates at stream time).
87
+ return { auth: { apiKey: "" }, source: "anonymous" };
88
+ },
89
+ },
90
+ },
91
+ getModels: () => liveModels,
92
+ refreshModels: async (context) => {
93
+ const refreshed = await refreshNeuralwattModels(
94
+ context,
95
+ refreshOptions(),
96
+ );
97
+ // Fresh or offline store: the refresh intentionally skipped the
98
+ // network; adopt the persisted catalog anyway so getModels reflects it
99
+ // (statics otherwise).
100
+ const next = refreshed ?? context.stored?.models;
101
+ if (!next || next.length === 0) return;
102
+ const adopted = toProviderModels(next as StoredProviderModels);
103
+ await context.publish({
104
+ update: () => {
105
+ liveModels = adopted;
106
+ },
107
+ });
108
+ },
109
+ stream: (model, context, options) =>
110
+ stream(model, context, options as ProviderStreamOptions | undefined),
111
+ streamSimple: streamSimpleOverride ?? streamSimple,
112
+ };
113
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-neuralwatt",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -38,8 +38,8 @@
38
38
  "@aliou/pi-utils-ui": "^0.5.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "@earendil-works/pi-ai": ">=0.80.8",
42
- "@earendil-works/pi-coding-agent": ">=0.80.8",
41
+ "@earendil-works/pi-ai": ">=0.84.0",
42
+ "@earendil-works/pi-coding-agent": ">=0.84.0",
43
43
  "@earendil-works/pi-tui": "*"
44
44
  },
45
45
  "devDependencies": {
@@ -1,69 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Backward-compat shim for the Pi coding-agent provider refresh context.
3
- //
4
- // Pi 0.84 replaced dynamic `context.store` read/write access with the
5
- // read-only `context.stored` snapshot and the generation-checked
6
- // `context.publish({ persist })` transaction. This module detects the
7
- // available API shape at runtime so the extension works on both <0.84 (store)
8
- // and >=0.84 (publish) hosts.
9
- //
10
- // Once the minimum supported @earendil-works/pi-coding-agent version is
11
- // >=0.84, delete this file and:
12
- // - replace `readStoredModels(context)` with `context.stored`
13
- // - replace `persistModels(context, entry)` with
14
- // `await context.publish({ persist: entry })` (skip when aborted)
15
- // ---------------------------------------------------------------------------
16
-
17
- import type {
18
- ModelsStoreEntry,
19
- RefreshModelsContext,
20
- } from "@earendil-works/pi-ai";
21
-
22
- type LegacyRefreshModelsContext = RefreshModelsContext & {
23
- store?: {
24
- read(): Promise<ModelsStoreEntry | undefined>;
25
- write(entry: ModelsStoreEntry): Promise<unknown>;
26
- };
27
- };
28
-
29
- /**
30
- * Returns the persisted catalog entry for the current provider, reading from
31
- * the 0.84+ `context.stored` snapshot when available and falling back to the
32
- * legacy `context.store.read()` on older hosts.
33
- */
34
- export async function readStoredModels(
35
- context: RefreshModelsContext,
36
- ): Promise<ModelsStoreEntry | undefined> {
37
- if (context.stored !== undefined) return context.stored;
38
- return readLegacyStore(context);
39
- }
40
-
41
- function readLegacyStore(
42
- context: RefreshModelsContext,
43
- ): Promise<ModelsStoreEntry | undefined> {
44
- const legacy = context as LegacyRefreshModelsContext;
45
- return legacy.store ? legacy.store.read() : Promise.resolve(undefined);
46
- }
47
-
48
- /**
49
- * Persists the catalog entry, publishing through
50
- * `context.publish({ persist: entry })` on 0.84+ hosts and writing through
51
- * the legacy `context.store.write(entry)` on older hosts.
52
- *
53
- * Returns true when the entry was persisted. On 0.84+ hosts a return value of
54
- * false means a newer refresh superseded this publication (generation check).
55
- */
56
- export async function persistModels(
57
- context: RefreshModelsContext,
58
- entry: ModelsStoreEntry,
59
- ): Promise<boolean> {
60
- if (typeof context.publish === "function") {
61
- return context.publish({ persist: entry });
62
- }
63
- const legacy = context as LegacyRefreshModelsContext;
64
- if (legacy.store) {
65
- await legacy.store.write(entry);
66
- return true;
67
- }
68
- return false;
69
- }