pi-cliproxyapi-native 0.1.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/LICENSE +21 -0
- package/README.md +246 -0
- package/extensions/index.ts +53 -0
- package/package.json +52 -0
- package/src/catalog.ts +248 -0
- package/src/config.ts +64 -0
- package/src/media-defaults.ts +79 -0
- package/src/media.ts +571 -0
- package/src/picker.ts +310 -0
- package/src/provider.ts +221 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const PROVIDER_ID = "cliproxyapi";
|
|
2
|
+
export const DEFAULT_BASE_URL = "http://localhost:8317";
|
|
3
|
+
|
|
4
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isModelId(value: unknown): value is string {
|
|
9
|
+
return typeof value === "string" && /^[!-~]{1,256}$/.test(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeBaseUrl(value: string) {
|
|
13
|
+
let url: URL;
|
|
14
|
+
try {
|
|
15
|
+
url = new URL(value);
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error("CLIProxyAPI baseUrl must be an absolute HTTP(S) URL.");
|
|
18
|
+
}
|
|
19
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
20
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
21
|
+
throw new Error("CLIProxyAPI requires HTTPS except on loopback addresses.");
|
|
22
|
+
}
|
|
23
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
24
|
+
throw new Error("CLIProxyAPI baseUrl must not contain credentials, a query, or a fragment.");
|
|
25
|
+
}
|
|
26
|
+
url.pathname = url.pathname.replace(/\/+$/, "").replace(/\/(?:v1|v1beta)$/, "");
|
|
27
|
+
return url.href.replace(/\/+$/, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseConfig(value: unknown, baseUrlOverride?: string) {
|
|
31
|
+
if (!isRecord(value) || Object.keys(value).some((key) => !["baseUrl", "aliases"].includes(key))) {
|
|
32
|
+
throw new Error("pi-cliproxyapi.json accepts only baseUrl and aliases.");
|
|
33
|
+
}
|
|
34
|
+
if (value.baseUrl !== undefined && typeof value.baseUrl !== "string") {
|
|
35
|
+
throw new Error("CLIProxyAPI baseUrl must be a string.");
|
|
36
|
+
}
|
|
37
|
+
const rawAliases = value.aliases === undefined ? {} : value.aliases;
|
|
38
|
+
if (!isRecord(rawAliases)) throw new Error("CLIProxyAPI aliases must be an object.");
|
|
39
|
+
const aliases = Object.fromEntries(
|
|
40
|
+
Object.entries(rawAliases).map(([id, reference]) => {
|
|
41
|
+
if (!isModelId(id) || typeof reference !== "string") {
|
|
42
|
+
throw new Error("CLIProxyAPI aliases must map model IDs to provider/model references.");
|
|
43
|
+
}
|
|
44
|
+
const separator = reference.indexOf("/");
|
|
45
|
+
const provider = reference.slice(0, separator);
|
|
46
|
+
if (
|
|
47
|
+
separator < 1 ||
|
|
48
|
+
!isModelId(reference.slice(separator + 1)) ||
|
|
49
|
+
!["anthropic", "openai", "openai-codex", "google"].includes(provider)
|
|
50
|
+
) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"CLIProxyAPI aliases require an anthropic, openai, openai-codex, or google reference.",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return [id, reference] as const;
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
return {
|
|
59
|
+
baseUrl: normalizeBaseUrl(baseUrlOverride ?? value.baseUrl ?? DEFAULT_BASE_URL),
|
|
60
|
+
aliases,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type Config = ReturnType<typeof parseConfig>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type MediaPurpose, mediaCapability, mediaPurpose, modelRoute } from "./catalog.ts";
|
|
3
|
+
import { type Config, isModelId, isRecord, PROVIDER_ID } from "./config.ts";
|
|
4
|
+
|
|
5
|
+
export const MEDIA_DEFAULTS_ENTRY = "cliproxyapi-media-defaults";
|
|
6
|
+
export const OPENAI_IMAGE_DEFAULT = "gpt-image-2.5-sunburst";
|
|
7
|
+
// null blocks fallback for invalid selections, including after unrelated picker saves.
|
|
8
|
+
export type MediaDefaults = Partial<Record<MediaPurpose, string | null>>;
|
|
9
|
+
export type DefaultsContext = {
|
|
10
|
+
sessionManager?: Pick<ExtensionContext["sessionManager"], "getBranch">;
|
|
11
|
+
model?: ExtensionContext["model"];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function readMediaDefaults(config: Config, ctx: DefaultsContext) {
|
|
15
|
+
let defaults: MediaDefaults = {};
|
|
16
|
+
for (const entry of ctx.sessionManager?.getBranch() ?? []) {
|
|
17
|
+
if (entry.type !== "custom" || entry.customType !== MEDIA_DEFAULTS_ENTRY) continue;
|
|
18
|
+
const data: unknown = entry.data;
|
|
19
|
+
if (!isRecord(data) || data.endpoint !== config.baseUrl) continue;
|
|
20
|
+
// An invalid latest snapshot must not resurrect an older or automatic default.
|
|
21
|
+
defaults = { image: null, video: null };
|
|
22
|
+
if (data.version !== 1 || !isRecord(data.defaults)) continue;
|
|
23
|
+
defaults = {};
|
|
24
|
+
for (const purpose of ["image", "video"] as const) {
|
|
25
|
+
if (!Object.hasOwn(data.defaults, purpose)) continue;
|
|
26
|
+
const id = data.defaults[purpose];
|
|
27
|
+
defaults[purpose] =
|
|
28
|
+
isModelId(id) && mediaPurpose(id) === purpose && !mediaCapability(id)?.disabledReason ? id : null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return defaults;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function automaticImageDefault(config: Config, ctx: DefaultsContext) {
|
|
35
|
+
const model = ctx.model;
|
|
36
|
+
if (!model || mediaPurpose(model.id)) return undefined;
|
|
37
|
+
const { metadataId, backend } = modelRoute(model.id);
|
|
38
|
+
const alias =
|
|
39
|
+
model.provider === PROVIDER_ID
|
|
40
|
+
? Object.hasOwn(config.aliases, model.id)
|
|
41
|
+
? config.aliases[model.id]
|
|
42
|
+
: backend && Object.hasOwn(config.aliases, metadataId)
|
|
43
|
+
? config.aliases[metadataId]
|
|
44
|
+
: undefined
|
|
45
|
+
: undefined;
|
|
46
|
+
const openai = alias
|
|
47
|
+
? /^(openai|openai-codex)\//.test(alias)
|
|
48
|
+
: model.provider === "openai" ||
|
|
49
|
+
model.provider === "openai-codex" ||
|
|
50
|
+
/^(?:(?:openai|openai-codex)\/)?(?:gpt-|chatgpt-|o[1-9](?:-|$))/.test(metadataId);
|
|
51
|
+
return openai ? OPENAI_IMAGE_DEFAULT : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function effectiveMediaDefaults(config: Config, ctx: DefaultsContext) {
|
|
55
|
+
const defaults = readMediaDefaults(config, ctx);
|
|
56
|
+
const automatic = defaults.image === undefined ? automaticImageDefault(config, ctx) : undefined;
|
|
57
|
+
if (automatic) defaults.image = automatic;
|
|
58
|
+
return { defaults, automatic };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveMediaModel(
|
|
62
|
+
config: Config,
|
|
63
|
+
ctx: DefaultsContext,
|
|
64
|
+
purpose: MediaPurpose,
|
|
65
|
+
explicit?: string,
|
|
66
|
+
) {
|
|
67
|
+
const model = explicit === undefined ? effectiveMediaDefaults(config, ctx).defaults[purpose] : explicit;
|
|
68
|
+
if (model === undefined || model === null) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`No ${purpose} default${model === null ? " (saved selection is invalid or disabled)" : ""}. Use /cli:model to select one, or pass an explicit model ID from cliproxyapi_media_models.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const disabledReason = mediaCapability(model)?.disabledReason;
|
|
74
|
+
if (disabledReason) throw new Error(disabledReason);
|
|
75
|
+
if (mediaPurpose(model) !== purpose) {
|
|
76
|
+
throw new Error(`Choose an explicit supported ${purpose} model from cliproxyapi_media_models.`);
|
|
77
|
+
}
|
|
78
|
+
return model;
|
|
79
|
+
}
|