@aliou/pi-ts-aperture 0.5.0 → 0.5.1
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 +1 -1
- package/src/extension/runtime.test.ts +121 -0
- package/src/extension/runtime.ts +28 -6
- package/src/lib/gateway.ts +22 -3
package/package.json
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { configLoader } from "../lib/config";
|
|
3
|
+
import { fetchGatewayModels } from "../lib/gateway";
|
|
4
|
+
import type { Api, Model } from "../lib/types";
|
|
5
|
+
import { ApertureRuntime } from "./runtime";
|
|
6
|
+
|
|
7
|
+
vi.mock("../lib/config", () => ({
|
|
8
|
+
configLoader: {
|
|
9
|
+
getConfig: vi.fn(),
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("../lib/gateway", () => ({
|
|
14
|
+
fetchGatewayModels: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const getConfig = vi.mocked(configLoader.getConfig);
|
|
18
|
+
const fetchModels = vi.mocked(fetchGatewayModels);
|
|
19
|
+
|
|
20
|
+
function model(provider: string, id: string): Model<Api> {
|
|
21
|
+
return { provider, id } as Model<Api>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function check(models: Model<Api>[]) {
|
|
25
|
+
const notify = vi.fn();
|
|
26
|
+
const runtime = new ApertureRuntime();
|
|
27
|
+
|
|
28
|
+
await runtime.checkMissingModels(
|
|
29
|
+
{
|
|
30
|
+
getModels: () => models,
|
|
31
|
+
notify,
|
|
32
|
+
},
|
|
33
|
+
"http://gateway.test",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return notify;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("ApertureRuntime.checkMissingModels", () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
getConfig.mockReturnValue({
|
|
42
|
+
baseUrl: "http://gateway.test",
|
|
43
|
+
providers: [],
|
|
44
|
+
checkGatewayModels: ["synthetic"],
|
|
45
|
+
});
|
|
46
|
+
fetchModels.mockResolvedValue([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("matches gateway models by provider and id", async () => {
|
|
50
|
+
fetchModels.mockResolvedValue([{ providerId: "openrouter", id: "foo" }]);
|
|
51
|
+
|
|
52
|
+
const notify = await check([
|
|
53
|
+
model("synthetic", "foo"),
|
|
54
|
+
model("openrouter", "foo"),
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
expect(notify).toHaveBeenCalledOnce();
|
|
58
|
+
expect(notify.mock.calls[0][0]).toContain("synthetic: foo");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("only checks configured providers", async () => {
|
|
62
|
+
getConfig.mockReturnValue({
|
|
63
|
+
baseUrl: "http://gateway.test",
|
|
64
|
+
providers: [],
|
|
65
|
+
checkGatewayModels: ["synthetic"],
|
|
66
|
+
});
|
|
67
|
+
fetchModels.mockResolvedValue([{ providerId: "synthetic", id: "foo" }]);
|
|
68
|
+
|
|
69
|
+
const notify = await check([
|
|
70
|
+
model("synthetic", "foo"),
|
|
71
|
+
model("openrouter", "missing-openrouter"),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
expect(notify).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("truncates missing models per provider", async () => {
|
|
78
|
+
getConfig.mockReturnValue({
|
|
79
|
+
baseUrl: "http://gateway.test",
|
|
80
|
+
providers: [],
|
|
81
|
+
checkGatewayModels: ["openrouter", "synthetic"],
|
|
82
|
+
});
|
|
83
|
+
fetchModels.mockResolvedValue([{ providerId: "synthetic", id: "syn-1" }]);
|
|
84
|
+
|
|
85
|
+
const notify = await check([
|
|
86
|
+
model("openrouter", "or-1"),
|
|
87
|
+
model("openrouter", "or-2"),
|
|
88
|
+
model("openrouter", "or-3"),
|
|
89
|
+
model("openrouter", "or-4"),
|
|
90
|
+
model("openrouter", "or-5"),
|
|
91
|
+
model("openrouter", "or-6"),
|
|
92
|
+
model("openrouter", "or-7"),
|
|
93
|
+
model("synthetic", "syn-1"),
|
|
94
|
+
model("synthetic", "syn-2"),
|
|
95
|
+
model("synthetic", "syn-3"),
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
expect(notify).toHaveBeenCalledOnce();
|
|
99
|
+
const message = notify.mock.calls[0][0];
|
|
100
|
+
expect(message).toContain(
|
|
101
|
+
"openrouter: or-1, or-2, or-3, or-4, or-5, 2 more",
|
|
102
|
+
);
|
|
103
|
+
expect(message).not.toContain("or-6");
|
|
104
|
+
expect(message).not.toContain("or-7");
|
|
105
|
+
expect(message).toContain("synthetic: syn-2, syn-3");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("does not warn when all checked provider models exist", async () => {
|
|
109
|
+
fetchModels.mockResolvedValue([
|
|
110
|
+
{ providerId: "synthetic", id: "foo" },
|
|
111
|
+
{ providerId: "synthetic", id: "bar" },
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const notify = await check([
|
|
115
|
+
model("synthetic", "foo"),
|
|
116
|
+
model("synthetic", "bar"),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
expect(notify).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/extension/runtime.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { getApiProvider } from "@mariozechner/pi-ai";
|
|
8
8
|
import { configLoader } from "../lib/config";
|
|
9
|
-
import {
|
|
9
|
+
import { fetchGatewayModels } from "../lib/gateway";
|
|
10
10
|
import type {
|
|
11
11
|
Api,
|
|
12
12
|
AssistantMessageEventStream,
|
|
@@ -27,6 +27,8 @@ const APERTURE_PROVENANCE_HEADERS = {
|
|
|
27
27
|
"X-Title": "npm:@aliou/pi-ts-aperture",
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
const MAX_MISSING_MODELS_PER_PROVIDER = 5;
|
|
31
|
+
|
|
30
32
|
function resolveProviderHeaders(models: Model<Api>[]): Record<string, string> {
|
|
31
33
|
const modelHeaders = models.find((m) => m.headers)?.headers ?? {};
|
|
32
34
|
return {
|
|
@@ -87,23 +89,43 @@ export class ApertureRuntime {
|
|
|
87
89
|
const config = configLoader.getConfig();
|
|
88
90
|
if (config.checkGatewayModels.length === 0) return;
|
|
89
91
|
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
+
const gatewayModels = await fetchGatewayModels(gatewayUrl);
|
|
93
|
+
if (gatewayModels.length === 0) return;
|
|
92
94
|
|
|
93
95
|
const allModels = deps.getModels();
|
|
94
96
|
const checkedProviders = new Set(config.checkGatewayModels);
|
|
97
|
+
const gatewayModelKeys = new Set(
|
|
98
|
+
gatewayModels.map((m) => `${m.providerId}:${m.id}`),
|
|
99
|
+
);
|
|
95
100
|
|
|
96
101
|
const routedModels = allModels.filter((m) =>
|
|
97
102
|
checkedProviders.has(m.provider),
|
|
98
103
|
);
|
|
99
104
|
const missingModels = routedModels.filter(
|
|
100
|
-
(m) => !
|
|
105
|
+
(m) => !gatewayModelKeys.has(`${m.provider}:${m.id}`),
|
|
101
106
|
);
|
|
102
107
|
|
|
103
108
|
if (missingModels.length > 0) {
|
|
104
|
-
const
|
|
109
|
+
const missingByProvider = new Map<string, Model<Api>[]>();
|
|
110
|
+
for (const model of missingModels) {
|
|
111
|
+
const providerModels = missingByProvider.get(model.provider) ?? [];
|
|
112
|
+
providerModels.push(model);
|
|
113
|
+
missingByProvider.set(model.provider, providerModels);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const summary = Array.from(missingByProvider.entries())
|
|
117
|
+
.map(([provider, models]) => {
|
|
118
|
+
const shownModels = models
|
|
119
|
+
.slice(0, MAX_MISSING_MODELS_PER_PROVIDER)
|
|
120
|
+
.map((m) => m.id);
|
|
121
|
+
const remainingCount = models.length - shownModels.length;
|
|
122
|
+
const more = remainingCount > 0 ? `, ${remainingCount} more` : "";
|
|
123
|
+
return `${provider}: ${shownModels.join(", ")}${more}`;
|
|
124
|
+
})
|
|
125
|
+
.join("; ");
|
|
126
|
+
|
|
105
127
|
deps.notify(
|
|
106
|
-
`[aperture] models not available on gateway: ${
|
|
128
|
+
`[aperture] models not available on gateway: ${summary}. Add them to the gateway configuration.`,
|
|
107
129
|
"warning",
|
|
108
130
|
);
|
|
109
131
|
}
|
package/src/lib/gateway.ts
CHANGED
|
@@ -26,7 +26,14 @@ export async function checkApertureHealth(
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export
|
|
29
|
+
export interface GatewayModel {
|
|
30
|
+
id: string;
|
|
31
|
+
providerId: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function fetchGatewayModels(
|
|
35
|
+
baseUrl: string,
|
|
36
|
+
): Promise<GatewayModel[]> {
|
|
30
37
|
const url = `${baseUrl.replace(/\/+$/, "")}/v1/models`;
|
|
31
38
|
try {
|
|
32
39
|
const res = await fetch(url, {
|
|
@@ -34,8 +41,20 @@ export async function fetchGatewayModelIds(baseUrl: string): Promise<string[]> {
|
|
|
34
41
|
signal: AbortSignal.timeout(5000),
|
|
35
42
|
});
|
|
36
43
|
if (!res.ok) return [];
|
|
37
|
-
const body = (await res.json()) as {
|
|
38
|
-
|
|
44
|
+
const body = (await res.json()) as {
|
|
45
|
+
data?: {
|
|
46
|
+
id: string;
|
|
47
|
+
metadata?: { provider?: { id?: string } };
|
|
48
|
+
}[];
|
|
49
|
+
};
|
|
50
|
+
return (
|
|
51
|
+
body.data
|
|
52
|
+
?.map((m) => ({
|
|
53
|
+
id: m.id,
|
|
54
|
+
providerId: m.metadata?.provider?.id ?? "",
|
|
55
|
+
}))
|
|
56
|
+
.filter((m) => m.providerId.length > 0) ?? []
|
|
57
|
+
);
|
|
39
58
|
} catch {
|
|
40
59
|
return [];
|
|
41
60
|
}
|