pi-custom-provider-model 0.1.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/src/types.ts ADDED
@@ -0,0 +1,112 @@
1
+ export const APIS = ["openai-completions", "openai-responses", "anthropic-messages"] as const;
2
+ export type Protocol = (typeof APIS)[number];
3
+ export type JsonObject = Record<string, any>;
4
+ export interface CompatibilityInput { supportsDeveloperRole?: boolean | null }
5
+
6
+ export interface ModelInput {
7
+ id: string;
8
+ name?: string;
9
+ contextWindow?: number;
10
+ maxTokens?: number;
11
+ reasoning?: boolean;
12
+ input?: ("text" | "image")[];
13
+ compat?: CompatibilityInput;
14
+ }
15
+
16
+ // Omitted edits preserve existing overrides; null explicitly restores inheritance.
17
+ export function mergeCompat(existing: JsonObject | undefined, edits: CompatibilityInput | undefined): JsonObject {
18
+ const compat = { ...existing };
19
+ if (edits?.supportsDeveloperRole === null) delete compat.supportsDeveloperRole;
20
+ else if (edits?.supportsDeveloperRole !== undefined) compat.supportsDeveloperRole = edits.supportsDeveloperRole;
21
+ return compat;
22
+ }
23
+
24
+ export interface ProviderDraft {
25
+ id: string;
26
+ baseUrl: string;
27
+ api: Protocol;
28
+ apiKey?: string;
29
+ authHeader?: boolean;
30
+ compat?: CompatibilityInput;
31
+ models: ModelInput[];
32
+ revision?: string;
33
+ }
34
+
35
+ export class AppError extends Error {
36
+ constructor(message: string, public status = 400) { super(message); }
37
+ }
38
+
39
+ export function object(value: unknown): value is JsonObject {
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ }
42
+
43
+ function validateCompat(value: unknown): CompatibilityInput | undefined {
44
+ if (value === undefined) return undefined;
45
+ if (!object(value)) throw new AppError("Invalid compatibility settings.");
46
+ const role = value.supportsDeveloperRole;
47
+ if (role === undefined) return undefined;
48
+ if (role !== null && typeof role !== "boolean") throw new AppError("Invalid developer-role compatibility setting.");
49
+ return { supportsDeveloperRole: role };
50
+ }
51
+
52
+ export function validateId(value: unknown): string {
53
+ if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(value) ||
54
+ ["constructor", "prototype", "__proto__"].includes(value)) {
55
+ throw new AppError("Provider ID must use 1–80 letters, numbers, dots, dashes or underscores.");
56
+ }
57
+ return value;
58
+ }
59
+
60
+ export function validateDraft(value: unknown): ProviderDraft {
61
+ if (!object(value)) throw new AppError("Expected a provider object.");
62
+ const id = validateId(value.id);
63
+ if (!APIS.includes(value.api)) throw new AppError("Select a supported API protocol.");
64
+ if (typeof value.baseUrl !== "string") throw new AppError("Base URL is required.");
65
+ let url: URL;
66
+ try { url = new URL(value.baseUrl.trim()); } catch { throw new AppError("Enter a valid HTTP(S) base URL."); }
67
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
68
+ throw new AppError("Use an HTTP(S) base URL without credentials, query parameters or fragments.");
69
+ }
70
+ if (value.apiKey !== undefined && (typeof value.apiKey !== "string" || value.apiKey.length > 8192 || /[\r\n]/.test(value.apiKey))) {
71
+ throw new AppError("API key must be a single line.");
72
+ }
73
+ if (!Array.isArray(value.models) || value.models.length > 10000) throw new AppError("Invalid model list.");
74
+ const seen = new Set<string>();
75
+ const models: ModelInput[] = value.models.map((m: unknown) => {
76
+ if (!object(m) || typeof m.id !== "string" || !m.id.trim() || m.id.length > 300 || /[\x00-\x1f]/.test(m.id)) {
77
+ throw new AppError("Each model needs a valid model ID.");
78
+ }
79
+ const model: ModelInput = { id: m.id.trim() };
80
+ if (seen.has(model.id)) throw new AppError(`Duplicate model ID: ${model.id}`);
81
+ seen.add(model.id);
82
+ if (m.name !== undefined) {
83
+ if (typeof m.name !== "string" || !m.name.trim()) throw new AppError("Model name cannot be empty.");
84
+ model.name = m.name.trim();
85
+ }
86
+ for (const field of ["contextWindow", "maxTokens"] as const) {
87
+ if (m[field] !== undefined) {
88
+ if (!Number.isSafeInteger(m[field]) || m[field] <= 0) throw new AppError(`${field} must be a positive integer.`);
89
+ model[field] = m[field];
90
+ }
91
+ }
92
+ if (m.reasoning !== undefined) {
93
+ if (typeof m.reasoning !== "boolean") throw new AppError("Invalid reasoning flag.");
94
+ model.reasoning = m.reasoning;
95
+ }
96
+ if (m.input !== undefined) {
97
+ if (!Array.isArray(m.input) || !m.input.includes("text") || m.input.some((i: unknown) => i !== "text" && i !== "image")) {
98
+ throw new AppError("Input must include text and optionally image.");
99
+ }
100
+ model.input = [...new Set(m.input)] as ModelInput["input"];
101
+ }
102
+ const compat = validateCompat(m.compat);
103
+ if (compat) model.compat = compat;
104
+ return model;
105
+ });
106
+ return {
107
+ id, baseUrl: url.toString().replace(/\/$/, ""), api: value.api, models, compat: validateCompat(value.compat),
108
+ ...(value.apiKey ? { apiKey: value.apiKey.trim() } : {}),
109
+ ...(typeof value.authHeader === "boolean" ? { authHeader: value.authHeader } : {}),
110
+ ...(typeof value.revision === "string" ? { revision: value.revision } : {}),
111
+ };
112
+ }