pi-hypercharm-provider 1.3.27 → 1.3.29
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/AGENTS.md +24 -0
- package/README.md +7 -5
- package/identity.ts +49 -0
- package/index.ts +144 -39
- package/notify.ts +50 -0
- package/package.json +15 -9
- package/prism.ts +43 -0
- package/tests/fixtures/official-surface.ts +42 -0
- package/tests/identity.test.ts +76 -0
- package/tests/notify.test.ts +111 -0
- package/tests/prism.test.ts +67 -0
- package/tests/provider.integration.test.ts +485 -0
- package/bun.lock +0 -342
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-principles co-installation guard: every identifier this extension
|
|
3
|
+
* registers into a shared Pi surface must be namespaced under PROVIDER_ID and
|
|
4
|
+
* disjoint from the official provider's reserved names.
|
|
5
|
+
*
|
|
6
|
+
* OFFICIAL_RESERVED mirrors @charmland/pi-hyper-provider@0.4.0's registration
|
|
7
|
+
* surface (src/hyper.ts, src/index.ts, src/settings.ts, src/credits.ts). Keep
|
|
8
|
+
* it in sync when the official package adds or renames a shared identifier: a
|
|
9
|
+
* failure here means the two extensions could collide on that surface.
|
|
10
|
+
*/
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { test } from "node:test";
|
|
13
|
+
import {
|
|
14
|
+
API_KEY_ENV,
|
|
15
|
+
API_NAME,
|
|
16
|
+
IDENTIFIERS,
|
|
17
|
+
PRISM_ENTRY_TYPE,
|
|
18
|
+
PROVIDER_ID,
|
|
19
|
+
STATUS_COMMAND,
|
|
20
|
+
STATUS_KEY_ACCOUNT,
|
|
21
|
+
STATUS_KEY_SESSION,
|
|
22
|
+
WIDGET_KEY,
|
|
23
|
+
} from "../identity.ts";
|
|
24
|
+
|
|
25
|
+
const OFFICIAL_RESERVED = [
|
|
26
|
+
"hyper", // provider id and status key (PROVIDER_NAME)
|
|
27
|
+
"Charm Hyper", // provider + oauth display name
|
|
28
|
+
"hyper-status", // /hyper-status command
|
|
29
|
+
"hyper-prism-route", // prism entry customType
|
|
30
|
+
"hyper-provider", // ~/.pi/agent/hyper-provider settings dir
|
|
31
|
+
"HYPER_API_KEY", // envApiKeyAuth env var (auth key)
|
|
32
|
+
"openai-completions", // api name (openAICompletionsApi)
|
|
33
|
+
"@charmland/pi-hyper-provider", // npm package name
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
test("every registered identifier is namespaced under the provider id", () => {
|
|
37
|
+
for (const identifier of IDENTIFIERS) {
|
|
38
|
+
assert.equal(typeof identifier, "string");
|
|
39
|
+
assert.ok(identifier.length > 0);
|
|
40
|
+
// Normalize the two conventions we use: env vars are upper-cased
|
|
41
|
+
// (HYPERCHARM_API_KEY) and pi's env-var placeholders carry a "$" sigil.
|
|
42
|
+
const normalized = identifier.replace(/^\$/, "").toLowerCase();
|
|
43
|
+
assert.ok(normalized.startsWith(PROVIDER_ID), identifier + " must start with " + PROVIDER_ID);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("identifiers are unique", () => {
|
|
48
|
+
assert.equal(new Set(IDENTIFIERS).size, IDENTIFIERS.length);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("no identifier collides with the official provider's reserved names", () => {
|
|
52
|
+
for (const identifier of IDENTIFIERS) {
|
|
53
|
+
assert.ok(!OFFICIAL_RESERVED.includes(identifier), identifier + " collides with the official provider");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("the specific shared surfaces differ from the official registration", () => {
|
|
58
|
+
assert.equal(PROVIDER_ID, "hypercharm");
|
|
59
|
+
assert.notEqual(PROVIDER_ID, "hyper");
|
|
60
|
+
assert.notEqual(API_NAME, "openai-completions");
|
|
61
|
+
assert.notEqual(API_KEY_ENV, "HYPER_API_KEY");
|
|
62
|
+
assert.notEqual(STATUS_COMMAND, "hyper-status");
|
|
63
|
+
assert.notEqual(PRISM_ENTRY_TYPE, "hyper-prism-route");
|
|
64
|
+
assert.notEqual(STATUS_KEY_SESSION, "hyper");
|
|
65
|
+
assert.notEqual(STATUS_KEY_ACCOUNT, "hyper");
|
|
66
|
+
assert.notEqual(WIDGET_KEY, "hyper");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("status and widget keys stay disjoint so pi cannot cross-clear them", () => {
|
|
70
|
+
assert.ok(WIDGET_KEY.startsWith(PROVIDER_ID));
|
|
71
|
+
assert.ok(STATUS_KEY_SESSION.startsWith(PROVIDER_ID + "-"));
|
|
72
|
+
assert.ok(STATUS_KEY_ACCOUNT.startsWith(PROVIDER_ID + "-"));
|
|
73
|
+
assert.notEqual(STATUS_KEY_SESSION, STATUS_KEY_ACCOUNT);
|
|
74
|
+
assert.notEqual(STATUS_KEY_SESSION, WIDGET_KEY);
|
|
75
|
+
assert.notEqual(STATUS_KEY_ACCOUNT, WIDGET_KEY);
|
|
76
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The warning sink is the difference between a degraded session and a silent
|
|
3
|
+
* one, so its three behaviours are pinned here: deduplication, UI routing once
|
|
4
|
+
* a UI session activates it, and a stderr fallback when the captured ctx has
|
|
5
|
+
* gone stale (a refresh landing after its session was replaced).
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import { createNotifier } from "../notify.ts";
|
|
10
|
+
|
|
11
|
+
function captureStderr() {
|
|
12
|
+
const chunks = [];
|
|
13
|
+
const original = process.stderr.write;
|
|
14
|
+
process.stderr.write = (chunk) => {
|
|
15
|
+
chunks.push(String(chunk));
|
|
16
|
+
return true;
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
chunks,
|
|
20
|
+
restore() {
|
|
21
|
+
process.stderr.write = original;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fakeContext(hasUI, notify) {
|
|
27
|
+
return { hasUI, ui: { notify: notify ?? (() => {}) } };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test("warnings are deduplicated and reach stderr before any UI exists", () => {
|
|
31
|
+
const stderr = captureStderr();
|
|
32
|
+
try {
|
|
33
|
+
const notifier = createNotifier();
|
|
34
|
+
notifier.warn("first failure");
|
|
35
|
+
notifier.warn("first failure");
|
|
36
|
+
notifier.warn("second failure");
|
|
37
|
+
const lines = stderr.chunks.filter((chunk) => chunk.includes("HyperCharm warning:"));
|
|
38
|
+
assert.equal(lines.length, 2, "each distinct warning is emitted once");
|
|
39
|
+
assert.ok(lines[0].includes("first failure"));
|
|
40
|
+
assert.ok(lines[1].includes("second failure"));
|
|
41
|
+
} finally {
|
|
42
|
+
stderr.restore();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("activation routes later warnings through the session UI as warnings", () => {
|
|
47
|
+
const stderr = captureStderr();
|
|
48
|
+
try {
|
|
49
|
+
const notifications = [];
|
|
50
|
+
const notifier = createNotifier();
|
|
51
|
+
notifier.activate(fakeContext(true, (message, type) => notifications.push([message, type])));
|
|
52
|
+
notifier.warn("catalog refresh failed");
|
|
53
|
+
notifier.warn("catalog refresh failed");
|
|
54
|
+
assert.deepEqual(notifications, [["catalog refresh failed", "warning"]]);
|
|
55
|
+
assert.equal(
|
|
56
|
+
stderr.chunks.some((chunk) => chunk.includes("catalog refresh failed")),
|
|
57
|
+
false,
|
|
58
|
+
"an activated notifier must not also write to stderr",
|
|
59
|
+
);
|
|
60
|
+
} finally {
|
|
61
|
+
stderr.restore();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a stale context falls back to stderr instead of throwing", () => {
|
|
66
|
+
const stderr = captureStderr();
|
|
67
|
+
try {
|
|
68
|
+
const notifier = createNotifier();
|
|
69
|
+
notifier.activate(
|
|
70
|
+
fakeContext(true, () => {
|
|
71
|
+
throw new Error("This extension ctx is stale");
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
assert.doesNotThrow(() => notifier.warn("after session replacement"));
|
|
75
|
+
assert.ok(stderr.chunks.some((chunk) => chunk.includes("after session replacement")));
|
|
76
|
+
} finally {
|
|
77
|
+
stderr.restore();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a non-UI session never claims the warning channel", () => {
|
|
82
|
+
const stderr = captureStderr();
|
|
83
|
+
try {
|
|
84
|
+
const notifications = [];
|
|
85
|
+
const notifier = createNotifier();
|
|
86
|
+
notifier.activate(fakeContext(false, (message) => notifications.push(message)));
|
|
87
|
+
notifier.warn("headless failure");
|
|
88
|
+
assert.deepEqual(notifications, []);
|
|
89
|
+
assert.ok(stderr.chunks.some((chunk) => chunk.includes("headless failure")));
|
|
90
|
+
} finally {
|
|
91
|
+
stderr.restore();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a context that throws on hasUI is ignored rather than fatal", () => {
|
|
96
|
+
const stderr = captureStderr();
|
|
97
|
+
try {
|
|
98
|
+
const notifier = createNotifier();
|
|
99
|
+
const hostile = {
|
|
100
|
+
get hasUI() {
|
|
101
|
+
throw new Error("stale ctx");
|
|
102
|
+
},
|
|
103
|
+
ui: { notify: () => {} },
|
|
104
|
+
};
|
|
105
|
+
assert.doesNotThrow(() => notifier.activate(hostile));
|
|
106
|
+
notifier.warn("still reported");
|
|
107
|
+
assert.ok(stderr.chunks.some((chunk) => chunk.includes("still reported")));
|
|
108
|
+
} finally {
|
|
109
|
+
stderr.restore();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { PRISM_ENTRY_TYPE, prismLabel, prismRouteFromHeaders, prismRouteLabel, readPrismRoute } from "../prism.ts";
|
|
4
|
+
|
|
5
|
+
test("prismLabel accepts trimmed printable labels", () => {
|
|
6
|
+
assert.equal(prismLabel(" GLM 5.3 Flash "), "GLM 5.3 Flash");
|
|
7
|
+
assert.equal(prismLabel("glm-5.3-flash"), "glm-5.3-flash");
|
|
8
|
+
assert.equal(prismLabel("model-\u30c7\u30fc\u30bf"), "model-\u30c7\u30fc\u30bf");
|
|
9
|
+
assert.equal(prismLabel("x".repeat(200)), "x".repeat(200));
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("prismLabel rejects unusable labels", () => {
|
|
13
|
+
const rejected = [
|
|
14
|
+
undefined,
|
|
15
|
+
null,
|
|
16
|
+
42,
|
|
17
|
+
{},
|
|
18
|
+
[],
|
|
19
|
+
"",
|
|
20
|
+
" ",
|
|
21
|
+
"x".repeat(201),
|
|
22
|
+
"bad\u001b[31m",
|
|
23
|
+
"bad\nline",
|
|
24
|
+
"bad\u202etext",
|
|
25
|
+
"bad\u0000",
|
|
26
|
+
];
|
|
27
|
+
for (const value of rejected) {
|
|
28
|
+
assert.equal(prismLabel(value), undefined, JSON.stringify(value));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("prismRouteFromHeaders keeps whichever routing headers are usable", () => {
|
|
33
|
+
assert.deepEqual(
|
|
34
|
+
prismRouteFromHeaders({ "x-prism-model-name": " GLM 5.3 Flash ", "x-prism-model-id": "glm-5.3-flash" }),
|
|
35
|
+
{ modelName: "GLM 5.3 Flash", modelId: "glm-5.3-flash" },
|
|
36
|
+
);
|
|
37
|
+
assert.deepEqual(prismRouteFromHeaders({ "x-prism-model-name": "GLM 5.3 Flash" }), {
|
|
38
|
+
modelName: "GLM 5.3 Flash",
|
|
39
|
+
modelId: undefined,
|
|
40
|
+
});
|
|
41
|
+
assert.deepEqual(prismRouteFromHeaders({ "x-prism-model-id": "glm-5.3-flash" }), {
|
|
42
|
+
modelName: undefined,
|
|
43
|
+
modelId: "glm-5.3-flash",
|
|
44
|
+
});
|
|
45
|
+
assert.equal(prismRouteFromHeaders({}), undefined);
|
|
46
|
+
assert.equal(prismRouteFromHeaders({ "x-prism-model-name": " " }), undefined);
|
|
47
|
+
assert.equal(prismRouteFromHeaders({ "x-prism-model-name": "bad\u001b[31m" }), undefined);
|
|
48
|
+
assert.equal(prismRouteFromHeaders({ "x-other-header": "glm-5.3-flash" }), undefined);
|
|
49
|
+
assert.equal(prismRouteFromHeaders(undefined), undefined);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("persisted entry data is re-validated before rendering", () => {
|
|
53
|
+
assert.deepEqual(readPrismRoute({ modelName: "GLM 5.3 Flash" }), { modelName: "GLM 5.3 Flash", modelId: undefined });
|
|
54
|
+
assert.deepEqual(readPrismRoute({ modelId: "glm-5.3-flash" }), { modelName: undefined, modelId: "glm-5.3-flash" });
|
|
55
|
+
const rejected = [null, undefined, 42, "glm-5.3", [], { modelName: 42 }, { modelName: "bad\u001b[31m" }, { modelId: "bad\nline" }, {}];
|
|
56
|
+
for (const data of rejected) {
|
|
57
|
+
assert.equal(readPrismRoute(data), undefined, JSON.stringify(data));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("the route label prefers the human name and the entry type stays namespaced", () => {
|
|
62
|
+
assert.equal(prismRouteLabel({ modelName: "GLM 5.3 Flash", modelId: "glm-5.3-flash" }), "GLM 5.3 Flash");
|
|
63
|
+
assert.equal(prismRouteLabel({ modelId: "glm-5.3-flash" }), "glm-5.3-flash");
|
|
64
|
+
assert.equal(prismRouteLabel({}), undefined);
|
|
65
|
+
assert.ok(PRISM_ENTRY_TYPE.startsWith("hypercharm"));
|
|
66
|
+
assert.notEqual(PRISM_ENTRY_TYPE, "hyper-prism-route");
|
|
67
|
+
});
|