pi-cliproxyapi 0.1.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/README.md +101 -0
- package/index.ts +67 -0
- package/package.json +37 -0
- package/src/apply.ts +209 -0
- package/src/commands.ts +247 -0
- package/src/compat.ts +128 -0
- package/src/config.ts +199 -0
- package/src/conflicts.ts +66 -0
- package/src/fetch-models.ts +335 -0
- package/src/fetch-usage.ts +103 -0
- package/src/log.ts +19 -0
- package/src/ui-overlay.ts +292 -0
- package/src/ui-picker.ts +842 -0
- package/src/ui-setup.ts +289 -0
- package/src/ui-usage.ts +191 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// Discovery:
|
|
2
|
+
// 1. GET <host>/.well-known/pi with User-Agent: pi-cliproxyapi/<ver>
|
|
3
|
+
// → returns the server contract document (see PLAN.md).
|
|
4
|
+
// 2. On 404 / 5xx / non-JSON / network error → fall back to:
|
|
5
|
+
// GET <endpoint>/v1/models with Authorization: Bearer <apiKey>
|
|
6
|
+
// → classify locally via compat.ts.
|
|
7
|
+
//
|
|
8
|
+
// fetchDiscovery() returns a normalized in-memory model. Callers shouldn't
|
|
9
|
+
// know which path was used (except for logging).
|
|
10
|
+
|
|
11
|
+
import type { Api } from "@earendil-works/pi-ai";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
classifyCustom,
|
|
15
|
+
isExcluded,
|
|
16
|
+
modelDefaults,
|
|
17
|
+
normalizeSuggestedProvider,
|
|
18
|
+
reasoningFromId,
|
|
19
|
+
} from "./compat.ts";
|
|
20
|
+
import type { ProxyConfig } from "./config.ts";
|
|
21
|
+
import { log } from "./log.ts";
|
|
22
|
+
|
|
23
|
+
export const PLUGIN_USER_AGENT = "pi-cliproxyapi/0.1.0";
|
|
24
|
+
const REQUEST_TIMEOUT_MS = 5_000;
|
|
25
|
+
|
|
26
|
+
export interface DiscoveryModelEntry {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
reasoning: boolean;
|
|
30
|
+
contextWindow: number;
|
|
31
|
+
maxTokens: number;
|
|
32
|
+
cost: {
|
|
33
|
+
input: number;
|
|
34
|
+
output: number;
|
|
35
|
+
cacheRead: number;
|
|
36
|
+
cacheWrite: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DiscoveryBuiltinProvider {
|
|
41
|
+
/** "openai" or "anthropic" — name of the Pi built-in provider. */
|
|
42
|
+
name: string;
|
|
43
|
+
api: Api;
|
|
44
|
+
/** Models from upstream that map to this built-in provider. */
|
|
45
|
+
models: DiscoveryModelEntry[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DiscoveryCustomEntry extends DiscoveryModelEntry {
|
|
49
|
+
api: Api;
|
|
50
|
+
/** Suggested custom provider slug (e.g. "myproxy-glm"). */
|
|
51
|
+
suggestedProvider: string;
|
|
52
|
+
/** Raw upstream owned_by, for diagnostics. */
|
|
53
|
+
ownedBy: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface Discovery {
|
|
57
|
+
source: "well-known" | "v1-models";
|
|
58
|
+
upstreamVersion: string | null;
|
|
59
|
+
builtinProviders: DiscoveryBuiltinProvider[];
|
|
60
|
+
customPool: DiscoveryCustomEntry[];
|
|
61
|
+
serverDiscoveryExcludes: string[];
|
|
62
|
+
/** Total ids seen before any filtering. */
|
|
63
|
+
upstreamTotal: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface RawUpstreamModel {
|
|
67
|
+
id: string;
|
|
68
|
+
owned_by: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --------------------------------------------------------------------------- HTTP
|
|
72
|
+
|
|
73
|
+
async function fetchWithTimeout(
|
|
74
|
+
url: string,
|
|
75
|
+
init: RequestInit,
|
|
76
|
+
): Promise<Response> {
|
|
77
|
+
const ctrl = new AbortController();
|
|
78
|
+
const timer = setTimeout(
|
|
79
|
+
() => ctrl.abort(new Error("timeout")),
|
|
80
|
+
REQUEST_TIMEOUT_MS,
|
|
81
|
+
);
|
|
82
|
+
try {
|
|
83
|
+
return await fetch(url, { ...init, signal: ctrl.signal });
|
|
84
|
+
} finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function discoveryUrl(endpoint: string): string {
|
|
90
|
+
return new URL("/.well-known/pi", new URL(endpoint).origin).toString();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --------------------------------------------------------------------------- well-known path
|
|
94
|
+
|
|
95
|
+
async function tryWellKnown(cfg: ProxyConfig): Promise<Discovery | null> {
|
|
96
|
+
const url = discoveryUrl(cfg.proxy.endpoint);
|
|
97
|
+
let resp: Response;
|
|
98
|
+
try {
|
|
99
|
+
resp = await fetchWithTimeout(url, {
|
|
100
|
+
headers: { "User-Agent": PLUGIN_USER_AGENT, Accept: "application/json" },
|
|
101
|
+
});
|
|
102
|
+
} catch (err) {
|
|
103
|
+
log.warn(
|
|
104
|
+
"well-known fetch failed:",
|
|
105
|
+
(err as Error).message,
|
|
106
|
+
"— falling back to /v1/models",
|
|
107
|
+
);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (!resp.ok) {
|
|
111
|
+
log.warn(`well-known returned ${resp.status} — falling back to /v1/models`);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
let body: any;
|
|
115
|
+
try {
|
|
116
|
+
body = await resp.json();
|
|
117
|
+
} catch {
|
|
118
|
+
log.warn("well-known returned non-JSON — falling back to /v1/models");
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
if (!body || body.schemaVersion !== 1) {
|
|
122
|
+
log.warn("well-known schemaVersion != 1 — falling back to /v1/models");
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const builtin: DiscoveryBuiltinProvider[] = [];
|
|
127
|
+
const builtinProviders = (body.builtinProviders ?? {}) as Record<string, any>;
|
|
128
|
+
for (const [name, p] of Object.entries(builtinProviders)) {
|
|
129
|
+
if (!p || !Array.isArray(p.models)) continue;
|
|
130
|
+
const models: DiscoveryModelEntry[] = p.models.map(
|
|
131
|
+
(m: any): DiscoveryModelEntry => ({
|
|
132
|
+
id: String(m.id),
|
|
133
|
+
name: typeof m.name === "string" ? m.name : String(m.id),
|
|
134
|
+
reasoning: Boolean(m.reasoning ?? reasoningFromId(String(m.id))),
|
|
135
|
+
contextWindow:
|
|
136
|
+
typeof m.contextWindow === "number" ? m.contextWindow : 200_000,
|
|
137
|
+
maxTokens: typeof m.maxTokens === "number" ? m.maxTokens : 16_000,
|
|
138
|
+
cost: m.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
builtin.push({ name, api: (p.api as Api) ?? "openai-responses", models });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const customPool: DiscoveryCustomEntry[] = (
|
|
145
|
+
Array.isArray(body.customModelPool) ? body.customModelPool : []
|
|
146
|
+
).map(
|
|
147
|
+
(m: any): DiscoveryCustomEntry => ({
|
|
148
|
+
id: String(m.id),
|
|
149
|
+
name: typeof m.name === "string" ? m.name : String(m.id),
|
|
150
|
+
reasoning: Boolean(m.reasoning ?? reasoningFromId(String(m.id))),
|
|
151
|
+
contextWindow:
|
|
152
|
+
typeof m.contextWindow === "number" ? m.contextWindow : 128_000,
|
|
153
|
+
maxTokens: typeof m.maxTokens === "number" ? m.maxTokens : 16_000,
|
|
154
|
+
cost: m.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
155
|
+
api: (m.api as Api) ?? "openai-completions",
|
|
156
|
+
suggestedProvider: normalizeSuggestedProvider(
|
|
157
|
+
typeof m.suggestedProviderName === "string"
|
|
158
|
+
? m.suggestedProviderName
|
|
159
|
+
: "misc",
|
|
160
|
+
cfg.proxy.providerPrefix,
|
|
161
|
+
),
|
|
162
|
+
ownedBy: typeof m.owned_by === "string" ? m.owned_by : "",
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
source: "well-known",
|
|
168
|
+
upstreamVersion:
|
|
169
|
+
typeof body?.upstream?.upstreamVersion === "string"
|
|
170
|
+
? body.upstream.upstreamVersion
|
|
171
|
+
: null,
|
|
172
|
+
builtinProviders: builtin,
|
|
173
|
+
customPool,
|
|
174
|
+
serverDiscoveryExcludes: Array.isArray(body.discoveryExcludes)
|
|
175
|
+
? body.discoveryExcludes.filter(
|
|
176
|
+
(s: unknown): s is string => typeof s === "string",
|
|
177
|
+
)
|
|
178
|
+
: [],
|
|
179
|
+
upstreamTotal:
|
|
180
|
+
typeof body?.counts?.upstreamTotal === "number"
|
|
181
|
+
? body.counts.upstreamTotal
|
|
182
|
+
: 0,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --------------------------------------------------------------------------- /v1/models path
|
|
187
|
+
|
|
188
|
+
async function fetchRawModels(
|
|
189
|
+
cfg: ProxyConfig,
|
|
190
|
+
resolvedKey: string,
|
|
191
|
+
): Promise<RawUpstreamModel[]> {
|
|
192
|
+
const url = new URL(
|
|
193
|
+
"/v1/models",
|
|
194
|
+
new URL(cfg.proxy.endpoint).origin,
|
|
195
|
+
).toString();
|
|
196
|
+
const resp = await fetchWithTimeout(url, {
|
|
197
|
+
headers: {
|
|
198
|
+
Authorization: `Bearer ${resolvedKey}`,
|
|
199
|
+
Accept: "application/json",
|
|
200
|
+
"User-Agent": PLUGIN_USER_AGENT,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
if (!resp.ok) {
|
|
204
|
+
throw new Error(`/v1/models returned ${resp.status}`);
|
|
205
|
+
}
|
|
206
|
+
const body = (await resp.json()) as {
|
|
207
|
+
data?: Array<{ id?: unknown; owned_by?: unknown }>;
|
|
208
|
+
};
|
|
209
|
+
if (!body?.data || !Array.isArray(body.data)) return [];
|
|
210
|
+
return body.data
|
|
211
|
+
.map((m) => ({
|
|
212
|
+
id: typeof m.id === "string" ? m.id : "",
|
|
213
|
+
owned_by: typeof m.owned_by === "string" ? m.owned_by : "",
|
|
214
|
+
}))
|
|
215
|
+
.filter((m) => m.id);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function classifyLocally(
|
|
219
|
+
raw: RawUpstreamModel[],
|
|
220
|
+
cfg: ProxyConfig,
|
|
221
|
+
): Discovery {
|
|
222
|
+
const excludes = cfg.discoveryExcludes;
|
|
223
|
+
const builtinByName = new Map<string, DiscoveryBuiltinProvider>();
|
|
224
|
+
const customPool: DiscoveryCustomEntry[] = [];
|
|
225
|
+
let upstreamTotal = 0;
|
|
226
|
+
|
|
227
|
+
for (const m of raw) {
|
|
228
|
+
upstreamTotal++;
|
|
229
|
+
if (isExcluded(m.id, excludes)) continue;
|
|
230
|
+
|
|
231
|
+
if (m.owned_by === "openai") {
|
|
232
|
+
const entry = modelDefaults(m.id);
|
|
233
|
+
pushBuiltin(
|
|
234
|
+
builtinByName,
|
|
235
|
+
"openai",
|
|
236
|
+
"openai-responses",
|
|
237
|
+
entryToDiscovery(entry),
|
|
238
|
+
);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (m.owned_by === "anthropic") {
|
|
242
|
+
const entry = modelDefaults(m.id);
|
|
243
|
+
pushBuiltin(
|
|
244
|
+
builtinByName,
|
|
245
|
+
"anthropic",
|
|
246
|
+
"anthropic-messages",
|
|
247
|
+
entryToDiscovery(entry),
|
|
248
|
+
);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const { slug, api } = classifyCustom(m.owned_by, cfg.proxy.providerPrefix);
|
|
252
|
+
const base = modelDefaults(m.id);
|
|
253
|
+
customPool.push({
|
|
254
|
+
id: m.id,
|
|
255
|
+
name: base.name ?? m.id,
|
|
256
|
+
reasoning: base.reasoning ?? false,
|
|
257
|
+
contextWindow: base.contextWindow ?? 128_000,
|
|
258
|
+
maxTokens: base.maxTokens ?? 16_000,
|
|
259
|
+
cost: base.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
260
|
+
api,
|
|
261
|
+
suggestedProvider: slug,
|
|
262
|
+
ownedBy: m.owned_by,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
source: "v1-models",
|
|
268
|
+
upstreamVersion: null,
|
|
269
|
+
builtinProviders: Array.from(builtinByName.values()),
|
|
270
|
+
customPool,
|
|
271
|
+
serverDiscoveryExcludes: [],
|
|
272
|
+
upstreamTotal,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function pushBuiltin(
|
|
277
|
+
map: Map<string, DiscoveryBuiltinProvider>,
|
|
278
|
+
name: string,
|
|
279
|
+
api: Api,
|
|
280
|
+
entry: DiscoveryModelEntry,
|
|
281
|
+
): void {
|
|
282
|
+
let p = map.get(name);
|
|
283
|
+
if (!p) {
|
|
284
|
+
p = { name, api, models: [] };
|
|
285
|
+
map.set(name, p);
|
|
286
|
+
}
|
|
287
|
+
p.models.push(entry);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function entryToDiscovery(
|
|
291
|
+
base: ReturnType<typeof modelDefaults>,
|
|
292
|
+
): DiscoveryModelEntry {
|
|
293
|
+
return {
|
|
294
|
+
id: base.id,
|
|
295
|
+
name: base.name ?? base.id,
|
|
296
|
+
reasoning: base.reasoning ?? false,
|
|
297
|
+
contextWindow: base.contextWindow ?? 128_000,
|
|
298
|
+
maxTokens: base.maxTokens ?? 16_000,
|
|
299
|
+
cost: base.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// --------------------------------------------------------------------------- public
|
|
304
|
+
|
|
305
|
+
export async function fetchDiscovery(
|
|
306
|
+
cfg: ProxyConfig,
|
|
307
|
+
resolvedKey: string,
|
|
308
|
+
): Promise<Discovery> {
|
|
309
|
+
const wk = await tryWellKnown(cfg);
|
|
310
|
+
if (wk) {
|
|
311
|
+
log.info(
|
|
312
|
+
`discovery via /.well-known/pi: ${wk.builtinProviders.length} builtin, ${wk.customPool.length} custom`,
|
|
313
|
+
);
|
|
314
|
+
return wk;
|
|
315
|
+
}
|
|
316
|
+
if (!resolvedKey) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
"well-known unavailable AND proxy apiKey is empty — cannot fall back to /v1/models. Set proxy.apiKey in config.",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const raw = await fetchRawModels(cfg, resolvedKey);
|
|
322
|
+
const d = classifyLocally(raw, cfg);
|
|
323
|
+
log.info(
|
|
324
|
+
`discovery via /v1/models: ${d.builtinProviders.length} builtin, ${d.customPool.length} custom`,
|
|
325
|
+
);
|
|
326
|
+
return d;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Convenience: flat set of all upstream ids (after server-applied excludes). */
|
|
330
|
+
export function discoveryToIdSet(d: Discovery): Set<string> {
|
|
331
|
+
const out = new Set<string>();
|
|
332
|
+
for (const p of d.builtinProviders) for (const m of p.models) out.add(m.id);
|
|
333
|
+
for (const m of d.customPool) out.add(m.id);
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// /api/usage client.
|
|
2
|
+
//
|
|
3
|
+
// GET <host>/api/usage with X-Plugin-Key. Returns the parsed document, with a
|
|
4
|
+
// small in-memory TTL cache. Caller passes `force: true` to bypass the cache.
|
|
5
|
+
|
|
6
|
+
import type { ProxyConfig } from "./config.ts";
|
|
7
|
+
import { PLUGIN_USER_AGENT } from "./fetch-models.ts";
|
|
8
|
+
import { log } from "./log.ts";
|
|
9
|
+
|
|
10
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
11
|
+
|
|
12
|
+
export interface UsageGroup {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
remainingFraction: number;
|
|
16
|
+
resetTime: string | null;
|
|
17
|
+
models?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UsageAccount {
|
|
21
|
+
provider: string;
|
|
22
|
+
account: string;
|
|
23
|
+
authIndex: string;
|
|
24
|
+
label: string;
|
|
25
|
+
status: string;
|
|
26
|
+
disabled: boolean;
|
|
27
|
+
unavailable: boolean;
|
|
28
|
+
success: number;
|
|
29
|
+
failed: number;
|
|
30
|
+
lastRequestAt: string | null;
|
|
31
|
+
supported: boolean;
|
|
32
|
+
error?: string;
|
|
33
|
+
groups?: UsageGroup[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UsageDocument {
|
|
37
|
+
schemaVersion: number;
|
|
38
|
+
generatedAt: string;
|
|
39
|
+
accounts: UsageAccount[];
|
|
40
|
+
unsupportedProviders: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface CacheEntry {
|
|
44
|
+
fetchedAt: number;
|
|
45
|
+
doc: UsageDocument;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let cache: CacheEntry | null = null;
|
|
49
|
+
|
|
50
|
+
export function clearUsageCache(): void {
|
|
51
|
+
cache = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function fetchUsage(
|
|
55
|
+
cfg: ProxyConfig,
|
|
56
|
+
resolvedUsageKey: string,
|
|
57
|
+
opts: { force?: boolean } = {},
|
|
58
|
+
): Promise<UsageDocument> {
|
|
59
|
+
if (
|
|
60
|
+
!opts.force &&
|
|
61
|
+
cache &&
|
|
62
|
+
Date.now() - cache.fetchedAt < cfg.usageCacheTtlMs
|
|
63
|
+
) {
|
|
64
|
+
return cache.doc;
|
|
65
|
+
}
|
|
66
|
+
if (!resolvedUsageKey) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"usage key not configured; set proxy.usageKey in config and rerun",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const url = new URL(
|
|
72
|
+
"/api/usage",
|
|
73
|
+
new URL(cfg.proxy.endpoint).origin,
|
|
74
|
+
).toString();
|
|
75
|
+
const ctrl = new AbortController();
|
|
76
|
+
const timer = setTimeout(
|
|
77
|
+
() => ctrl.abort(new Error("timeout")),
|
|
78
|
+
REQUEST_TIMEOUT_MS,
|
|
79
|
+
);
|
|
80
|
+
let resp: Response;
|
|
81
|
+
try {
|
|
82
|
+
resp = await fetch(url, {
|
|
83
|
+
headers: {
|
|
84
|
+
"X-Plugin-Key": resolvedUsageKey,
|
|
85
|
+
Accept: "application/json",
|
|
86
|
+
"User-Agent": PLUGIN_USER_AGENT,
|
|
87
|
+
},
|
|
88
|
+
signal: ctrl.signal,
|
|
89
|
+
});
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
if (!resp.ok) {
|
|
94
|
+
throw new Error(`/api/usage returned ${resp.status}`);
|
|
95
|
+
}
|
|
96
|
+
const body = (await resp.json()) as UsageDocument;
|
|
97
|
+
if (!body || body.schemaVersion !== 1 || !Array.isArray(body.accounts)) {
|
|
98
|
+
throw new Error("/api/usage returned unexpected payload shape");
|
|
99
|
+
}
|
|
100
|
+
cache = { fetchedAt: Date.now(), doc: body };
|
|
101
|
+
log.debug("usage fetched, accounts:", body.accounts.length);
|
|
102
|
+
return body;
|
|
103
|
+
}
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Tiny logger that prefixes all messages with the extension tag.
|
|
2
|
+
// Uses console.* directly — pi pipes stdout/stderr into its log sink.
|
|
3
|
+
|
|
4
|
+
const TAG = "[pi-cliproxyapi]";
|
|
5
|
+
|
|
6
|
+
export const log = {
|
|
7
|
+
info(...args: unknown[]): void {
|
|
8
|
+
console.log(TAG, ...args);
|
|
9
|
+
},
|
|
10
|
+
warn(...args: unknown[]): void {
|
|
11
|
+
console.warn(TAG, ...args);
|
|
12
|
+
},
|
|
13
|
+
error(...args: unknown[]): void {
|
|
14
|
+
console.error(TAG, ...args);
|
|
15
|
+
},
|
|
16
|
+
debug(...args: unknown[]): void {
|
|
17
|
+
if (process.env.PI_CLIPROXYAPI_DEBUG) console.log(TAG, "[debug]", ...args);
|
|
18
|
+
},
|
|
19
|
+
};
|