@geminixiang/mikan 0.4.2 → 0.4.3
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/CHANGELOG.md +7 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +3 -0
- package/dist/agent.js.map +1 -1
- package/dist/log.d.ts +1 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +5 -0
- package/dist/log.js.map +1 -1
- package/dist/web/admin/portal.d.ts.map +1 -1
- package/dist/web/admin/portal.js +18 -6
- package/dist/web/admin/portal.js.map +1 -1
- package/dist/web/admin/provider-models.d.ts +10 -0
- package/dist/web/admin/provider-models.d.ts.map +1 -0
- package/dist/web/admin/provider-models.js +104 -0
- package/dist/web/admin/provider-models.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
type AdminModelStatus = "available" | "unverified";
|
|
4
|
+
interface AdminModelAccessStatus {
|
|
5
|
+
status: AdminModelStatus;
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveAdminModelAccessStatuses(registry: ModelRegistry, models: Model<Api>[]): Promise<Map<string, AdminModelAccessStatus>>;
|
|
8
|
+
export declare function modelKey(provider: string, id: string): string;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=provider-models.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-models.d.ts","sourceRoot":"","sources":["../../../src/web/admin/provider-models.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE,KAAK,gBAAgB,GAAG,WAAW,GAAG,YAAY,CAAC;AAEnD,UAAU,sBAAsB;IAC9B,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAYD,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,aAAa,EACvB,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GACnB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAgB9C;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE7D","sourcesContent":["import { createHash } from \"crypto\";\nimport type { Api, Model } from \"@earendil-works/pi-ai\";\nimport type { ModelRegistry } from \"@earendil-works/pi-coding-agent\";\n\ntype AdminModelStatus = \"available\" | \"unverified\";\n\ninterface AdminModelAccessStatus {\n status: AdminModelStatus;\n}\n\nconst CACHE_TTL_MS = 5 * 60 * 1000;\nconst PROBE_TIMEOUT_MS = 3000;\n\ninterface ProviderModelsCacheEntry {\n expiresAt: number;\n modelIds: Set<string>;\n}\n\nconst providerModelsCache = new Map<string, ProviderModelsCacheEntry>();\n\nexport async function resolveAdminModelAccessStatuses(\n registry: ModelRegistry,\n models: Model<Api>[],\n): Promise<Map<string, AdminModelAccessStatus>> {\n const result = new Map<string, AdminModelAccessStatus>();\n const byProvider = groupModelsByProvider(models);\n\n await Promise.all(\n [...byProvider.entries()].map(async ([provider, providerModels]) => {\n const providerModelIds = await fetchProviderModelIds(registry, provider);\n for (const model of providerModels) {\n result.set(modelKey(model.provider, model.id), {\n status: modelIsListed(providerModelIds, model.id) ? \"available\" : \"unverified\",\n });\n }\n }),\n );\n\n return result;\n}\n\nexport function modelKey(provider: string, id: string): string {\n return `${provider}/${id}`;\n}\n\nfunction groupModelsByProvider(models: Model<Api>[]): Map<string, Model<Api>[]> {\n const groups = new Map<string, Model<Api>[]>();\n for (const model of models) {\n const group = groups.get(model.provider) ?? [];\n group.push(model);\n groups.set(model.provider, group);\n }\n return groups;\n}\n\nasync function fetchProviderModelIds(\n registry: ModelRegistry,\n provider: string,\n): Promise<Set<string>> {\n if (provider !== \"openai\" && provider !== \"anthropic\") return new Set<string>();\n\n const apiKey = await registry.getApiKeyForProvider(provider);\n if (!apiKey) return new Set<string>();\n\n const cacheKey = `${provider}:${hashApiKey(apiKey)}`;\n const cached = providerModelsCache.get(cacheKey);\n if (cached && cached.expiresAt > Date.now()) return cached.modelIds;\n\n try {\n const modelIds =\n provider === \"openai\"\n ? await fetchOpenAiModelIds(apiKey)\n : await fetchAnthropicModelIds(apiKey);\n providerModelsCache.set(cacheKey, {\n expiresAt: Date.now() + CACHE_TTL_MS,\n modelIds,\n });\n return modelIds;\n } catch {\n return new Set<string>();\n }\n}\n\nasync function fetchOpenAiModelIds(apiKey: string): Promise<Set<string>> {\n const response = await fetchWithTimeout(\"https://api.openai.com/v1/models\", {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n if (!response.ok) throw new Error(`OpenAI models request failed: ${response.status}`);\n const body = (await response.json()) as { data?: Array<{ id?: unknown }> };\n return new Set((body.data ?? []).map((model) => model.id).filter(isString));\n}\n\nasync function fetchAnthropicModelIds(apiKey: string): Promise<Set<string>> {\n const response = await fetchWithTimeout(\"https://api.anthropic.com/v1/models\", {\n headers: {\n \"anthropic-version\": \"2023-06-01\",\n \"x-api-key\": apiKey,\n },\n });\n if (!response.ok) throw new Error(`Anthropic models request failed: ${response.status}`);\n const body = (await response.json()) as { data?: Array<{ id?: unknown }> };\n return new Set((body.data ?? []).map((model) => model.id).filter(isString));\n}\n\nasync function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } finally {\n clearTimeout(timeout);\n }\n}\n\nfunction modelIsListed(providerModelIds: Set<string>, registryModelId: string): boolean {\n if (providerModelIds.size === 0) return true;\n if (providerModelIds.has(registryModelId)) return true;\n\n for (const providerModelId of providerModelIds) {\n if (providerModelId.startsWith(`${registryModelId}-`)) return true;\n if (registryModelId.startsWith(`${providerModelId}-`)) return true;\n }\n return false;\n}\n\nfunction hashApiKey(apiKey: string): string {\n return createHash(\"sha256\").update(apiKey).digest(\"hex\").slice(0, 16);\n}\n\nfunction isString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n"]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
3
|
+
const PROBE_TIMEOUT_MS = 3000;
|
|
4
|
+
const providerModelsCache = new Map();
|
|
5
|
+
export async function resolveAdminModelAccessStatuses(registry, models) {
|
|
6
|
+
const result = new Map();
|
|
7
|
+
const byProvider = groupModelsByProvider(models);
|
|
8
|
+
await Promise.all([...byProvider.entries()].map(async ([provider, providerModels]) => {
|
|
9
|
+
const providerModelIds = await fetchProviderModelIds(registry, provider);
|
|
10
|
+
for (const model of providerModels) {
|
|
11
|
+
result.set(modelKey(model.provider, model.id), {
|
|
12
|
+
status: modelIsListed(providerModelIds, model.id) ? "available" : "unverified",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
export function modelKey(provider, id) {
|
|
19
|
+
return `${provider}/${id}`;
|
|
20
|
+
}
|
|
21
|
+
function groupModelsByProvider(models) {
|
|
22
|
+
const groups = new Map();
|
|
23
|
+
for (const model of models) {
|
|
24
|
+
const group = groups.get(model.provider) ?? [];
|
|
25
|
+
group.push(model);
|
|
26
|
+
groups.set(model.provider, group);
|
|
27
|
+
}
|
|
28
|
+
return groups;
|
|
29
|
+
}
|
|
30
|
+
async function fetchProviderModelIds(registry, provider) {
|
|
31
|
+
if (provider !== "openai" && provider !== "anthropic")
|
|
32
|
+
return new Set();
|
|
33
|
+
const apiKey = await registry.getApiKeyForProvider(provider);
|
|
34
|
+
if (!apiKey)
|
|
35
|
+
return new Set();
|
|
36
|
+
const cacheKey = `${provider}:${hashApiKey(apiKey)}`;
|
|
37
|
+
const cached = providerModelsCache.get(cacheKey);
|
|
38
|
+
if (cached && cached.expiresAt > Date.now())
|
|
39
|
+
return cached.modelIds;
|
|
40
|
+
try {
|
|
41
|
+
const modelIds = provider === "openai"
|
|
42
|
+
? await fetchOpenAiModelIds(apiKey)
|
|
43
|
+
: await fetchAnthropicModelIds(apiKey);
|
|
44
|
+
providerModelsCache.set(cacheKey, {
|
|
45
|
+
expiresAt: Date.now() + CACHE_TTL_MS,
|
|
46
|
+
modelIds,
|
|
47
|
+
});
|
|
48
|
+
return modelIds;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return new Set();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function fetchOpenAiModelIds(apiKey) {
|
|
55
|
+
const response = await fetchWithTimeout("https://api.openai.com/v1/models", {
|
|
56
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok)
|
|
59
|
+
throw new Error(`OpenAI models request failed: ${response.status}`);
|
|
60
|
+
const body = (await response.json());
|
|
61
|
+
return new Set((body.data ?? []).map((model) => model.id).filter(isString));
|
|
62
|
+
}
|
|
63
|
+
async function fetchAnthropicModelIds(apiKey) {
|
|
64
|
+
const response = await fetchWithTimeout("https://api.anthropic.com/v1/models", {
|
|
65
|
+
headers: {
|
|
66
|
+
"anthropic-version": "2023-06-01",
|
|
67
|
+
"x-api-key": apiKey,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok)
|
|
71
|
+
throw new Error(`Anthropic models request failed: ${response.status}`);
|
|
72
|
+
const body = (await response.json());
|
|
73
|
+
return new Set((body.data ?? []).map((model) => model.id).filter(isString));
|
|
74
|
+
}
|
|
75
|
+
async function fetchWithTimeout(url, init) {
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
78
|
+
try {
|
|
79
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function modelIsListed(providerModelIds, registryModelId) {
|
|
86
|
+
if (providerModelIds.size === 0)
|
|
87
|
+
return true;
|
|
88
|
+
if (providerModelIds.has(registryModelId))
|
|
89
|
+
return true;
|
|
90
|
+
for (const providerModelId of providerModelIds) {
|
|
91
|
+
if (providerModelId.startsWith(`${registryModelId}-`))
|
|
92
|
+
return true;
|
|
93
|
+
if (registryModelId.startsWith(`${providerModelId}-`))
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
function hashApiKey(apiKey) {
|
|
99
|
+
return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
100
|
+
}
|
|
101
|
+
function isString(value) {
|
|
102
|
+
return typeof value === "string" && value.length > 0;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=provider-models.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-models.js","sourceRoot":"","sources":["../../../src/web/admin/provider-models.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"crypto\";\nimport type { Api, Model } from \"@earendil-works/pi-ai\";\nimport type { ModelRegistry } from \"@earendil-works/pi-coding-agent\";\n\ntype AdminModelStatus = \"available\" | \"unverified\";\n\ninterface AdminModelAccessStatus {\n status: AdminModelStatus;\n}\n\nconst CACHE_TTL_MS = 5 * 60 * 1000;\nconst PROBE_TIMEOUT_MS = 3000;\n\ninterface ProviderModelsCacheEntry {\n expiresAt: number;\n modelIds: Set<string>;\n}\n\nconst providerModelsCache = new Map<string, ProviderModelsCacheEntry>();\n\nexport async function resolveAdminModelAccessStatuses(\n registry: ModelRegistry,\n models: Model<Api>[],\n): Promise<Map<string, AdminModelAccessStatus>> {\n const result = new Map<string, AdminModelAccessStatus>();\n const byProvider = groupModelsByProvider(models);\n\n await Promise.all(\n [...byProvider.entries()].map(async ([provider, providerModels]) => {\n const providerModelIds = await fetchProviderModelIds(registry, provider);\n for (const model of providerModels) {\n result.set(modelKey(model.provider, model.id), {\n status: modelIsListed(providerModelIds, model.id) ? \"available\" : \"unverified\",\n });\n }\n }),\n );\n\n return result;\n}\n\nexport function modelKey(provider: string, id: string): string {\n return `${provider}/${id}`;\n}\n\nfunction groupModelsByProvider(models: Model<Api>[]): Map<string, Model<Api>[]> {\n const groups = new Map<string, Model<Api>[]>();\n for (const model of models) {\n const group = groups.get(model.provider) ?? [];\n group.push(model);\n groups.set(model.provider, group);\n }\n return groups;\n}\n\nasync function fetchProviderModelIds(\n registry: ModelRegistry,\n provider: string,\n): Promise<Set<string>> {\n if (provider !== \"openai\" && provider !== \"anthropic\") return new Set<string>();\n\n const apiKey = await registry.getApiKeyForProvider(provider);\n if (!apiKey) return new Set<string>();\n\n const cacheKey = `${provider}:${hashApiKey(apiKey)}`;\n const cached = providerModelsCache.get(cacheKey);\n if (cached && cached.expiresAt > Date.now()) return cached.modelIds;\n\n try {\n const modelIds =\n provider === \"openai\"\n ? await fetchOpenAiModelIds(apiKey)\n : await fetchAnthropicModelIds(apiKey);\n providerModelsCache.set(cacheKey, {\n expiresAt: Date.now() + CACHE_TTL_MS,\n modelIds,\n });\n return modelIds;\n } catch {\n return new Set<string>();\n }\n}\n\nasync function fetchOpenAiModelIds(apiKey: string): Promise<Set<string>> {\n const response = await fetchWithTimeout(\"https://api.openai.com/v1/models\", {\n headers: { Authorization: `Bearer ${apiKey}` },\n });\n if (!response.ok) throw new Error(`OpenAI models request failed: ${response.status}`);\n const body = (await response.json()) as { data?: Array<{ id?: unknown }> };\n return new Set((body.data ?? []).map((model) => model.id).filter(isString));\n}\n\nasync function fetchAnthropicModelIds(apiKey: string): Promise<Set<string>> {\n const response = await fetchWithTimeout(\"https://api.anthropic.com/v1/models\", {\n headers: {\n \"anthropic-version\": \"2023-06-01\",\n \"x-api-key\": apiKey,\n },\n });\n if (!response.ok) throw new Error(`Anthropic models request failed: ${response.status}`);\n const body = (await response.json()) as { data?: Array<{ id?: unknown }> };\n return new Set((body.data ?? []).map((model) => model.id).filter(isString));\n}\n\nasync function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } finally {\n clearTimeout(timeout);\n }\n}\n\nfunction modelIsListed(providerModelIds: Set<string>, registryModelId: string): boolean {\n if (providerModelIds.size === 0) return true;\n if (providerModelIds.has(registryModelId)) return true;\n\n for (const providerModelId of providerModelIds) {\n if (providerModelId.startsWith(`${registryModelId}-`)) return true;\n if (registryModelId.startsWith(`${providerModelId}-`)) return true;\n }\n return false;\n}\n\nfunction hashApiKey(apiKey: string): string {\n return createHash(\"sha256\").update(apiKey).digest(\"hex\").slice(0, 16);\n}\n\nfunction isString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n"]}
|