pi-inferhub 0.2.0 → 0.3.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/LICENSE +21 -0
- package/README.md +43 -11
- package/package.json +22 -4
- package/src/catalog.ts +98 -20
- package/src/config.ts +75 -0
- package/src/index.ts +11 -4
- package/AGENTS.md +0 -146
- package/docs/superpowers/plans/2026-10-06-inferhub-provider.md +0 -438
- package/docs/superpowers/specs/2026-10-06-inferhub-provider-design.md +0 -207
- package/pi-inferhub-0.2.0.tgz +0 -0
- package/test/auth.test.ts +0 -43
- package/test/catalog.test.ts +0 -123
- package/test/http.test.ts +0 -43
- package/test/index.test.ts +0 -159
- package/tsconfig.json +0 -14
package/test/auth.test.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { loginInferhub } from "../src/auth.js";
|
|
3
|
-
|
|
4
|
-
function interaction(responses: string[], notified: string[] = []) {
|
|
5
|
-
let i = 0;
|
|
6
|
-
return {
|
|
7
|
-
signal: new AbortController().signal,
|
|
8
|
-
prompt: vi.fn(async () => {
|
|
9
|
-
const v = responses[i++];
|
|
10
|
-
if (v === undefined) throw new Error("prompt exhausted");
|
|
11
|
-
return v;
|
|
12
|
-
}),
|
|
13
|
-
notify: (e: { type: string; message?: string }) => notified.push(`${e.type}: ${e.message}`),
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe("loginInferhub", () => {
|
|
18
|
-
it("validates the key against /me and returns an api_key credential", async () => {
|
|
19
|
-
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) =>
|
|
20
|
-
new Response(JSON.stringify({}), { status: 200, headers: { "content-type": "application/json" } }));
|
|
21
|
-
const result = await loginInferhub(interaction(["sk-airo-abc"]), { fetchImpl: fetchMock as typeof fetch });
|
|
22
|
-
expect(result).toEqual({ type: "api_key", key: "sk-airo-abc" });
|
|
23
|
-
expect((fetchMock.mock.calls[0][1]?.headers as Record<string, string>)["Authorization"]).toBe("Bearer sk-airo-abc");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("re-prompts after a 401 and notifies, up to 3 attempts", async () => {
|
|
27
|
-
// Ruled fix (controller 2026-10-06): the brief's mock was always-401, contradicting
|
|
28
|
-
// this test's intent and test 3. Mock returns 401 twice, then 200 on call 3.
|
|
29
|
-
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ error: { code: "invalid_api_key", message: "bad key" } }), { status: 401 }))
|
|
30
|
-
.mockImplementationOnce(async () => new Response(JSON.stringify({ error: { code: "invalid_api_key", message: "bad key" } }), { status: 401 }))
|
|
31
|
-
.mockImplementationOnce(async () => new Response(JSON.stringify({ error: { code: "invalid_api_key", message: "bad key" } }), { status: 401 }))
|
|
32
|
-
.mockImplementationOnce(async () => new Response(JSON.stringify({}), { status: 200 }));
|
|
33
|
-
const notified: string[] = [];
|
|
34
|
-
const result = await loginInferhub(interaction(["sk-bad", "sk-bad", "sk-airo-good"], notified), { fetchImpl: fetchMock as typeof fetch });
|
|
35
|
-
expect(result.key).toBe("sk-airo-good");
|
|
36
|
-
expect(notified.some((n) => n.startsWith("info:"))).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("throws after 3 failed attempts", async () => {
|
|
40
|
-
const fetchMock = vi.fn(async () => new Response("{}", { status: 401 }));
|
|
41
|
-
await expect(loginInferhub(interaction(["a", "b", "c"]), { fetchImpl: fetchMock as typeof fetch })).rejects.toThrow(/three/);
|
|
42
|
-
});
|
|
43
|
-
});
|
package/test/catalog.test.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type { CatalogModel, Combo } from "../src/catalog.js";
|
|
3
|
-
import { combosToModels } from "../src/catalog.js";
|
|
4
|
-
import { INFERENCE_BASE_URL, PROVIDER_NAME } from "../src/inferhub.js";
|
|
5
|
-
|
|
6
|
-
const base = {
|
|
7
|
-
input_token_limit: 200_000,
|
|
8
|
-
max_output_tokens: 32_000,
|
|
9
|
-
modality: "text,image",
|
|
10
|
-
reasoning_levels: ["low", "high"],
|
|
11
|
-
pricing: { official_in: 3.0, official_out: 15.0 },
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const catalog: CatalogModel[] = [
|
|
15
|
-
{ id: "croc/glm-5.3", ...base },
|
|
16
|
-
{ id: "ag/claude-opus", ...base, modality: "text", input_token_limit: 100_000, max_output_tokens: 16_000, pricing: { official_in: 1.0, official_out: 5.0 } },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
const combo = (members: Combo["members"], overrides: Partial<Combo> = {}): Combo => ({
|
|
20
|
-
id: "c1", name: "My Combo", slug: "my-combo", members, ...overrides,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
/** Bare alias row shape observed live 2026-10-15: id without prefix, owned_by "alias",
|
|
24
|
-
* no limits/pricing/reasoning fields at all. */
|
|
25
|
-
const aliasRow = (name: string): CatalogModel => ({ id: name, owned_by: "alias" });
|
|
26
|
-
|
|
27
|
-
describe("combosToModels", () => {
|
|
28
|
-
it("maps id, name, provider, baseUrl, api", () => {
|
|
29
|
-
const [m] = combosToModels([combo([{ kind: "model", model: "croc/glm-5.3" }])], catalog);
|
|
30
|
-
expect(m.id).toBe("combo/my-combo");
|
|
31
|
-
expect(m.name).toBe("My Combo");
|
|
32
|
-
expect(m.provider).toBe(PROVIDER_NAME);
|
|
33
|
-
expect(m.baseUrl).toBe(INFERENCE_BASE_URL);
|
|
34
|
-
expect(m.api).toBe("openai-completions");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("derives min limits, all-members image rule, and cheapest-member cost", () => {
|
|
38
|
-
const [m] = combosToModels([combo([{ kind: "model", model: "croc/glm-5.3" }, { kind: "model", model: "ag/claude-opus" }])], catalog);
|
|
39
|
-
expect(m.contextWindow).toBe(100_000);
|
|
40
|
-
expect(m.maxTokens).toBe(16_000);
|
|
41
|
-
expect(m.input).toEqual(["text"]); // one text-only member demotes the combo
|
|
42
|
-
expect(m.reasoning).toBe(true);
|
|
43
|
-
expect(m.cost).toEqual({ input: 1.0, output: 5.0, cacheRead: 0, cacheWrite: 0 });
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("falls back to slug when name is blank; empty name whitespace counts", () => {
|
|
47
|
-
const [m] = combosToModels([combo([{ kind: "model", model: "croc/glm-5.3" }], { name: " " })], catalog);
|
|
48
|
-
expect(m.name).toBe("my-combo");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("skips unresolvable members and alias members; alias-only combo gets safe defaults", () => {
|
|
52
|
-
const aliasOnly = combosToModels([combo([{ kind: "alias", alias: "glm-5" }])], catalog);
|
|
53
|
-
expect(aliasOnly[0].input).toEqual(["text"]);
|
|
54
|
-
expect(aliasOnly[0].reasoning).toBe(true);
|
|
55
|
-
expect(aliasOnly[0].cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
56
|
-
expect(aliasOnly[0].contextWindow).toBe(262_144);
|
|
57
|
-
expect(aliasOnly[0].maxTokens).toBe(131_072);
|
|
58
|
-
|
|
59
|
-
const partial = combosToModels([combo([{ kind: "model", model: "ghost/gone" }, { kind: "model", model: "croc/glm-5.3" }])], catalog);
|
|
60
|
-
expect(partial[0].contextWindow).toBe(200_000);
|
|
61
|
-
expect(partial[0].input).toEqual(["text", "image"]);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("preserves combo order and handles empty combo list", () => {
|
|
65
|
-
const models = combosToModels([
|
|
66
|
-
combo([{ kind: "model", model: "ag/claude-opus" }], { slug: "b-combo", name: "B" }),
|
|
67
|
-
combo([{ kind: "model", model: "croc/glm-5.3" }], { slug: "a-combo", name: "A" }),
|
|
68
|
-
], catalog);
|
|
69
|
-
expect(models.map((m) => m.id)).toEqual(["combo/b-combo", "combo/a-combo"]);
|
|
70
|
-
expect(combosToModels([], catalog)).toEqual([]);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("raises bare alias rows and alias-kind members via prefixed sibling join (live combo/fast shape)", () => {
|
|
74
|
-
// Members named like a user-built combo: bare names whose catalog rows are
|
|
75
|
-
// information-free alias rows, but prefixed servings of the same model exist.
|
|
76
|
-
const sibCatalog: CatalogModel[] = [
|
|
77
|
-
aliasRow("qwen3.8-flash"),
|
|
78
|
-
aliasRow("deepseek-v4.1-flash"),
|
|
79
|
-
aliasRow("glm-5.3-flash"),
|
|
80
|
-
{ id: "ocg/qwen3.8-flash", ...base },
|
|
81
|
-
{ id: "ali/qwen3.8-flash", ...base },
|
|
82
|
-
{ id: "ocg/deepseek-v4.1-flash", ...base, pricing: { official_in: 2.0, official_out: 10.0 } },
|
|
83
|
-
{ id: "zai/glm-5.3-flash", ...base },
|
|
84
|
-
];
|
|
85
|
-
const [m] = combosToModels([combo([
|
|
86
|
-
{ kind: "model", model: "qwen3.8-flash" },
|
|
87
|
-
{ kind: "alias", alias: "deepseek-v4.1-flash" },
|
|
88
|
-
{ kind: "model", model: "glm-5.3-flash" },
|
|
89
|
-
])], sibCatalog);
|
|
90
|
-
expect(m.contextWindow).toBe(200_000); // min over sibling servings, not the 128k fallback
|
|
91
|
-
expect(m.maxTokens).toBe(32_000);
|
|
92
|
-
expect(m.reasoning).toBe(true);
|
|
93
|
-
expect(m.thinkingLevelMap).toMatchObject({ low: "low", high: "high" });
|
|
94
|
-
expect(m.input).toEqual(["text", "image"]); // every sibling serving is vision-capable
|
|
95
|
-
expect(m.cost).toEqual({ input: 2.0, output: 10.0, cacheRead: 0, cacheWrite: 0 });
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("does not cross-join a siblingless alias and keeps safe defaults (claude fable shape)", () => {
|
|
99
|
-
// cc/ row exists but is null everywhere and has NO prefixed siblings carrying data.
|
|
100
|
-
const [m] = combosToModels([combo([{ kind: "model", model: "claude-fable-5-1" }])], [aliasRow("claude-fable-5-1")]);
|
|
101
|
-
expect(m.contextWindow).toBe(262_144);
|
|
102
|
-
expect(m.maxTokens).toBe(131_072);
|
|
103
|
-
expect(m.reasoning).toBe(true); // alias-only optimism invariant
|
|
104
|
-
expect(m.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("prefers a direct row's real data over sibling join", () => {
|
|
108
|
-
// A prefixed member resolves directly; siblings must not inflate its set.
|
|
109
|
-
const sibCatalog: CatalogModel[] = [
|
|
110
|
-
{ id: "croc/glm-5.3", ...base },
|
|
111
|
-
{ id: "other/glm-5.3", ...base, input_token_limit: 1_000_000, max_output_tokens: 128_000 },
|
|
112
|
-
];
|
|
113
|
-
const [m] = combosToModels([combo([{ kind: "model", model: "croc/glm-5.3" }])], sibCatalog);
|
|
114
|
-
expect(m.contextWindow).toBe(200_000); // only the directly-joined member counts
|
|
115
|
-
expect(m.maxTokens).toBe(32_000);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("reasoning false when no member advertises levels", () => {
|
|
119
|
-
const noLevels: CatalogModel = { id: "x/plain", modality: "text" };
|
|
120
|
-
const [m] = combosToModels([combo([{ kind: "model", model: "x/plain" }])], [noLevels]);
|
|
121
|
-
expect(m.reasoning).toBe(false);
|
|
122
|
-
});
|
|
123
|
-
});
|
package/test/http.test.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
import { Compile } from "typebox/compile";
|
|
4
|
-
import { fetchJson, InferhubHttpError } from "../src/http.js";
|
|
5
|
-
|
|
6
|
-
const Payload = Type.Object({ object: Type.String() });
|
|
7
|
-
const Validator = Compile(Payload);
|
|
8
|
-
|
|
9
|
-
function respond(status: number, body: string, headers: Record<string, string> = {}): Response {
|
|
10
|
-
const h = new Headers({ "content-type": "application/json", ...headers });
|
|
11
|
-
return new Response(body, { status, headers: h });
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe("fetchJson", () => {
|
|
15
|
-
it("sends bearer auth and user agent, parses and validates JSON", async () => {
|
|
16
|
-
const fetchMock = vi.fn(async () => respond(200, JSON.stringify({ object: "list" })));
|
|
17
|
-
const result = await fetchJson("https://x/y", "sk-test", Validator, undefined, { fetchImpl: fetchMock as typeof fetch });
|
|
18
|
-
expect(result).toEqual({ object: "list" });
|
|
19
|
-
const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
|
|
20
|
-
expect(url).toBe("https://x/y");
|
|
21
|
-
expect((init.headers as Record<string, string>)["Authorization"]).toBe("Bearer sk-test");
|
|
22
|
-
expect((init.headers as Record<string, string>)["User-Agent"]).toMatch(/^pi-inferhub\//);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("throws InferhubHttpError with parsed code/message on error JSON", async () => {
|
|
26
|
-
const fetchMock = vi.fn(async () => respond(403, JSON.stringify({ error: { code: "account_blocked", message: "banned: reason" } })));
|
|
27
|
-
await expect(fetchJson("https://x/y", "k", Validator, undefined, { fetchImpl: fetchMock as typeof fetch }))
|
|
28
|
-
.rejects.toMatchObject({ status: 403, code: "account_blocked", message: "banned: reason" });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("aborts via signal and surfaces non-JSON error bodies as raw text", async () => {
|
|
32
|
-
const controller = new AbortController();
|
|
33
|
-
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
|
|
34
|
-
controller.abort();
|
|
35
|
-
throw new DOMException("aborted", "AbortError");
|
|
36
|
-
});
|
|
37
|
-
await expect(fetchJson("https://x/y", "k", Validator, controller.signal, { fetchImpl: fetchMock as typeof fetch }))
|
|
38
|
-
.rejects.toMatchObject({ name: "AbortError" });
|
|
39
|
-
const fetchMock2 = vi.fn(async () => respond(500, "<html>oops</html>"));
|
|
40
|
-
await expect(fetchJson("https://x/y", "k", Validator, undefined, { fetchImpl: fetchMock2 as typeof fetch }))
|
|
41
|
-
.rejects.toMatchObject({ status: 500, code: "http_500" });
|
|
42
|
-
});
|
|
43
|
-
});
|
package/test/index.test.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import type { ModelsPublication, Provider, RefreshModelsContext } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { inferhubApiKeyAuth, loginInferhub } from "../src/auth.js";
|
|
5
|
-
import { registerInferhubProvider, type InferhubRegistrationPi } from "../src/index.js";
|
|
6
|
-
import { INFERENCE_BASE_URL } from "../src/inferhub.js";
|
|
7
|
-
|
|
8
|
-
function registrationHarness() {
|
|
9
|
-
const providers = new Map<string, Provider>();
|
|
10
|
-
const commands = new Map<string, { description?: string; handler: (args: string, ctx: ExtensionCommandContext) => Promise<void> }>();
|
|
11
|
-
const pi: InferhubRegistrationPi = {
|
|
12
|
-
registerProvider: (provider) => void providers.set(provider.id, provider),
|
|
13
|
-
registerCommand: (name, options) => void commands.set(name, options),
|
|
14
|
-
};
|
|
15
|
-
return { pi, providers, commands };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function refreshContext(overrides: Partial<RefreshModelsContext> = {}) {
|
|
19
|
-
const publications: ModelsPublication[] = [];
|
|
20
|
-
const context: RefreshModelsContext = {
|
|
21
|
-
credential: { type: "api_key", key: "sk-test" },
|
|
22
|
-
publish: async (publication) => {
|
|
23
|
-
publications.push(publication);
|
|
24
|
-
publication.update?.();
|
|
25
|
-
return true;
|
|
26
|
-
},
|
|
27
|
-
allowNetwork: true,
|
|
28
|
-
signal: new AbortController().signal,
|
|
29
|
-
...overrides,
|
|
30
|
-
};
|
|
31
|
-
return { context, publications };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Stub GET /combos + GET /v1/models, recording Authorization headers per call. */
|
|
35
|
-
function stubCatalogFetch() {
|
|
36
|
-
const authHeaders: string[] = [];
|
|
37
|
-
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
|
|
38
|
-
const headers = (init?.headers ?? {}) as Record<string, string>;
|
|
39
|
-
authHeaders.push(headers["Authorization"] ?? "");
|
|
40
|
-
const body = String(url).includes("/combos")
|
|
41
|
-
? [{ id: "c1", name: "Test Combo", slug: "test-combo", members: [{ kind: "model", model: "croc/glm-5.3" }] }]
|
|
42
|
-
: {
|
|
43
|
-
data: [
|
|
44
|
-
{
|
|
45
|
-
id: "croc/glm-5.3",
|
|
46
|
-
modality: "text,image",
|
|
47
|
-
input_token_limit: 200_000,
|
|
48
|
-
max_output_tokens: 8_192,
|
|
49
|
-
reasoning_levels: ["low"],
|
|
50
|
-
pricing: { official_in: 3, official_out: 15 },
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
};
|
|
54
|
-
return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } });
|
|
55
|
-
});
|
|
56
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
57
|
-
return { fetchMock, authHeaders };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
afterEach(() => {
|
|
61
|
-
vi.unstubAllGlobals();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe("registerInferhubProvider", () => {
|
|
65
|
-
it("registers the inferhub provider with empty baseline, login+env auth, and dynamic model fetch", () => {
|
|
66
|
-
const { pi, providers } = registrationHarness();
|
|
67
|
-
registerInferhubProvider(pi);
|
|
68
|
-
expect(providers.has("inferhub")).toBe(true);
|
|
69
|
-
const provider = providers.get("inferhub")!;
|
|
70
|
-
expect(provider.name).toBe("InferHub");
|
|
71
|
-
expect(provider.baseUrl).toBe(INFERENCE_BASE_URL);
|
|
72
|
-
expect(provider.getModels()).toEqual([]); // baseline; combos arrive via refreshModels
|
|
73
|
-
expect(provider.refreshModels).toBeTypeOf("function");
|
|
74
|
-
expect(typeof provider.streamSimple).toBe("function");
|
|
75
|
-
// envApiKeyAuth resolution + our prompt-validate login overriding the plain default
|
|
76
|
-
expect(provider.auth.apiKey?.login).toBe(loginInferhub);
|
|
77
|
-
expect(provider.auth.apiKey?.resolve).toBe(inferhubApiKeyAuth.resolve);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("the default export registers through the same surface", async () => {
|
|
81
|
-
const { pi, providers, commands } = registrationHarness();
|
|
82
|
-
const mod = await import("../src/index.js");
|
|
83
|
-
mod.default(pi as unknown as ExtensionAPI);
|
|
84
|
-
expect(providers.has("inferhub")).toBe(true);
|
|
85
|
-
expect(commands.has("inferhub-login")).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("refreshModels maps the combo catalog using the credential key", async () => {
|
|
89
|
-
const { pi, providers } = registrationHarness();
|
|
90
|
-
registerInferhubProvider(pi);
|
|
91
|
-
const { authHeaders } = stubCatalogFetch();
|
|
92
|
-
const { context, publications } = refreshContext();
|
|
93
|
-
await providers.get("inferhub")!.refreshModels!(context);
|
|
94
|
-
expect(providers.get("inferhub")!.getModels().map((m) => m.id)).toEqual(["combo/test-combo"]);
|
|
95
|
-
const persisted = publications.at(-1)?.persist?.models ?? [];
|
|
96
|
-
expect(persisted.map((m) => m.id)).toEqual(["combo/test-combo"]);
|
|
97
|
-
expect(persisted[0]).toMatchObject({
|
|
98
|
-
name: "Test Combo",
|
|
99
|
-
api: "openai-completions",
|
|
100
|
-
provider: "inferhub",
|
|
101
|
-
baseUrl: INFERENCE_BASE_URL,
|
|
102
|
-
input: ["text", "image"],
|
|
103
|
-
contextWindow: 200_000,
|
|
104
|
-
});
|
|
105
|
-
expect(authHeaders).toEqual(["Bearer sk-test", "Bearer sk-test"]); // /combos + /v1/models
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("refreshModels keeps the empty baseline without a credential (no network)", async () => {
|
|
109
|
-
const { pi, providers } = registrationHarness();
|
|
110
|
-
registerInferhubProvider(pi);
|
|
111
|
-
const { fetchMock } = stubCatalogFetch();
|
|
112
|
-
const { context } = refreshContext({ credential: undefined });
|
|
113
|
-
await providers.get("inferhub")!.refreshModels!(context);
|
|
114
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
115
|
-
expect(providers.get("inferhub")!.getModels()).toEqual([]);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("auth resolves the env key when no stored credential exists", async () => {
|
|
119
|
-
const { pi, providers } = registrationHarness();
|
|
120
|
-
registerInferhubProvider(pi);
|
|
121
|
-
const result = await providers.get("inferhub")!.auth.apiKey!.resolve({
|
|
122
|
-
ctx: {
|
|
123
|
-
env: async (name) => (name === "INFERHUB_API_KEY" ? "sk-env" : undefined),
|
|
124
|
-
fileExists: async () => false,
|
|
125
|
-
},
|
|
126
|
-
credential: undefined,
|
|
127
|
-
signal: new AbortController().signal,
|
|
128
|
-
});
|
|
129
|
-
expect(result).toMatchObject({ auth: { apiKey: "sk-env" }, source: "INFERHUB_API_KEY" });
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("registers the inferhub-login command pointing at /login inferhub", async () => {
|
|
133
|
-
const { pi, commands } = registrationHarness();
|
|
134
|
-
registerInferhubProvider(pi);
|
|
135
|
-
expect(commands.has("inferhub-login")).toBe(true);
|
|
136
|
-
const command = commands.get("inferhub-login")!;
|
|
137
|
-
expect(command.description).toBeTruthy();
|
|
138
|
-
|
|
139
|
-
const notified: string[] = [];
|
|
140
|
-
const queried: string[] = [];
|
|
141
|
-
const ctxFor = (configured: boolean) =>
|
|
142
|
-
({
|
|
143
|
-
modelRegistry: {
|
|
144
|
-
getProviderAuthStatus: (provider: string) => {
|
|
145
|
-
queried.push(provider);
|
|
146
|
-
return { configured };
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
ui: { notify: (message: string) => void notified.push(message) },
|
|
150
|
-
}) as unknown as ExtensionCommandContext;
|
|
151
|
-
|
|
152
|
-
await command.handler("", ctxFor(false));
|
|
153
|
-
expect(queried).toEqual(["inferhub"]);
|
|
154
|
-
expect(notified[0]).toContain("/login inferhub");
|
|
155
|
-
|
|
156
|
-
await command.handler("", ctxFor(true));
|
|
157
|
-
expect(notified[1]).not.toContain("not configured");
|
|
158
|
-
});
|
|
159
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"module": "NodeNext",
|
|
4
|
-
"moduleResolution": "NodeNext",
|
|
5
|
-
"target": "ES2022",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"strict": true,
|
|
8
|
-
"noEmit": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"types": ["node"],
|
|
11
|
-
"forceConsistentCasingInFileNames": true
|
|
12
|
-
},
|
|
13
|
-
"include": ["src", "test"]
|
|
14
|
-
}
|