@aliou/pi-synthetic 0.10.0 → 0.10.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-synthetic",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -15,50 +15,56 @@ export function registerQuotasCommand(pi: ExtensionAPI): void {
15
15
  ctx.ui.notify(MISSING_AUTH_MESSAGE, "warning");
16
16
  return;
17
17
  }
18
+ const key: string = apiKey;
18
19
 
19
20
  const result = await ctx.ui.custom<null>((tui, theme, _kb, done) => {
20
- const component = new QuotasComponent(theme, () => done(null));
21
+ const controller = new AbortController();
22
+ const component = new QuotasComponent(theme, tui, () => {
23
+ controller.abort();
24
+ done(null);
25
+ });
21
26
 
22
- fetchQuotas(apiKey)
23
- .then((quotas) => {
24
- if (!quotas) {
25
- component.setState({
26
- type: "error",
27
- message:
28
- "Failed to fetch quotas. Check your Synthetic subscription status.",
29
- });
30
- } else {
31
- component.setState({ type: "loaded", quotas });
32
- }
33
- tui.requestRender();
34
- })
35
- .catch(() => {
27
+ async function loadQuotas(): Promise<void> {
28
+ const fetchResult = await fetchQuotas(key, controller.signal);
29
+ if (controller.signal.aborted) return;
30
+ if (fetchResult.success) {
31
+ component.setState({
32
+ type: "loaded",
33
+ quotas: fetchResult.data.quotas,
34
+ });
35
+ } else {
36
36
  component.setState({
37
37
  type: "error",
38
- message:
39
- "Failed to fetch quotas. Check your Synthetic subscription status.",
38
+ message: fetchResult.error.message,
40
39
  });
41
- tui.requestRender();
42
- });
40
+ }
41
+ tui.requestRender();
42
+ }
43
+
44
+ void loadQuotas();
43
45
 
44
46
  return {
45
47
  render: (width: number) => component.render(width),
46
48
  invalidate: () => component.invalidate(),
47
49
  handleInput: (data: string) => component.handleInput(data),
50
+ dispose: () => {
51
+ controller.abort();
52
+ component.destroy();
53
+ },
48
54
  };
49
55
  });
50
56
 
51
- // RPC fallback: return JSON
57
+ // Non-interactive fallback (RPC, print, JSON modes)
52
58
  if (result === undefined) {
53
- const quotas = await fetchQuotas(apiKey);
54
- if (!quotas) {
59
+ const fetchResult = await fetchQuotas(key);
60
+ if (!fetchResult.success) {
55
61
  ctx.ui.notify(
56
- JSON.stringify({ error: "Failed to fetch quotas" }),
62
+ JSON.stringify({ error: fetchResult.error.message }),
57
63
  "error",
58
64
  );
59
65
  return;
60
66
  }
61
- ctx.ui.notify(JSON.stringify(quotas, null, 2), "info");
67
+ ctx.ui.notify(JSON.stringify(fetchResult.data.quotas), "info");
62
68
  }
63
69
  },
64
70
  });
@@ -1,7 +1,8 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { DynamicBorder } from "@mariozechner/pi-coding-agent";
3
- import type { Component } from "@mariozechner/pi-tui";
3
+ import type { Component, TUI } from "@mariozechner/pi-tui";
4
4
  import {
5
+ Loader,
5
6
  matchesKey,
6
7
  truncateToWidth,
7
8
  visibleWidth,
@@ -251,14 +252,36 @@ function renderSimpleIndicatorBar(
251
252
  export class QuotasComponent implements Component {
252
253
  private state: QuotasState = { type: "loading" };
253
254
  private theme: Theme;
255
+ private tui: TUI;
254
256
  private onClose: () => void;
257
+ private loader: Loader | null = null;
255
258
 
256
- constructor(theme: Theme, onClose: () => void) {
259
+ constructor(theme: Theme, tui: TUI, onClose: () => void) {
257
260
  this.theme = theme;
261
+ this.tui = tui;
258
262
  this.onClose = onClose;
263
+ this.startLoader();
264
+ }
265
+
266
+ private startLoader(): void {
267
+ this.loader = new Loader(
268
+ this.tui,
269
+ (s: string) => this.theme.fg("accent", s),
270
+ (s: string) => this.theme.fg("muted", s),
271
+ "Fetching quotas...",
272
+ );
273
+ }
274
+
275
+ destroy(): void {
276
+ this.loader?.stop();
277
+ this.loader = null;
259
278
  }
260
279
 
261
280
  setState(state: QuotasState): void {
281
+ if (this.state.type === "loading" && state.type !== "loading") {
282
+ this.loader?.stop();
283
+ this.loader = null;
284
+ }
262
285
  this.state = state;
263
286
  }
264
287
 
@@ -286,7 +309,11 @@ export class QuotasComponent implements Component {
286
309
 
287
310
  switch (this.state.type) {
288
311
  case "loading":
289
- lines.push(this.theme.fg("muted", " Loading..."));
312
+ if (this.loader) {
313
+ lines.push(...this.loader.render(width));
314
+ } else {
315
+ lines.push(this.theme.fg("muted", " Fetching quotas..."));
316
+ }
290
317
  break;
291
318
  case "error":
292
319
  lines.push(this.theme.fg("error", ` ${this.state.message}`));
@@ -107,10 +107,13 @@ async function emitCurrentUsage(
107
107
  ): Promise<void> {
108
108
  const apiKey = await getSyntheticApiKey(authStorage);
109
109
  if (!apiKey) return;
110
- const quotas = await fetchQuotas(apiKey);
111
- if (!quotas) return;
110
+ const result = await fetchQuotas(apiKey);
111
+ if (!result.success) return;
112
112
  pi.events.emit("sub-core:update-current", {
113
- state: { provider: "synthetic", usage: toUsageSnapshot(quotas) },
113
+ state: {
114
+ provider: "synthetic",
115
+ usage: toUsageSnapshot(result.data.quotas),
116
+ },
114
117
  });
115
118
  }
116
119
 
@@ -36,7 +36,7 @@ const SYNTHETIC_REASONING_EFFORT_MAP = {
36
36
  } as const;
37
37
 
38
38
  export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
39
- // API: hf:zai-org/GLM-4.7 → ctx=202752, out=65536
39
+ // API: hf:zai-org/GLM-4.7 → ctx=202752, multimodal
40
40
  {
41
41
  id: "hf:zai-org/GLM-4.7",
42
42
  name: "zai-org/GLM-4.7",
@@ -45,11 +45,11 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
45
45
  supportsReasoningEffort: true,
46
46
  reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
47
47
  },
48
- input: ["text"],
48
+ input: ["text", "image"],
49
49
  cost: {
50
- input: 0.45,
50
+ input: 2.19,
51
51
  output: 2.19,
52
- cacheRead: 0.45,
52
+ cacheRead: 2.19,
53
53
  cacheWrite: 0,
54
54
  },
55
55
  contextWindow: 202752,
@@ -74,6 +74,26 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
74
74
  contextWindow: 196608,
75
75
  maxTokens: 65536,
76
76
  },
77
+ // API: hf:zai-org/GLM-5.1 → ctx=196608, out=65536
78
+ {
79
+ id: "hf:zai-org/GLM-5.1",
80
+ name: "zai-org/GLM-5.1",
81
+ reasoning: true,
82
+ compat: {
83
+ supportsReasoningEffort: true,
84
+ reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
85
+ supportsDeveloperRole: false,
86
+ },
87
+ input: ["text"],
88
+ cost: {
89
+ input: 1,
90
+ output: 3,
91
+ cacheRead: 1,
92
+ cacheWrite: 0,
93
+ },
94
+ contextWindow: 196608,
95
+ maxTokens: 65536,
96
+ },
77
97
  // API: hf:zai-org/GLM-4.7-Flash → ctx=196608
78
98
  {
79
99
  id: "hf:zai-org/GLM-4.7-Flash",
@@ -1,3 +1,14 @@
1
+ export type QuotasErrorKind =
2
+ | "cancelled"
3
+ | "timeout"
4
+ | "config"
5
+ | "http"
6
+ | "network";
7
+
8
+ export type QuotasResult =
9
+ | { success: true; data: { quotas: QuotasResponse } }
10
+ | { success: false; error: { message: string; kind: QuotasErrorKind } };
11
+
1
12
  export interface QuotasResponse {
2
13
  subscription?: {
3
14
  limit: number;
@@ -1,20 +1,73 @@
1
- import type { QuotasResponse } from "../types/quotas";
1
+ import type { QuotasResponse, QuotasResult } from "../types/quotas";
2
+
3
+ const FETCH_TIMEOUT_MS = 15_000;
4
+
5
+ function isTimeoutReason(reason: unknown): boolean {
6
+ return (
7
+ (reason instanceof DOMException && reason.name === "TimeoutError") ||
8
+ (reason instanceof Error && reason.name === "TimeoutError")
9
+ );
10
+ }
2
11
 
3
12
  export async function fetchQuotas(
4
13
  apiKey: string,
5
- ): Promise<QuotasResponse | null> {
6
- if (!apiKey) return null;
14
+ signal?: AbortSignal,
15
+ ): Promise<QuotasResult> {
16
+ if (!apiKey) {
17
+ return {
18
+ success: false,
19
+ error: { message: "No API key provided", kind: "config" },
20
+ };
21
+ }
22
+
23
+ const signals: AbortSignal[] = [AbortSignal.timeout(FETCH_TIMEOUT_MS)];
24
+ if (signal) signals.push(signal);
25
+ const combined = AbortSignal.any(signals);
7
26
 
8
27
  try {
9
28
  const response = await fetch("https://api.synthetic.new/v2/quotas", {
10
29
  headers: { Authorization: `Bearer ${apiKey}` },
30
+ signal: combined,
11
31
  });
12
32
 
13
- if (!response.ok) return null;
33
+ if (!response.ok) {
34
+ let message = response.statusText;
35
+ try {
36
+ const body = await response.text();
37
+ if (body) {
38
+ try {
39
+ const parsed = JSON.parse(body) as { error?: string };
40
+ if (parsed.error) message = parsed.error;
41
+ } catch {
42
+ message = body;
43
+ }
44
+ }
45
+ } catch {
46
+ return { success: false, error: { message, kind: "http" } };
47
+ }
48
+ return { success: false, error: { message, kind: "http" } };
49
+ }
50
+
14
51
  const data: QuotasResponse = await response.json();
15
- return data;
16
- } catch {
17
- return null;
52
+ return { success: true, data: { quotas: data } };
53
+ } catch (err: unknown) {
54
+ const isAbort =
55
+ combined.aborted ||
56
+ (err instanceof DOMException && err.name === "AbortError");
57
+ if (isAbort) {
58
+ if (isTimeoutReason(combined.reason)) {
59
+ return {
60
+ success: false,
61
+ error: { message: "Request timed out", kind: "timeout" },
62
+ };
63
+ }
64
+ return {
65
+ success: false,
66
+ error: { message: "Request cancelled", kind: "cancelled" },
67
+ };
68
+ }
69
+ const message = err instanceof Error ? err.message : "Unknown error";
70
+ return { success: false, error: { message, kind: "network" } };
18
71
  }
19
72
  }
20
73