@bytetrue/pi-vendor 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 +11 -0
- package/package.json +44 -0
- package/src/command.ts +595 -0
- package/src/custom-select.ts +231 -0
- package/src/enrich.ts +64 -0
- package/src/fuzzy.ts +139 -0
- package/src/index.ts +45 -0
- package/src/models-json.ts +123 -0
- package/src/official-catalog.ts +194 -0
- package/src/openai-models.ts +91 -0
- package/src/templates.ts +120 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
import type { ProviderModelConfig } from "./models-json.js";
|
|
6
|
+
|
|
7
|
+
export type OfficialModelConfig = Record<string, unknown> & {
|
|
8
|
+
id: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
api?: string;
|
|
11
|
+
provider?: string;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
authHeader?: boolean;
|
|
16
|
+
contextWindow?: number;
|
|
17
|
+
maxTokens?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type OfficialModelsCatalog = Record<string, Record<string, OfficialModelConfig>>;
|
|
21
|
+
|
|
22
|
+
export type OfficialModelCandidate = {
|
|
23
|
+
provider: string;
|
|
24
|
+
model: OfficialModelConfig;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const STRIPPED_FIELDS = ["provider", "baseUrl", "headers", "apiKey", "authHeader"] as const;
|
|
28
|
+
|
|
29
|
+
function cloneJson<T>(value: T): T {
|
|
30
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolvePackageRoot(startDir: string): string | null {
|
|
34
|
+
let current = startDir;
|
|
35
|
+
for (;;) {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = JSON.parse(readFileSync(join(current, "package.json"), "utf8")) as { name?: string };
|
|
38
|
+
if (pkg.name === "@earendil-works/pi-coding-agent") {
|
|
39
|
+
return current;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// keep walking
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parent = dirname(current);
|
|
46
|
+
if (parent === current) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
current = parent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveCandidateRoots(): string[] {
|
|
54
|
+
const roots = new Set<string>();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const resolvedUrl = import.meta.resolve("@earendil-works/pi-coding-agent");
|
|
58
|
+
if (resolvedUrl.startsWith("file://")) {
|
|
59
|
+
const root = resolvePackageRoot(dirname(fileURLToPath(resolvedUrl)));
|
|
60
|
+
if (root) roots.add(root);
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const localRoot = resolvePackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
67
|
+
if (localRoot) roots.add(localRoot);
|
|
68
|
+
|
|
69
|
+
return [...roots];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function candidateCatalogPaths(): string[] {
|
|
73
|
+
const paths = new Set<string>();
|
|
74
|
+
for (const root of resolveCandidateRoots()) {
|
|
75
|
+
paths.add(join(root, "node_modules", "@earendil-works", "pi-ai", "dist", "models.generated.js"));
|
|
76
|
+
|
|
77
|
+
let current = root;
|
|
78
|
+
for (;;) {
|
|
79
|
+
paths.add(
|
|
80
|
+
join(current, "node_modules", "@earendil-works", "pi-coding-agent", "node_modules", "@earendil-works", "pi-ai", "dist", "models.generated.js"),
|
|
81
|
+
);
|
|
82
|
+
const parent = dirname(current);
|
|
83
|
+
if (parent === current) break;
|
|
84
|
+
current = parent;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...paths];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let cachedCatalogPath: string | null = null;
|
|
91
|
+
let cachedCatalog: OfficialModelsCatalog | null = null;
|
|
92
|
+
|
|
93
|
+
export function findOfficialCatalogPath(): string | null {
|
|
94
|
+
for (const candidate of candidateCatalogPaths()) {
|
|
95
|
+
if (existsSync(candidate)) return candidate;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function loadOfficialCatalog(): Promise<OfficialModelsCatalog | null> {
|
|
101
|
+
const path = findOfficialCatalogPath();
|
|
102
|
+
if (!path) {
|
|
103
|
+
cachedCatalogPath = null;
|
|
104
|
+
cachedCatalog = null;
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (cachedCatalogPath === path) return cachedCatalog;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const mod = await import(pathToFileURL(path).href);
|
|
111
|
+
const catalog = mod.MODELS as OfficialModelsCatalog | undefined;
|
|
112
|
+
if (!catalog || typeof catalog !== "object") {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
cachedCatalogPath = path;
|
|
116
|
+
cachedCatalog = catalog;
|
|
117
|
+
return cachedCatalog;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function collectOfficialCandidates(catalog: OfficialModelsCatalog | null | undefined, modelId: string): OfficialModelCandidate[] {
|
|
124
|
+
if (!catalog) return [];
|
|
125
|
+
|
|
126
|
+
const matches: OfficialModelCandidate[] = [];
|
|
127
|
+
for (const [provider, providerModels] of Object.entries(catalog)) {
|
|
128
|
+
const model = providerModels?.[modelId];
|
|
129
|
+
if (model && typeof model === "object" && !Array.isArray(model) && typeof model.id === "string") {
|
|
130
|
+
matches.push({ provider, model: cloneJson(model) as OfficialModelConfig });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return matches;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type OfficialModelEntry = {
|
|
137
|
+
provider: string;
|
|
138
|
+
modelId: string;
|
|
139
|
+
model: OfficialModelConfig;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type OfficialModelGroup = {
|
|
143
|
+
modelId: string;
|
|
144
|
+
entries: OfficialModelEntry[];
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export function listAllOfficialModels(catalog: OfficialModelsCatalog | null | undefined): OfficialModelEntry[] {
|
|
148
|
+
if (!catalog) return [];
|
|
149
|
+
|
|
150
|
+
const entries: OfficialModelEntry[] = [];
|
|
151
|
+
for (const [provider, providerModels] of Object.entries(catalog)) {
|
|
152
|
+
for (const [modelId, model] of Object.entries(providerModels)) {
|
|
153
|
+
if (model && typeof model === "object" && !Array.isArray(model) && typeof model.id === "string") {
|
|
154
|
+
entries.push({ provider, modelId, model: cloneJson(model) as OfficialModelConfig });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return entries;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function groupOfficialModelsById(entries: OfficialModelEntry[]): OfficialModelGroup[] {
|
|
162
|
+
const groups: OfficialModelGroup[] = [];
|
|
163
|
+
const byId = new Map<string, OfficialModelGroup>();
|
|
164
|
+
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
let group = byId.get(entry.modelId);
|
|
167
|
+
if (!group) {
|
|
168
|
+
group = { modelId: entry.modelId, entries: [] };
|
|
169
|
+
byId.set(entry.modelId, group);
|
|
170
|
+
groups.push(group);
|
|
171
|
+
}
|
|
172
|
+
group.entries.push(entry);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return groups;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function stripOfficialRoutingFields(model: OfficialModelConfig): ProviderModelConfig {
|
|
179
|
+
const next = cloneJson(model) as ProviderModelConfig;
|
|
180
|
+
for (const field of STRIPPED_FIELDS) {
|
|
181
|
+
delete next[field];
|
|
182
|
+
}
|
|
183
|
+
return next;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function formatOfficialCandidate(candidate: OfficialModelCandidate): string {
|
|
187
|
+
const name = typeof candidate.model.name === "string" && candidate.model.name.trim() && candidate.model.name !== candidate.model.id ? candidate.model.name.trim() : undefined;
|
|
188
|
+
const api = typeof candidate.model.api === "string" && candidate.model.api.trim() ? candidate.model.api.trim() : undefined;
|
|
189
|
+
const contextWindow = typeof candidate.model.contextWindow === "number" ? `ctx ${candidate.model.contextWindow}` : undefined;
|
|
190
|
+
const maxTokens = typeof candidate.model.maxTokens === "number" ? `max ${candidate.model.maxTokens}` : undefined;
|
|
191
|
+
const meta = [api, contextWindow, maxTokens].filter(Boolean).join(", ");
|
|
192
|
+
const head = name ? `${candidate.provider}/${candidate.model.id} - ${name}` : `${candidate.provider}/${candidate.model.id}`;
|
|
193
|
+
return meta ? `${head} (${meta})` : head;
|
|
194
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type OpenAIModelsProviderDraft = {
|
|
2
|
+
baseUrl?: string;
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type FetchLike = (input: string, init?: { method?: string; headers?: Record<string, string> }) => Promise<{
|
|
7
|
+
ok: boolean;
|
|
8
|
+
status: number;
|
|
9
|
+
statusText: string;
|
|
10
|
+
json(): Promise<unknown>;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
export function buildOpenAIModelsUrl(baseUrl: string): string {
|
|
14
|
+
const url = new URL(baseUrl);
|
|
15
|
+
if (!url.pathname.endsWith("/")) {
|
|
16
|
+
url.pathname += "/";
|
|
17
|
+
}
|
|
18
|
+
return new URL("models", url).toString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveApiKeyValue(apiKey: string, env: NodeJS.ProcessEnv = process.env): { value: string; source: "literal" | "env" } {
|
|
22
|
+
const trimmed = apiKey.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
throw new Error("Missing API key");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const match = trimmed.match(/^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))$/);
|
|
28
|
+
if (!match) {
|
|
29
|
+
return { value: trimmed, source: "literal" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const name = match[1] ?? match[2];
|
|
33
|
+
if (!name) {
|
|
34
|
+
throw new Error("Missing API key");
|
|
35
|
+
}
|
|
36
|
+
const value = env[name];
|
|
37
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
38
|
+
throw new Error(`Environment variable ${name} is not set`);
|
|
39
|
+
}
|
|
40
|
+
return { value: value.trim(), source: "env" };
|
|
41
|
+
}
|
|
42
|
+
export function parseOpenAIModelsResponse(payload: unknown): string[] {
|
|
43
|
+
if (!payload || typeof payload !== "object") return [];
|
|
44
|
+
const data = (payload as { data?: unknown }).data;
|
|
45
|
+
if (!Array.isArray(data)) return [];
|
|
46
|
+
|
|
47
|
+
const ids = new Set<string>();
|
|
48
|
+
for (const entry of data) {
|
|
49
|
+
if (!entry || typeof entry !== "object") continue;
|
|
50
|
+
const id = (entry as { id?: unknown }).id;
|
|
51
|
+
if (typeof id === "string" && id.trim()) {
|
|
52
|
+
ids.add(id.trim());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [...ids].sort((a, b) => a.localeCompare(b));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function fetchOpenAIModelIds(
|
|
59
|
+
provider: OpenAIModelsProviderDraft,
|
|
60
|
+
fetchImpl: FetchLike = globalThis.fetch as unknown as FetchLike,
|
|
61
|
+
): Promise<string[]> {
|
|
62
|
+
if (!provider.baseUrl?.trim()) {
|
|
63
|
+
throw new Error("Missing provider base URL");
|
|
64
|
+
}
|
|
65
|
+
if (!provider.apiKey?.trim()) {
|
|
66
|
+
throw new Error("Missing provider API key");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { value: apiKey } = resolveApiKeyValue(provider.apiKey);
|
|
70
|
+
const endpoint = buildOpenAIModelsUrl(provider.baseUrl.trim());
|
|
71
|
+
const response = await fetchImpl(endpoint, {
|
|
72
|
+
method: "GET",
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${apiKey}`,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`Failed to fetch ${endpoint}: ${response.status} ${response.statusText}`.trim());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let payload: unknown;
|
|
83
|
+
try {
|
|
84
|
+
payload = await response.json();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
throw new Error(`Failed to parse ${endpoint} response: ${message}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return parseOpenAIModelsResponse(payload);
|
|
91
|
+
}
|
package/src/templates.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { ProviderModelConfig } from "./models-json.js";
|
|
2
|
+
|
|
3
|
+
export type ModelTemplate = {
|
|
4
|
+
id?: string;
|
|
5
|
+
prefix?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
reasoning?: boolean;
|
|
8
|
+
input?: Array<"text" | "image">;
|
|
9
|
+
contextWindow?: number;
|
|
10
|
+
maxTokens?: number;
|
|
11
|
+
cost?: Record<string, number>;
|
|
12
|
+
compat?: Record<string, unknown>;
|
|
13
|
+
thinkingLevelMap?: Record<string, string | null>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const MODEL_TEMPLATES: readonly ModelTemplate[] = [
|
|
17
|
+
{
|
|
18
|
+
id: "gpt-4o",
|
|
19
|
+
name: "GPT-4o",
|
|
20
|
+
reasoning: true,
|
|
21
|
+
input: ["text", "image"],
|
|
22
|
+
contextWindow: 128000,
|
|
23
|
+
maxTokens: 16384,
|
|
24
|
+
compat: {
|
|
25
|
+
supportsReasoningEffort: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
prefix: "gpt-4",
|
|
30
|
+
name: "GPT-4 family",
|
|
31
|
+
reasoning: true,
|
|
32
|
+
input: ["text", "image"],
|
|
33
|
+
contextWindow: 128000,
|
|
34
|
+
maxTokens: 16384,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
prefix: "claude-3.7",
|
|
38
|
+
name: "Claude 3.7 family",
|
|
39
|
+
reasoning: true,
|
|
40
|
+
input: ["text", "image"],
|
|
41
|
+
contextWindow: 200000,
|
|
42
|
+
maxTokens: 8192,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
prefix: "gemini-2.5",
|
|
46
|
+
name: "Gemini 2.5 family",
|
|
47
|
+
reasoning: true,
|
|
48
|
+
input: ["text", "image"],
|
|
49
|
+
contextWindow: 1000000,
|
|
50
|
+
maxTokens: 8192,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
prefix: "deepseek-v3",
|
|
54
|
+
name: "DeepSeek V3 family",
|
|
55
|
+
reasoning: true,
|
|
56
|
+
input: ["text"],
|
|
57
|
+
contextWindow: 128000,
|
|
58
|
+
maxTokens: 16384,
|
|
59
|
+
},
|
|
60
|
+
] as const;
|
|
61
|
+
|
|
62
|
+
function cloneJson<T>(value: T): T {
|
|
63
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function listModelTemplates(): ModelTemplate[] {
|
|
67
|
+
return MODEL_TEMPLATES.map((template) => cloneJson(template));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function templateLabel(template: ModelTemplate): string {
|
|
71
|
+
if (template.id) {
|
|
72
|
+
return template.name && template.name !== template.id ? `${template.id} - ${template.name}` : template.id;
|
|
73
|
+
}
|
|
74
|
+
if (template.prefix) {
|
|
75
|
+
return template.name ? `${template.prefix}* - ${template.name}` : `${template.prefix}*`;
|
|
76
|
+
}
|
|
77
|
+
return template.name ?? "template";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function matchTemplate(modelId: string, templates: readonly ModelTemplate[] = MODEL_TEMPLATES): ModelTemplate | undefined {
|
|
81
|
+
const exact = templates.find((template) => template.id === modelId);
|
|
82
|
+
if (exact) return exact;
|
|
83
|
+
|
|
84
|
+
let best: ModelTemplate | undefined;
|
|
85
|
+
let bestLength = -1;
|
|
86
|
+
for (const template of templates) {
|
|
87
|
+
if (!template.prefix) continue;
|
|
88
|
+
if (!modelId.startsWith(template.prefix)) continue;
|
|
89
|
+
if (template.prefix.length > bestLength) {
|
|
90
|
+
best = template;
|
|
91
|
+
bestLength = template.prefix.length;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return best;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createTemplateModelConfig(modelId: string, template: ModelTemplate): ProviderModelConfig {
|
|
98
|
+
return {
|
|
99
|
+
id: modelId,
|
|
100
|
+
name: template.name?.trim() || modelId,
|
|
101
|
+
reasoning: template.reasoning ?? false,
|
|
102
|
+
input: template.input ? [...template.input] : ["text"],
|
|
103
|
+
contextWindow: template.contextWindow ?? 128000,
|
|
104
|
+
maxTokens: template.maxTokens ?? 16384,
|
|
105
|
+
...(template.cost ? { cost: cloneJson(template.cost) } : {}),
|
|
106
|
+
...(template.compat ? { compat: cloneJson(template.compat) } : {}),
|
|
107
|
+
...(template.thinkingLevelMap ? { thinkingLevelMap: cloneJson(template.thinkingLevelMap) } : {}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createDefaultModelConfig(modelId: string): ProviderModelConfig {
|
|
112
|
+
return {
|
|
113
|
+
id: modelId,
|
|
114
|
+
name: modelId,
|
|
115
|
+
reasoning: false,
|
|
116
|
+
input: ["text"],
|
|
117
|
+
contextWindow: 128000,
|
|
118
|
+
maxTokens: 16384,
|
|
119
|
+
};
|
|
120
|
+
}
|