@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.
package/schema.json CHANGED
@@ -16,10 +16,6 @@
16
16
  "$ref": "#/definitions/NeuralwattQuotaWarningsConfig",
17
17
  "description": "Quota warning feature."
18
18
  },
19
- "subBarIntegration": {
20
- "$ref": "#/definitions/NeuralwattSubBarIntegrationConfig",
21
- "description": "Sub-bar/status-bar integration feature."
22
- },
23
19
  "provider": {
24
20
  "$ref": "#/definitions/NeuralwattProviderConfig",
25
21
  "description": "Provider behavior (API surface)."
@@ -35,7 +31,7 @@
35
31
  "pattern": "^\\d{1,15}(\\.\\d{1,15})?(\\.\\d{1,15})?$"
36
32
  }
37
33
  ],
38
- "description": "Config schema version, stamped by migrations. Current version: 0.12.0."
34
+ "description": "Config schema version, stamped by migrations."
39
35
  }
40
36
  },
41
37
  "additionalProperties": false
@@ -60,22 +56,16 @@
60
56
  },
61
57
  "additionalProperties": false
62
58
  },
63
- "NeuralwattSubBarIntegrationConfig": {
64
- "type": "object",
65
- "properties": {
66
- "enabled": {
67
- "type": "boolean",
68
- "description": "Show usage in the sub-bar / status bar."
69
- }
70
- },
71
- "additionalProperties": false
72
- },
73
59
  "NeuralwattProviderConfig": {
74
60
  "type": "object",
75
61
  "properties": {
76
62
  "api": {
77
63
  "$ref": "#/definitions/NeuralwattApi",
78
64
  "description": "Which API serves model requests."
65
+ },
66
+ "apiBaseUrl": {
67
+ "type": "string",
68
+ "description": "Fallback base URL for Neuralwatt API calls, used only by models that carry no baseUrl of their own (models.json, custom providers, and proxy routing all take precedence) and by the extension's own catalog and quota fetches. Defaults to the direct upstream."
79
69
  }
80
70
  },
81
71
  "additionalProperties": false
@@ -1,5 +1,7 @@
1
1
  import type { ResolvedNeuralwattConfig } from "./types";
2
2
 
3
+ export const NEURALWATT_API_BASE_URL = "https://api.neuralwatt.com/v1";
4
+
3
5
  export const DEFAULT_CONFIG: ResolvedNeuralwattConfig = {
4
6
  quotaCommand: {
5
7
  enabled: true,
@@ -7,10 +9,8 @@ export const DEFAULT_CONFIG: ResolvedNeuralwattConfig = {
7
9
  quotaWarnings: {
8
10
  enabled: true,
9
11
  },
10
- subBarIntegration: {
11
- enabled: true,
12
- },
13
12
  provider: {
14
13
  api: "openai-completions",
14
+ apiBaseUrl: NEURALWATT_API_BASE_URL,
15
15
  },
16
16
  };
@@ -26,13 +26,9 @@ function normalizeResolvedConfig(
26
26
  enabled:
27
27
  config.quotaWarnings?.enabled ?? DEFAULT_CONFIG.quotaWarnings.enabled,
28
28
  },
29
- subBarIntegration: {
30
- enabled:
31
- config.subBarIntegration?.enabled ??
32
- DEFAULT_CONFIG.subBarIntegration.enabled,
33
- },
34
29
  provider: {
35
30
  api: resolveApi(config.provider?.api),
31
+ apiBaseUrl: resolveApiBaseUrl(config.provider?.apiBaseUrl),
36
32
  },
37
33
  };
38
34
  }
@@ -44,6 +40,30 @@ export function resolveApi(value: string | undefined): NeuralwattApi {
44
40
  return DEFAULT_CONFIG.provider.api;
45
41
  }
46
42
 
43
+ export function resolveApiBaseUrl(value: string | undefined): string {
44
+ const trimmed = value?.trim().replace(/\/+$/u, "");
45
+ if (!trimmed) {
46
+ return DEFAULT_CONFIG.provider.apiBaseUrl;
47
+ }
48
+ try {
49
+ const url = new URL(trimmed);
50
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
51
+ return DEFAULT_CONFIG.provider.apiBaseUrl;
52
+ }
53
+ } catch {
54
+ return DEFAULT_CONFIG.provider.apiBaseUrl;
55
+ }
56
+ return trimmed;
57
+ }
58
+
59
+ export function configuredApiBaseUrl(): string {
60
+ try {
61
+ return configLoader.getConfig().provider.apiBaseUrl;
62
+ } catch {
63
+ return DEFAULT_CONFIG.provider.apiBaseUrl;
64
+ }
65
+ }
66
+
47
67
  export const configLoader = new ConfigLoader<
48
68
  NeuralwattConfig,
49
69
  ResolvedNeuralwattConfig
@@ -0,0 +1,31 @@
1
+ import type { Migration } from "@aliou/pi-utils-settings";
2
+
3
+ export const SUB_BAR_GIST_URL =
4
+ "https://gist.github.com/aliou/9939544f3c5a4f917b0a8e5c0497cdb2";
5
+
6
+ interface PreRemovalNeuralwattConfig {
7
+ $schema?: string;
8
+ quotaCommand?: { enabled?: boolean };
9
+ quotaWarnings?: { enabled?: boolean };
10
+ subBarIntegration?: { enabled?: boolean };
11
+ provider?: {
12
+ api?: string;
13
+ apiBaseUrl?: string;
14
+ includeLegacyModelIds?: boolean;
15
+ includeEarlyAccessModels?: boolean;
16
+ };
17
+ }
18
+
19
+ export const removeSubBarIntegrationMigration: Migration<PreRemovalNeuralwattConfig> =
20
+ {
21
+ name: "remove-sub-bar-integration",
22
+ version: "0.18.0",
23
+ shouldRun: (config) => config.subBarIntegration !== undefined,
24
+ message: `[neuralwatt] The built-in sub-bar integration was removed. Test with \`pi -e ${SUB_BAR_GIST_URL}\` and install with \`pi install ${SUB_BAR_GIST_URL}\`. The standalone extension won't be kept up to date.`,
25
+ run: (config) => {
26
+ if (config.subBarIntegration === undefined) return config;
27
+
28
+ const { subBarIntegration: _removed, ...rest } = config;
29
+ return rest;
30
+ },
31
+ };
@@ -1,19 +1,18 @@
1
1
  import type { Migration } from "@aliou/pi-utils-settings";
2
- import type { NeuralwattConfig } from "../types";
3
2
 
4
3
  export {
5
4
  backupConfig,
6
5
  flatToNestedConfigMigration,
7
6
  } from "./02-flat-to-nested-config";
8
7
  export { renameHiddenToEarlyAccessMigration } from "./03-rename-hidden-to-early-access";
8
+ export { removeSubBarIntegrationMigration } from "./04-remove-sub-bar-integration";
9
9
 
10
10
  import { flatToNestedConfigMigration } from "./02-flat-to-nested-config";
11
11
  import { renameHiddenToEarlyAccessMigration } from "./03-rename-hidden-to-early-access";
12
+ import { removeSubBarIntegrationMigration } from "./04-remove-sub-bar-integration";
12
13
 
13
- // Each migration is typed against its own historical input shape. The loader
14
- // applies them in sequence on the raw config record, so they are cast to the
15
- // current config type for the array.
16
14
  export const migrations = [
17
15
  flatToNestedConfigMigration,
18
16
  renameHiddenToEarlyAccessMigration,
19
- ] as unknown as Migration<NeuralwattConfig>[];
17
+ removeSubBarIntegrationMigration,
18
+ ] as Migration<object>[];
@@ -8,11 +8,6 @@ export interface NeuralwattQuotaWarningsConfig {
8
8
  enabled?: boolean;
9
9
  }
10
10
 
11
- export interface NeuralwattSubBarIntegrationConfig {
12
- /** Show usage in the sub-bar / status bar. */
13
- enabled?: boolean;
14
- }
15
-
16
11
  /**
17
12
  * Neuralwatt serves every chat model twice: on an OpenAI-compatible
18
13
  * `chat/completions` endpoint and on a vLLM-backed Anthropic-compatible
@@ -23,6 +18,14 @@ export type NeuralwattApi = "openai-completions" | "anthropic-messages";
23
18
  export interface NeuralwattProviderConfig {
24
19
  /** Which API serves model requests. */
25
20
  api?: NeuralwattApi;
21
+
22
+ /**
23
+ * Fallback base URL for Neuralwatt API calls, used only by models that
24
+ * carry no baseUrl of their own (models.json, custom providers, and proxy
25
+ * routing all take precedence) and by the extension's own catalog and
26
+ * quota fetches. Defaults to the direct upstream.
27
+ */
28
+ apiBaseUrl?: string;
26
29
  }
27
30
 
28
31
  export interface NeuralwattConfig {
@@ -35,9 +38,6 @@ export interface NeuralwattConfig {
35
38
  /** Quota warning feature. */
36
39
  quotaWarnings?: NeuralwattQuotaWarningsConfig;
37
40
 
38
- /** Sub-bar/status-bar integration feature. */
39
- subBarIntegration?: NeuralwattSubBarIntegrationConfig;
40
-
41
41
  /** Provider behavior (API surface). */
42
42
  provider?: NeuralwattProviderConfig;
43
43
  }
@@ -49,10 +49,8 @@ export interface ResolvedNeuralwattConfig {
49
49
  quotaWarnings: {
50
50
  enabled: boolean;
51
51
  };
52
- subBarIntegration: {
53
- enabled: boolean;
54
- };
55
52
  provider: {
56
53
  api: NeuralwattApi;
54
+ apiBaseUrl: string;
57
55
  };
58
56
  }
package/src/events.ts CHANGED
@@ -1,10 +1,7 @@
1
1
  import type { ResolvedNeuralwattConfig } from "./config";
2
2
  import type { NeuralwattQuotas } from "./types/quota-api";
3
3
 
4
- export type NeuralwattFeatureId =
5
- | "quotaCommand"
6
- | "quotaWarnings"
7
- | "subBarIntegration";
4
+ export type NeuralwattFeatureId = "quotaCommand" | "quotaWarnings";
8
5
 
9
6
  export const NEURALWATT_EXTENSIONS_REQUEST_EVENT =
10
7
  "neuralwatt:extensions:request" as const;
@@ -1,3 +1,4 @@
1
+ import { configuredApiBaseUrl } from "../config/loader";
1
2
  import type {
2
3
  NeuralwattApiModel,
3
4
  NeuralwattApiModelsResponse,
@@ -5,7 +6,6 @@ import type {
5
6
  import type { NeuralwattQuotas } from "../types/quota-api";
6
7
  import type { QuotasResult } from "../types/quota-result";
7
8
 
8
- const BASE_URL = "https://api.neuralwatt.com/v1";
9
9
  const FETCH_TIMEOUT_MS = 15_000;
10
10
 
11
11
  function authHeaders(apiKey: string | undefined): Record<string, string> {
@@ -31,7 +31,7 @@ async function neuralwattFetch(
31
31
  signal?: AbortSignal,
32
32
  headers?: Record<string, string>,
33
33
  ): Promise<Response> {
34
- return fetch(`${BASE_URL}${path}`, {
34
+ return fetch(`${configuredApiBaseUrl()}${path}`, {
35
35
  headers: { ...authHeaders(apiKey), ...headers },
36
36
  signal: combineSignals(signal),
37
37
  });
@@ -1,131 +0,0 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionContext,
4
- Theme,
5
- } from "@earendil-works/pi-coding-agent";
6
- import { configLoader } from "../../src/config";
7
- import {
8
- NEURALWATT_CONFIG_UPDATED_EVENT,
9
- NEURALWATT_EXTENSIONS_REGISTER_EVENT,
10
- NEURALWATT_EXTENSIONS_REQUEST_EVENT,
11
- NEURALWATT_QUOTAS_REQUEST_EVENT,
12
- NEURALWATT_QUOTAS_UPDATED_EVENT,
13
- type NeuralwattConfigUpdatedPayload,
14
- type NeuralwattQuotasUpdatedPayload,
15
- } from "../../src/events";
16
- import type { NeuralwattQuotas } from "../../src/types/quota-api";
17
- import {
18
- percentCreditsRemaining,
19
- percentEnergyRemaining,
20
- } from "../../src/utils/quota-bar";
21
- import { formatKwh, formatUsd } from "../../src/utils/quota-format";
22
- import { toUsageSnapshot } from "./snapshot";
23
-
24
- function formatStatus(quotas: NeuralwattQuotas, theme: Theme): string {
25
- const parts: string[] = [];
26
-
27
- const creditsRemaining = percentCreditsRemaining(quotas);
28
- const creditsColor =
29
- creditsRemaining > 50
30
- ? "success"
31
- : creditsRemaining > 20
32
- ? "warning"
33
- : "error";
34
- parts.push(
35
- `${theme.fg("dim", "credits:")} ${theme.fg(creditsColor, formatUsd(quotas.balance.credits_remaining_usd))}`,
36
- );
37
-
38
- if (quotas.subscription) {
39
- const energyRemaining = percentEnergyRemaining(quotas.subscription);
40
- const energyColor =
41
- energyRemaining > 50
42
- ? "success"
43
- : energyRemaining > 20
44
- ? "warning"
45
- : "error";
46
- parts.push(
47
- `${theme.fg("dim", "energy:")} ${theme.fg(energyColor, formatKwh(quotas.subscription.kwh_remaining))}`,
48
- );
49
- }
50
-
51
- return parts.join(" ");
52
- }
53
-
54
- export default async function (pi: ExtensionAPI) {
55
- await configLoader.load();
56
-
57
- let enabled = configLoader.getConfig().subBarIntegration.enabled;
58
- let subCoreReady = false;
59
- let currentProvider: string | undefined;
60
- let unsubscribeQuotas: (() => void) | undefined;
61
-
62
- // Listen for config changes at runtime
63
- pi.events.on(NEURALWATT_CONFIG_UPDATED_EVENT, (data: unknown) => {
64
- enabled = (data as NeuralwattConfigUpdatedPayload).config.subBarIntegration
65
- .enabled;
66
- });
67
-
68
- function isActive(): boolean {
69
- return currentProvider === "neuralwatt";
70
- }
71
-
72
- function emitUsage(quotas: NeuralwattQuotas): void {
73
- pi.events.emit("sub-core:update-current", {
74
- state: {
75
- provider: "neuralwatt",
76
- usage: toUsageSnapshot(quotas),
77
- },
78
- });
79
- }
80
-
81
- function requestQuotas(): void {
82
- pi.events.emit(NEURALWATT_QUOTAS_REQUEST_EVENT, undefined);
83
- }
84
-
85
- // The quota handler runs on the shared event bus, so it must only touch a
86
- // session ctx captured by a live session-scoped subscription: pi
87
- // invalidates session-bound ctx after session replacement (newSession/
88
- // fork/switchSession/reload), and dereferencing a stale ctx throws. We
89
- // subscribe in session_start (capturing the fresh ctx in the closure) and
90
- // unsubscribe in session_shutdown, before the ctx can go stale.
91
- function handleQuotas(ctx: ExtensionContext, data: unknown): void {
92
- if (!isActive() || !subCoreReady || !enabled) return;
93
- if (!data || typeof data !== "object") return;
94
- const { quotas } = data as NeuralwattQuotasUpdatedPayload;
95
- emitUsage(quotas);
96
-
97
- ctx.ui.setStatus("neuralwatt-usage", formatStatus(quotas, ctx.ui.theme));
98
- }
99
-
100
- pi.events.on("sub-core:ready", () => {
101
- subCoreReady = true;
102
- });
103
-
104
- pi.on("session_start", async (_event, ctx) => {
105
- unsubscribeQuotas?.();
106
- currentProvider = ctx.model?.provider;
107
- unsubscribeQuotas = pi.events.on(NEURALWATT_QUOTAS_UPDATED_EVENT, (data) =>
108
- handleQuotas(ctx, data),
109
- );
110
- });
111
-
112
- pi.on("model_select", async (_event, ctx) => {
113
- currentProvider = ctx.model?.provider;
114
-
115
- if (subCoreReady && isActive() && enabled) {
116
- requestQuotas();
117
- }
118
- });
119
-
120
- pi.on("session_shutdown", () => {
121
- unsubscribeQuotas?.();
122
- unsubscribeQuotas = undefined;
123
- currentProvider = undefined;
124
- });
125
-
126
- pi.events.on(NEURALWATT_EXTENSIONS_REQUEST_EVENT, () => {
127
- pi.events.emit(NEURALWATT_EXTENSIONS_REGISTER_EVENT, {
128
- feature: "subBarIntegration",
129
- });
130
- });
131
- }
@@ -1,47 +0,0 @@
1
- import type { NeuralwattQuotas } from "../../src/types/quota-api";
2
- import {
3
- percentCreditsRemaining,
4
- percentEnergyRemaining,
5
- } from "../../src/utils/quota-bar";
6
- import { formatKwh } from "../../src/utils/quota-format";
7
-
8
- interface RateWindow {
9
- label: string;
10
- usedPercent: number;
11
- resetDescription?: string;
12
- }
13
-
14
- export interface UsageSnapshot {
15
- provider: string;
16
- displayName: string;
17
- windows: RateWindow[];
18
- lastSuccessAt?: number;
19
- }
20
-
21
- export function toUsageSnapshot(quotas: NeuralwattQuotas): UsageSnapshot {
22
- const windows: RateWindow[] = [];
23
-
24
- const creditsRemaining = percentCreditsRemaining(quotas);
25
- const creditsUsed = 100 - creditsRemaining;
26
- windows.push({
27
- label: "Credits",
28
- usedPercent: Math.max(0, Math.min(100, creditsUsed)),
29
- });
30
-
31
- if (quotas.subscription) {
32
- const energyRemaining = percentEnergyRemaining(quotas.subscription);
33
- const energyUsed = 100 - energyRemaining;
34
- windows.push({
35
- label: "Energy",
36
- usedPercent: Math.max(0, Math.min(100, energyUsed)),
37
- resetDescription: `${formatKwh(quotas.subscription.kwh_remaining)} left`,
38
- });
39
- }
40
-
41
- return {
42
- provider: "neuralwatt",
43
- displayName: "Neuralwatt",
44
- windows,
45
- lastSuccessAt: Date.now(),
46
- };
47
- }