@baique/pi-web-sky 0.1.35 → 0.1.36
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +16 -16
- package/.next/build-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +3 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
- package/.next/server/app/api/agent/[id]/events/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +1 -1
- package/.next/server/app/api/default-cwd/route.js +1 -1
- package/.next/server/app/api/file-index/route.js +1 -1
- package/.next/server/app/api/files/[...path]/route.js +1 -1
- package/.next/server/app/api/mcp/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/project-trust/route.js +1 -1
- package/.next/server/app/api/search/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/install/route.js +1 -1
- package/.next/server/app/api/skills/route.js +2 -31
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/update/route.js +1 -1
- package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +16 -16
- package/.next/server/chunks/2404.js +1 -1
- package/.next/server/chunks/3500.js +30 -0
- package/.next/server/chunks/4226.js +2 -2
- package/.next/server/chunks/542.js +10 -5
- package/.next/server/chunks/5794.js +1 -1
- package/.next/server/chunks/6429.js +1 -1
- package/.next/server/chunks/898.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
- package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
- package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
- package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
- package/.next/static/css/422c7dc0faa04d89.css +1 -0
- package/.next/trace +8 -9
- package/.next/trace-build +1 -1
- package/bin/pi-web-sky.js +15 -20
- package/lib/agent-client.test.mjs +64 -0
- package/lib/agent-client.ts +53 -0
- package/lib/agent-event-connection.test.mjs +193 -0
- package/lib/agent-event-connection.ts +202 -0
- package/lib/agent-event-stream.test.mjs +155 -0
- package/lib/agent-event-stream.ts +132 -0
- package/lib/agent-event-wire.test.mjs +278 -0
- package/lib/agent-event-wire.ts +107 -0
- package/lib/agent-phase.ts +24 -0
- package/lib/allowed-roots.ts +31 -0
- package/lib/ansi.test.mjs +52 -0
- package/lib/ansi.ts +211 -0
- package/lib/api-types.ts +139 -0
- package/lib/app-update.test.mjs +27 -0
- package/lib/app-update.ts +28 -0
- package/lib/atomic-file.test.mjs +37 -0
- package/lib/atomic-file.ts +34 -0
- package/lib/audit-session.ts +235 -0
- package/lib/bash-output.test.mjs +63 -0
- package/lib/bash-output.ts +53 -0
- package/lib/bg-image.ts +129 -0
- package/lib/board-events.ts +83 -0
- package/lib/board-purge.test.mjs +66 -0
- package/lib/board-purge.ts +65 -0
- package/lib/board-search.test.mjs +110 -0
- package/lib/board-search.ts +63 -0
- package/lib/board-store.test.mjs +542 -0
- package/lib/board-store.ts +794 -0
- package/lib/board-types.ts +96 -0
- package/lib/board-utils.ts +17 -0
- package/lib/bounded-form-data.test.mjs +54 -0
- package/lib/bounded-form-data.ts +49 -0
- package/lib/browser-notifications.test.mjs +167 -0
- package/lib/browser-notifications.ts +104 -0
- package/lib/chat-lazy-load.test.mjs +140 -0
- package/lib/chat-lazy-load.ts +64 -0
- package/lib/clipboard-paths.test.mjs +140 -0
- package/lib/clipboard-paths.ts +192 -0
- package/lib/clipboard.ts +18 -0
- package/lib/compaction-summary.test.mjs +44 -0
- package/lib/compaction-summary.ts +34 -0
- package/lib/custom-ui-terminal.test.mjs +32 -0
- package/lib/custom-ui-terminal.ts +27 -0
- package/lib/deepseek-pricing.test.mjs +120 -0
- package/lib/deepseek-pricing.ts +71 -0
- package/lib/directory-browser.test.mjs +60 -0
- package/lib/directory-browser.ts +83 -0
- package/lib/draft-stash.test.mjs +91 -0
- package/lib/draft-stash.ts +102 -0
- package/lib/draft-store.ts +105 -0
- package/lib/dropdown-direction.ts +27 -0
- package/lib/extension-command.test.mjs +63 -0
- package/lib/extension-command.ts +81 -0
- package/lib/file-access.test.mjs +31 -0
- package/lib/file-access.ts +59 -0
- package/lib/file-dirent.test.mjs +56 -0
- package/lib/file-dirent.ts +17 -0
- package/lib/file-explorer-state.test.mjs +63 -0
- package/lib/file-explorer-state.ts +36 -0
- package/lib/file-fuzzy.test.mjs +29 -0
- package/lib/file-fuzzy.ts +189 -0
- package/lib/file-links.test.mjs +92 -0
- package/lib/file-links.ts +148 -0
- package/lib/file-paths.ts +43 -0
- package/lib/file-types.test.mjs +34 -0
- package/lib/file-types.ts +73 -0
- package/lib/file-upload.test.mjs +70 -0
- package/lib/file-upload.ts +59 -0
- package/lib/file-viewer-state.test.mjs +20 -0
- package/lib/file-viewer-state.ts +15 -0
- package/lib/frontmatter.test.mjs +55 -0
- package/lib/frontmatter.ts +87 -0
- package/lib/git-changes.test.mjs +52 -0
- package/lib/git-changes.ts +232 -0
- package/lib/git-status.ts +47 -0
- package/lib/git-types.ts +29 -0
- package/lib/http-dispatcher.test.mjs +83 -0
- package/lib/http-dispatcher.ts +86 -0
- package/lib/i18n/format.test.mjs +19 -0
- package/lib/i18n/format.ts +60 -0
- package/lib/i18n/messages/en.ts +662 -0
- package/lib/i18n/messages/zh-CN.ts +661 -0
- package/lib/i18n/registry.test.mjs +32 -0
- package/lib/i18n/registry.ts +43 -0
- package/lib/i18n/types.ts +15 -0
- package/lib/image-attachments.test.mjs +27 -0
- package/lib/image-attachments.ts +56 -0
- package/lib/initial-navigation.test.mjs +65 -0
- package/lib/initial-navigation.ts +15 -0
- package/lib/markdown.test.mjs +104 -0
- package/lib/markdown.ts +347 -0
- package/lib/message-display.test.mjs +130 -0
- package/lib/message-display.ts +47 -0
- package/lib/model-catalog.test.mjs +327 -0
- package/lib/model-catalog.ts +414 -0
- package/lib/model-discovery-auth.ts +58 -0
- package/lib/model-discovery.test.mjs +50 -0
- package/lib/model-discovery.ts +75 -0
- package/lib/model-id-preference.ts +46 -0
- package/lib/model-scope.test.mjs +193 -0
- package/lib/model-scope.ts +198 -0
- package/lib/models-cache.test.mjs +122 -0
- package/lib/models-cache.ts +90 -0
- package/lib/models-config-store.test.mjs +200 -0
- package/lib/models-config-store.ts +84 -0
- package/lib/next-config-esm.test.mjs +14 -0
- package/lib/next-config.test.mjs +13 -0
- package/lib/node-version.test.mjs +33 -0
- package/lib/normalize.test.mjs +54 -0
- package/lib/normalize.ts +63 -0
- package/lib/npx.ts +62 -0
- package/lib/panel-layout.test.mjs +56 -0
- package/lib/panel-layout.ts +52 -0
- package/lib/patch.test.mjs +110 -0
- package/lib/patch.ts +134 -0
- package/lib/path-security.ts +42 -0
- package/lib/paths.test.mjs +58 -0
- package/lib/paths.ts +71 -0
- package/lib/pi-types.ts +183 -0
- package/lib/pi-web-options.test.mjs +63 -0
- package/lib/process-lifecycle.test.mjs +83 -0
- package/lib/project-command-env.test.mjs +298 -0
- package/lib/project-command-env.ts +129 -0
- package/lib/project-groups.test.mjs +60 -0
- package/lib/project-groups.ts +53 -0
- package/lib/project-identity.test.mjs +37 -0
- package/lib/project-identity.ts +26 -0
- package/lib/project-tree.test.mjs +115 -0
- package/lib/project-tree.ts +176 -0
- package/lib/project-trust.test.mjs +117 -0
- package/lib/project-trust.ts +48 -0
- package/lib/prompt-recovery.test.mjs +33 -0
- package/lib/prompt-recovery.ts +47 -0
- package/lib/provider-api-key-route.test.mjs +12 -0
- package/lib/provider-credential-store.test.mjs +126 -0
- package/lib/provider-credential-store.ts +114 -0
- package/lib/provider-listing-runtime.ts +37 -0
- package/lib/provider-listing.test.mjs +114 -0
- package/lib/provider-listing.ts +118 -0
- package/lib/provider-services.ts +64 -0
- package/lib/request-security.test.mjs +256 -0
- package/lib/request-security.ts +136 -0
- package/lib/rpc-manager-shutdown.test.mjs +372 -0
- package/lib/rpc-manager-widgets.test.mjs +342 -0
- package/lib/rpc-manager.test.mjs +139 -0
- package/lib/rpc-manager.ts +1703 -0
- package/lib/rpc-session-info.test.mjs +93 -0
- package/lib/session-delete.test.mjs +53 -0
- package/lib/session-delete.ts +86 -0
- package/lib/session-file-references-core.ts +82 -0
- package/lib/session-file-references.test.mjs +91 -0
- package/lib/session-file-references.ts +29 -0
- package/lib/session-path.test.mjs +24 -0
- package/lib/session-path.ts +11 -0
- package/lib/session-reader.pagination.test.mjs +115 -0
- package/lib/session-reader.test.mjs +446 -0
- package/lib/session-reader.ts +548 -0
- package/lib/session-resolve.test.mjs +65 -0
- package/lib/session-row-context-menu.test.mjs +46 -0
- package/lib/session-row-context-menu.ts +29 -0
- package/lib/session-scanner.test.mjs +151 -0
- package/lib/session-scanner.ts +238 -0
- package/lib/session-search.test.mjs +173 -0
- package/lib/session-search.ts +284 -0
- package/lib/session-stats.ts +26 -0
- package/lib/session-timing.test.mjs +107 -0
- package/lib/session-timing.ts +45 -0
- package/lib/session-title.test.mjs +284 -0
- package/lib/session-title.ts +255 -0
- package/lib/skill-frontmatter.test.mjs +75 -0
- package/lib/skill-frontmatter.ts +40 -0
- package/lib/skill-lock.test.mjs +182 -0
- package/lib/skill-lock.ts +146 -0
- package/lib/skill-updates.test.mjs +182 -0
- package/lib/skill-updates.ts +263 -0
- package/lib/skills-service.ts +16 -0
- package/lib/slash-display.test.mjs +122 -0
- package/lib/slash-display.ts +30 -0
- package/lib/sqlite-db.test.mjs +140 -0
- package/lib/sqlite-db.ts +205 -0
- package/lib/startup-preferences.test.mjs +130 -0
- package/lib/startup-preferences.ts +65 -0
- package/lib/streaming-message.test.mjs +212 -0
- package/lib/streaming-message.ts +151 -0
- package/lib/subagent-widget.test.mjs +87 -0
- package/lib/subagent-widget.ts +109 -0
- package/lib/sync-room-server.mjs +137 -0
- package/lib/task-card-store.test.mjs +309 -0
- package/lib/task-card-store.ts +559 -0
- package/lib/task-scheduler.ts +498 -0
- package/lib/task-store.test.mjs +216 -0
- package/lib/task-store.ts +271 -0
- package/lib/terminal-input.test.mjs +46 -0
- package/lib/terminal-input.ts +63 -0
- package/lib/terminal-manager.test.mjs +71 -0
- package/lib/terminal-manager.ts +245 -0
- package/lib/tool-execution-progress.test.mjs +32 -0
- package/lib/tool-execution-progress.ts +29 -0
- package/lib/tool-names.ts +25 -0
- package/lib/tool-preset-preference.test.mjs +72 -0
- package/lib/tool-preset-preference.ts +41 -0
- package/lib/tool-presets.test.mjs +56 -0
- package/lib/tool-presets.ts +42 -0
- package/lib/turn-written-files.test.mjs +137 -0
- package/lib/turn-written-files.ts +60 -0
- package/lib/types.ts +349 -0
- package/lib/wallpaper-settings.ts +191 -0
- package/lib/web-auth.test.mjs +50 -0
- package/lib/web-auth.ts +45 -0
- package/lib/workspace-memory.test.mjs +129 -0
- package/lib/workspace-memory.ts +98 -0
- package/lib/worktree.test.mjs +50 -0
- package/lib/worktree.ts +250 -0
- package/package.json +3 -1
- package/server.mjs +34 -0
- package/.next/server/chunks/7513.js +0 -30
- package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
- package/.next/static/css/cf25164cb7c04d95.css +0 -1
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
export interface ModelCatalogCost {
|
|
2
|
+
input?: number;
|
|
3
|
+
output?: number;
|
|
4
|
+
cacheRead?: number;
|
|
5
|
+
cacheWrite?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type CompleteModelCatalogCost = Required<ModelCatalogCost>;
|
|
9
|
+
|
|
10
|
+
export interface ModelCatalogEntry {
|
|
11
|
+
key: string;
|
|
12
|
+
providerId: string;
|
|
13
|
+
providerName: string;
|
|
14
|
+
providerBaseUrl?: string;
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
reasoning?: boolean;
|
|
18
|
+
input?: string[];
|
|
19
|
+
contextWindow?: number;
|
|
20
|
+
maxTokens?: number;
|
|
21
|
+
cost: ModelCatalogCost;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ModelCatalogPreset {
|
|
25
|
+
name?: string;
|
|
26
|
+
reasoning?: boolean;
|
|
27
|
+
input?: string[];
|
|
28
|
+
contextWindow?: number;
|
|
29
|
+
maxTokens?: number;
|
|
30
|
+
cost?: CompleteModelCatalogCost;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ModelCatalogMatchMethod = "provider" | "base-url" | "consensus" | "none";
|
|
34
|
+
|
|
35
|
+
export type ModelCatalogPriceRecommendation =
|
|
36
|
+
| {
|
|
37
|
+
status: "reliable";
|
|
38
|
+
method: Exclude<ModelCatalogMatchMethod, "none">;
|
|
39
|
+
cost: CompleteModelCatalogCost;
|
|
40
|
+
providerId?: string;
|
|
41
|
+
providerName?: string;
|
|
42
|
+
support: number;
|
|
43
|
+
total: number;
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
status: "unreliable";
|
|
47
|
+
reason: "no-exact-match" | "no-valid-price" | "insufficient-support" | "conflict";
|
|
48
|
+
support: number;
|
|
49
|
+
total: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export interface ModelCatalogRecommendation {
|
|
53
|
+
exactMatches: number;
|
|
54
|
+
metadataMethod: ModelCatalogMatchMethod;
|
|
55
|
+
matchedProviderId?: string;
|
|
56
|
+
matchedProviderName?: string;
|
|
57
|
+
preset: ModelCatalogPreset;
|
|
58
|
+
price: ModelCatalogPriceRecommendation;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const CONSENSUS_MIN_SHARE = 0.6;
|
|
62
|
+
const CONSENSUS_MIN_SUPPORT = 5;
|
|
63
|
+
const KNOWN_PROVIDER_HOSTS: Record<string, readonly string[]> = {
|
|
64
|
+
anthropic: ["api.anthropic.com"],
|
|
65
|
+
google: ["generativelanguage.googleapis.com"],
|
|
66
|
+
openai: ["api.openai.com"],
|
|
67
|
+
openrouter: ["openrouter.ai"],
|
|
68
|
+
};
|
|
69
|
+
const SUPPORTED_INPUT_MODALITIES = new Set(["text", "image"]);
|
|
70
|
+
|
|
71
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
72
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cleanString(value: unknown): string | undefined {
|
|
76
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function optionalNonNegativeNumber(value: unknown): number | undefined {
|
|
80
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function optionalPositiveNumber(value: unknown): number | undefined {
|
|
84
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readCost(value: unknown): ModelCatalogCost {
|
|
88
|
+
if (!isRecord(value)) return {};
|
|
89
|
+
return {
|
|
90
|
+
input: optionalNonNegativeNumber(value.input),
|
|
91
|
+
output: optionalNonNegativeNumber(value.output),
|
|
92
|
+
cacheRead: optionalNonNegativeNumber(value.cache_read),
|
|
93
|
+
cacheWrite: optionalNonNegativeNumber(value.cache_write),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readInputModalities(value: unknown): string[] | undefined {
|
|
98
|
+
if (!isRecord(value) || !Array.isArray(value.input)) return undefined;
|
|
99
|
+
const input = Array.from(new Set(value.input
|
|
100
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
101
|
+
.map((entry) => entry.trim().toLocaleLowerCase())
|
|
102
|
+
.filter((entry) => SUPPORTED_INPUT_MODALITIES.has(entry))));
|
|
103
|
+
return input.length ? input : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeProvider(value: string): string {
|
|
107
|
+
return value.trim().toLocaleLowerCase().replace(/[^a-z0-9]/g, "");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeModelId(value: string): string {
|
|
111
|
+
return value.trim().toLocaleLowerCase().replace(/^models\//, "");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hostname(value: string | undefined): string | undefined {
|
|
115
|
+
if (!value) return undefined;
|
|
116
|
+
try {
|
|
117
|
+
return new URL(value).hostname.toLocaleLowerCase().replace(/\.$/, "");
|
|
118
|
+
} catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function hostMatches(actual: string, expected: string): boolean {
|
|
124
|
+
return actual === expected || actual.endsWith(`.${expected}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function providerMatches(entry: ModelCatalogEntry, providerHint: string): boolean {
|
|
128
|
+
const normalizedHint = normalizeProvider(providerHint);
|
|
129
|
+
if (!normalizedHint) return false;
|
|
130
|
+
return normalizeProvider(entry.providerId) === normalizedHint
|
|
131
|
+
|| normalizeProvider(entry.providerName) === normalizedHint;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function baseUrlMatches(entry: ModelCatalogEntry, baseUrl: string): boolean {
|
|
135
|
+
const actualHost = hostname(baseUrl);
|
|
136
|
+
if (!actualHost) return false;
|
|
137
|
+
const knownHosts = KNOWN_PROVIDER_HOSTS[normalizeProvider(entry.providerId)] ?? [];
|
|
138
|
+
const providerHost = hostname(entry.providerBaseUrl);
|
|
139
|
+
return [...knownHosts, ...(providerHost ? [providerHost] : [])]
|
|
140
|
+
.some((candidate) => hostMatches(actualHost, candidate));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function exactModelMatches(entry: ModelCatalogEntry, query: string): boolean {
|
|
144
|
+
const normalizedQuery = normalizeModelId(query);
|
|
145
|
+
if (!normalizedQuery) return false;
|
|
146
|
+
const normalizedId = normalizeModelId(entry.id);
|
|
147
|
+
const normalizedFullId = `${entry.providerId.toLocaleLowerCase()}/${normalizedId}`;
|
|
148
|
+
return normalizedId === normalizedQuery || normalizedFullId === normalizedQuery;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validPrice(entry: ModelCatalogEntry): entry is ModelCatalogEntry & {
|
|
152
|
+
cost: ModelCatalogCost & { input: number; output: number };
|
|
153
|
+
} {
|
|
154
|
+
return entry.cost.input !== undefined && entry.cost.output !== undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function modeValue<T>(
|
|
158
|
+
values: readonly T[],
|
|
159
|
+
total: number,
|
|
160
|
+
keyFor: (value: T) => string,
|
|
161
|
+
): T | undefined {
|
|
162
|
+
if (values.length === 0 || total <= 0) return undefined;
|
|
163
|
+
const groups = new Map<string, { value: T; count: number }>();
|
|
164
|
+
for (const value of values) {
|
|
165
|
+
const key = keyFor(value);
|
|
166
|
+
const current = groups.get(key);
|
|
167
|
+
if (current) current.count += 1;
|
|
168
|
+
else groups.set(key, { value, count: 1 });
|
|
169
|
+
}
|
|
170
|
+
const ranked = [...groups.values()].sort((a, b) => b.count - a.count);
|
|
171
|
+
const winner = ranked[0];
|
|
172
|
+
if (!winner || winner.count / total < CONSENSUS_MIN_SHARE) return undefined;
|
|
173
|
+
if (ranked[1]?.count === winner.count) return undefined;
|
|
174
|
+
return winner.value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function modeNumber(values: readonly number[]): number | undefined {
|
|
178
|
+
if (values.length === 0) return undefined;
|
|
179
|
+
const groups = new Map<number, number>();
|
|
180
|
+
for (const value of values) groups.set(value, (groups.get(value) ?? 0) + 1);
|
|
181
|
+
const ranked = [...groups.entries()].sort((a, b) => b[1] - a[1]);
|
|
182
|
+
if (!ranked[0] || ranked[1]?.[1] === ranked[0][1]) return undefined;
|
|
183
|
+
return ranked[0][0];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function metadataFromEntry(entry: ModelCatalogEntry): ModelCatalogPreset {
|
|
187
|
+
return {
|
|
188
|
+
name: entry.name,
|
|
189
|
+
reasoning: entry.reasoning,
|
|
190
|
+
input: entry.input,
|
|
191
|
+
contextWindow: entry.contextWindow,
|
|
192
|
+
maxTokens: entry.maxTokens,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function consensusMetadata(entries: readonly ModelCatalogEntry[]): ModelCatalogPreset {
|
|
197
|
+
const total = entries.length;
|
|
198
|
+
return {
|
|
199
|
+
name: modeValue(entries.map((entry) => entry.name), total, (value) => value.toLocaleLowerCase()),
|
|
200
|
+
reasoning: modeValue(
|
|
201
|
+
entries.flatMap((entry) => entry.reasoning === undefined ? [] : [entry.reasoning]),
|
|
202
|
+
total,
|
|
203
|
+
String,
|
|
204
|
+
),
|
|
205
|
+
input: modeValue(
|
|
206
|
+
entries.flatMap((entry) => entry.input ? [entry.input] : []),
|
|
207
|
+
total,
|
|
208
|
+
(value) => [...value].sort().join(","),
|
|
209
|
+
),
|
|
210
|
+
contextWindow: modeValue(
|
|
211
|
+
entries.flatMap((entry) => entry.contextWindow === undefined ? [] : [entry.contextWindow]),
|
|
212
|
+
total,
|
|
213
|
+
String,
|
|
214
|
+
),
|
|
215
|
+
maxTokens: modeValue(
|
|
216
|
+
entries.flatMap((entry) => entry.maxTokens === undefined ? [] : [entry.maxTokens]),
|
|
217
|
+
total,
|
|
218
|
+
String,
|
|
219
|
+
),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function priceFromEntry(
|
|
224
|
+
entry: ModelCatalogEntry & { cost: ModelCatalogCost & { input: number; output: number } },
|
|
225
|
+
method: "provider" | "base-url",
|
|
226
|
+
): ModelCatalogPriceRecommendation {
|
|
227
|
+
return {
|
|
228
|
+
status: "reliable",
|
|
229
|
+
method,
|
|
230
|
+
cost: {
|
|
231
|
+
input: entry.cost.input,
|
|
232
|
+
output: entry.cost.output,
|
|
233
|
+
cacheRead: entry.cost.cacheRead ?? 0,
|
|
234
|
+
cacheWrite: entry.cost.cacheWrite ?? 0,
|
|
235
|
+
},
|
|
236
|
+
providerId: entry.providerId,
|
|
237
|
+
providerName: entry.providerName,
|
|
238
|
+
support: 1,
|
|
239
|
+
total: 1,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function consensusPrice(entries: readonly ModelCatalogEntry[]): ModelCatalogPriceRecommendation {
|
|
244
|
+
const priced = entries.filter(validPrice);
|
|
245
|
+
if (priced.length === 0) {
|
|
246
|
+
return { status: "unreliable", reason: "no-valid-price", support: 0, total: 0 };
|
|
247
|
+
}
|
|
248
|
+
if (priced.length === 1) {
|
|
249
|
+
return { status: "unreliable", reason: "insufficient-support", support: 1, total: 1 };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const groups = new Map<string, typeof priced>();
|
|
253
|
+
for (const entry of priced) {
|
|
254
|
+
const key = JSON.stringify([entry.cost.input, entry.cost.output]);
|
|
255
|
+
const group = groups.get(key);
|
|
256
|
+
if (group) group.push(entry);
|
|
257
|
+
else groups.set(key, [entry]);
|
|
258
|
+
}
|
|
259
|
+
const ranked = [...groups.values()].sort((a, b) => b.length - a.length);
|
|
260
|
+
const winner = ranked[0];
|
|
261
|
+
if (!winner) {
|
|
262
|
+
return { status: "unreliable", reason: "no-valid-price", support: 0, total: priced.length };
|
|
263
|
+
}
|
|
264
|
+
const hasConsensus = winner.length / priced.length >= CONSENSUS_MIN_SHARE
|
|
265
|
+
|| winner.length >= CONSENSUS_MIN_SUPPORT;
|
|
266
|
+
if (ranked[1]?.length === winner.length || !hasConsensus) {
|
|
267
|
+
return {
|
|
268
|
+
status: "unreliable",
|
|
269
|
+
reason: "conflict",
|
|
270
|
+
support: winner.length,
|
|
271
|
+
total: priced.length,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const cacheRead = modeNumber(winner.map((entry) => entry.cost.cacheRead ?? 0)) ?? 0;
|
|
276
|
+
const cacheWrite = modeNumber(winner.map((entry) => entry.cost.cacheWrite ?? 0)) ?? 0;
|
|
277
|
+
return {
|
|
278
|
+
status: "reliable",
|
|
279
|
+
method: "consensus",
|
|
280
|
+
cost: {
|
|
281
|
+
input: winner[0].cost.input,
|
|
282
|
+
output: winner[0].cost.output,
|
|
283
|
+
cacheRead,
|
|
284
|
+
cacheWrite,
|
|
285
|
+
},
|
|
286
|
+
support: winner.length,
|
|
287
|
+
total: priced.length,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function flattenModelsDevCatalog(value: unknown): ModelCatalogEntry[] {
|
|
292
|
+
if (!isRecord(value)) return [];
|
|
293
|
+
|
|
294
|
+
const entries: ModelCatalogEntry[] = [];
|
|
295
|
+
for (const [providerId, rawProvider] of Object.entries(value)) {
|
|
296
|
+
if (!isRecord(rawProvider) || !isRecord(rawProvider.models)) continue;
|
|
297
|
+
const providerName = cleanString(rawProvider.name) ?? providerId;
|
|
298
|
+
const providerBaseUrl = cleanString(rawProvider.api);
|
|
299
|
+
|
|
300
|
+
for (const [fallbackId, rawModel] of Object.entries(rawProvider.models)) {
|
|
301
|
+
if (!isRecord(rawModel)) continue;
|
|
302
|
+
const id = cleanString(rawModel.id) ?? fallbackId;
|
|
303
|
+
if (!id) continue;
|
|
304
|
+
const name = cleanString(rawModel.name) ?? id;
|
|
305
|
+
const entry: ModelCatalogEntry = {
|
|
306
|
+
key: `${providerId}/${id}`,
|
|
307
|
+
providerId,
|
|
308
|
+
providerName,
|
|
309
|
+
id,
|
|
310
|
+
name,
|
|
311
|
+
cost: readCost(rawModel.cost),
|
|
312
|
+
};
|
|
313
|
+
if (providerBaseUrl) entry.providerBaseUrl = providerBaseUrl;
|
|
314
|
+
if (typeof rawModel.reasoning === "boolean") entry.reasoning = rawModel.reasoning;
|
|
315
|
+
const input = readInputModalities(rawModel.modalities);
|
|
316
|
+
if (input) entry.input = input;
|
|
317
|
+
if (isRecord(rawModel.limit)) {
|
|
318
|
+
const contextWindow = optionalPositiveNumber(rawModel.limit.context);
|
|
319
|
+
const maxTokens = optionalPositiveNumber(rawModel.limit.output);
|
|
320
|
+
if (contextWindow !== undefined) entry.contextWindow = contextWindow;
|
|
321
|
+
if (maxTokens !== undefined) entry.maxTokens = maxTokens;
|
|
322
|
+
}
|
|
323
|
+
entries.push(entry);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return entries;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function recommendModelCatalogPreset(
|
|
331
|
+
entries: readonly ModelCatalogEntry[],
|
|
332
|
+
query: string,
|
|
333
|
+
providerHint = "",
|
|
334
|
+
baseUrl = "",
|
|
335
|
+
): ModelCatalogRecommendation {
|
|
336
|
+
const exactEntries = entries.filter((entry) => exactModelMatches(entry, query));
|
|
337
|
+
if (exactEntries.length === 0) {
|
|
338
|
+
return {
|
|
339
|
+
exactMatches: 0,
|
|
340
|
+
metadataMethod: "none",
|
|
341
|
+
preset: {},
|
|
342
|
+
price: { status: "unreliable", reason: "no-exact-match", support: 0, total: 0 },
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const providerEntries = exactEntries.filter((entry) => providerMatches(entry, providerHint));
|
|
347
|
+
const baseUrlEntries = exactEntries.filter((entry) => baseUrlMatches(entry, baseUrl));
|
|
348
|
+
const metadataEntry = providerEntries[0] ?? baseUrlEntries[0];
|
|
349
|
+
const metadataMethod: ModelCatalogMatchMethod = providerEntries.length
|
|
350
|
+
? "provider"
|
|
351
|
+
: baseUrlEntries.length
|
|
352
|
+
? "base-url"
|
|
353
|
+
: "consensus";
|
|
354
|
+
const preset = metadataEntry ? metadataFromEntry(metadataEntry) : consensusMetadata(exactEntries);
|
|
355
|
+
|
|
356
|
+
const providerPrice = providerEntries.find(validPrice);
|
|
357
|
+
const baseUrlPrice = baseUrlEntries.find(validPrice);
|
|
358
|
+
const price = providerPrice
|
|
359
|
+
? priceFromEntry(providerPrice, "provider")
|
|
360
|
+
: baseUrlPrice
|
|
361
|
+
? priceFromEntry(baseUrlPrice, "base-url")
|
|
362
|
+
: consensusPrice(exactEntries);
|
|
363
|
+
if (price.status === "reliable") preset.cost = price.cost;
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
exactMatches: exactEntries.length,
|
|
367
|
+
metadataMethod,
|
|
368
|
+
matchedProviderId: metadataEntry?.providerId,
|
|
369
|
+
matchedProviderName: metadataEntry?.providerName,
|
|
370
|
+
preset,
|
|
371
|
+
price,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function matchRank(entry: ModelCatalogEntry, query: string, providerHint: string): number {
|
|
376
|
+
const id = entry.id.toLocaleLowerCase();
|
|
377
|
+
const name = entry.name.toLocaleLowerCase();
|
|
378
|
+
const providerId = entry.providerId.toLocaleLowerCase();
|
|
379
|
+
const providerName = entry.providerName.toLocaleLowerCase();
|
|
380
|
+
const fullId = `${providerId}/${id}`;
|
|
381
|
+
|
|
382
|
+
let rank = 20;
|
|
383
|
+
if (!query) rank = 10;
|
|
384
|
+
else if (id === query || fullId === query) rank = 0;
|
|
385
|
+
else if (name === query) rank = 1;
|
|
386
|
+
else if (id.startsWith(query) || name.startsWith(query)) rank = 2;
|
|
387
|
+
else if (fullId.startsWith(query) || providerId === query || providerName === query) rank = 3;
|
|
388
|
+
else if (id.includes(query) || name.includes(query)) rank = 4;
|
|
389
|
+
else if (fullId.includes(query) || providerName.includes(query)) rank = 5;
|
|
390
|
+
|
|
391
|
+
if (rank < 20 && providerHint && (providerId === providerHint || providerName === providerHint)) rank -= 0.5;
|
|
392
|
+
return rank;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function searchModelCatalog(
|
|
396
|
+
entries: readonly ModelCatalogEntry[],
|
|
397
|
+
query: string,
|
|
398
|
+
providerHint = "",
|
|
399
|
+
limit = 50,
|
|
400
|
+
): ModelCatalogEntry[] {
|
|
401
|
+
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
402
|
+
const normalizedProvider = providerHint.trim().toLocaleLowerCase();
|
|
403
|
+
const cappedLimit = Math.max(1, Math.min(100, Math.floor(limit) || 50));
|
|
404
|
+
|
|
405
|
+
return entries
|
|
406
|
+
.map((entry) => ({ entry, rank: matchRank(entry, normalizedQuery, normalizedProvider) }))
|
|
407
|
+
.filter(({ rank }) => !normalizedQuery || rank < 20)
|
|
408
|
+
.sort((a, b) => a.rank - b.rank
|
|
409
|
+
|| a.entry.providerName.localeCompare(b.entry.providerName, undefined, { sensitivity: "base" })
|
|
410
|
+
|| a.entry.name.localeCompare(b.entry.name, undefined, { numeric: true, sensitivity: "base" })
|
|
411
|
+
|| a.entry.id.localeCompare(b.entry.id, undefined, { numeric: true, sensitivity: "base" }))
|
|
412
|
+
.slice(0, cappedLimit)
|
|
413
|
+
.map(({ entry }) => entry);
|
|
414
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export interface ModelDiscoveryAuth {
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
headers: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function stringRecord(value: unknown): Record<string, string> {
|
|
16
|
+
if (!isRecord(value)) return {};
|
|
17
|
+
return Object.fromEntries(Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function resolveModelDiscoveryAuth(
|
|
21
|
+
providerName: string,
|
|
22
|
+
provider: Record<string, unknown>,
|
|
23
|
+
): Promise<ModelDiscoveryAuth> {
|
|
24
|
+
let tempDir: string | undefined;
|
|
25
|
+
try {
|
|
26
|
+
tempDir = mkdtempSync(join(tmpdir(), "pi-web-model-discovery-"));
|
|
27
|
+
const modelsPath = join(tempDir, "models.json");
|
|
28
|
+
const discoveryModelId = "__pi_web_model_discovery__";
|
|
29
|
+
writeFileSync(modelsPath, JSON.stringify({
|
|
30
|
+
providers: {
|
|
31
|
+
[providerName]: {
|
|
32
|
+
...provider,
|
|
33
|
+
models: [{ id: discoveryModelId }],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}, null, 2), "utf8");
|
|
37
|
+
|
|
38
|
+
const modelRuntime = await ModelRuntime.create({ modelsPath });
|
|
39
|
+
const loadError = modelRuntime.getError();
|
|
40
|
+
if (loadError) throw new Error(loadError);
|
|
41
|
+
const model = modelRuntime.getModel(providerName, discoveryModelId);
|
|
42
|
+
if (!model) throw new Error(`Unable to load provider "${providerName}"`);
|
|
43
|
+
|
|
44
|
+
const resolved = await modelRuntime.getAuth(model);
|
|
45
|
+
if (resolved) {
|
|
46
|
+
return {
|
|
47
|
+
apiKey: resolved.auth.apiKey,
|
|
48
|
+
headers: stringRecord(resolved.auth.headers),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
headers: stringRecord(modelRuntime.getCompatibilityRequestConfig(model).headers),
|
|
54
|
+
};
|
|
55
|
+
} finally {
|
|
56
|
+
if (tempDir) rmSync(tempDir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
async function loadSubject(path) {
|
|
5
|
+
try {
|
|
6
|
+
const { createJiti } = await import("jiti");
|
|
7
|
+
return createJiti(import.meta.url).import(path);
|
|
8
|
+
} catch {
|
|
9
|
+
return import(path);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { buildModelsListUrl, parseDiscoveredModels } = await loadSubject("./model-discovery.ts");
|
|
14
|
+
const { resolveModelDiscoveryAuth } = await loadSubject("./model-discovery-auth.ts");
|
|
15
|
+
|
|
16
|
+
test("builds protocol-appropriate model list URLs", () => {
|
|
17
|
+
assert.equal(buildModelsListUrl("https://api.example.com/v1/", "openai-completions").toString(), "https://api.example.com/v1/models");
|
|
18
|
+
assert.equal(buildModelsListUrl("https://api.anthropic.com", "anthropic-messages").toString(), "https://api.anthropic.com/v1/models?limit=1000");
|
|
19
|
+
assert.equal(buildModelsListUrl("https://generativelanguage.googleapis.com", "google-generative-ai").toString(), "https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000");
|
|
20
|
+
assert.equal(buildModelsListUrl("https://api.example.com/custom/models", "openai-responses").toString(), "https://api.example.com/custom/models");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("parses OpenAI, Anthropic, Google, and string model lists", () => {
|
|
24
|
+
assert.deepEqual(parseDiscoveredModels({ data: [{ id: "gpt-5" }, { id: "claude", display_name: "Claude" }] }), [
|
|
25
|
+
{ id: "claude", name: "Claude" },
|
|
26
|
+
{ id: "gpt-5" },
|
|
27
|
+
]);
|
|
28
|
+
assert.deepEqual(parseDiscoveredModels({ models: [{ name: "models/gemini-2.5-pro", displayName: "Gemini 2.5 Pro" }] }), [
|
|
29
|
+
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
|
|
30
|
+
]);
|
|
31
|
+
assert.deepEqual(parseDiscoveredModels(["zeta", "alpha", "alpha"]), [
|
|
32
|
+
{ id: "alpha" },
|
|
33
|
+
{ id: "zeta" },
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("resolves environment-backed headers without an API key", async () => {
|
|
38
|
+
process.env.PI_WEB_DISCOVERY_TEST_TOKEN = "resolved-token";
|
|
39
|
+
try {
|
|
40
|
+
const auth = await resolveModelDiscoveryAuth("pi-web-header-only-test", {
|
|
41
|
+
baseUrl: "https://example.invalid/v1",
|
|
42
|
+
api: "openai-completions",
|
|
43
|
+
headers: { "X-Discovery-Token": "$PI_WEB_DISCOVERY_TEST_TOKEN" },
|
|
44
|
+
});
|
|
45
|
+
assert.equal(auth.apiKey, undefined);
|
|
46
|
+
assert.deepEqual(auth.headers, { "X-Discovery-Token": "resolved-token" });
|
|
47
|
+
} finally {
|
|
48
|
+
delete process.env.PI_WEB_DISCOVERY_TEST_TOKEN;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface DiscoveredModel {
|
|
2
|
+
id: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function cleanString(value: unknown): string | undefined {
|
|
11
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function modelFromValue(value: unknown): DiscoveredModel | null {
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
const id = value.trim();
|
|
17
|
+
return id ? { id } : null;
|
|
18
|
+
}
|
|
19
|
+
if (!isRecord(value)) return null;
|
|
20
|
+
|
|
21
|
+
const rawId = cleanString(value.id) ?? cleanString(value.model) ?? cleanString(value.name);
|
|
22
|
+
if (!rawId) return null;
|
|
23
|
+
const id = rawId.startsWith("models/") ? rawId.slice("models/".length) : rawId;
|
|
24
|
+
if (!id) return null;
|
|
25
|
+
const name = cleanString(value.display_name)
|
|
26
|
+
?? cleanString(value.displayName)
|
|
27
|
+
?? (cleanString(value.id) || cleanString(value.model) ? cleanString(value.name) : undefined);
|
|
28
|
+
return name && name !== id ? { id, name } : { id };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function listFromResponse(value: unknown): unknown[] {
|
|
32
|
+
if (Array.isArray(value)) return value;
|
|
33
|
+
if (!isRecord(value)) return [];
|
|
34
|
+
for (const key of ["data", "models", "results", "items"]) {
|
|
35
|
+
const candidate = value[key];
|
|
36
|
+
if (Array.isArray(candidate)) return candidate;
|
|
37
|
+
if (isRecord(candidate)) return Object.values(candidate);
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseDiscoveredModels(value: unknown): DiscoveredModel[] {
|
|
43
|
+
const seen = new Set<string>();
|
|
44
|
+
const models: DiscoveredModel[] = [];
|
|
45
|
+
for (const item of listFromResponse(value)) {
|
|
46
|
+
const model = modelFromValue(item);
|
|
47
|
+
if (!model || seen.has(model.id)) continue;
|
|
48
|
+
seen.add(model.id);
|
|
49
|
+
models.push(model);
|
|
50
|
+
}
|
|
51
|
+
return models.sort((a, b) => (a.name ?? a.id).localeCompare(b.name ?? b.id, undefined, {
|
|
52
|
+
numeric: true,
|
|
53
|
+
sensitivity: "base",
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildModelsListUrl(baseUrl: string, api: string): URL {
|
|
58
|
+
const url = new URL(baseUrl.trim());
|
|
59
|
+
const trimmedPath = url.pathname.replace(/\/+$/, "");
|
|
60
|
+
|
|
61
|
+
if (!/\/models$/i.test(trimmedPath)) {
|
|
62
|
+
let path = trimmedPath;
|
|
63
|
+
if (api === "anthropic-messages" && !/\/v\d+(?:beta)?$/i.test(path)) path += "/v1";
|
|
64
|
+
if (api === "google-generative-ai" && !/\/v\d+(?:beta)?$/i.test(path)) path += "/v1beta";
|
|
65
|
+
url.pathname = `${path}/models`.replace(/\/+/g, "/");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (api === "anthropic-messages" && !url.searchParams.has("limit")) {
|
|
69
|
+
url.searchParams.set("limit", "1000");
|
|
70
|
+
}
|
|
71
|
+
if (api === "google-generative-ai" && !url.searchParams.has("pageSize")) {
|
|
72
|
+
url.searchParams.set("pageSize", "1000");
|
|
73
|
+
}
|
|
74
|
+
return url;
|
|
75
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-persisted preference for the model selector: show model IDs
|
|
3
|
+
* instead of display names. Some providers expose multiple models that
|
|
4
|
+
* share the same display name, so the ID is the only way to tell them
|
|
5
|
+
* apart. Mirrors the tool-preset preference pattern (localStorage,
|
|
6
|
+
* best-effort).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const STORAGE_KEY = "pi-model-id-display";
|
|
10
|
+
|
|
11
|
+
interface StorageLike {
|
|
12
|
+
getItem(key: string): string | null;
|
|
13
|
+
setItem(key: string, value: string): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getBrowserStorage(): StorageLike | null {
|
|
17
|
+
if (typeof window === "undefined") return null;
|
|
18
|
+
try {
|
|
19
|
+
return window.localStorage;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getPreferredModelIdDisplay(
|
|
26
|
+
storage: StorageLike | null = getBrowserStorage(),
|
|
27
|
+
): boolean {
|
|
28
|
+
if (!storage) return false;
|
|
29
|
+
try {
|
|
30
|
+
return storage.getItem(STORAGE_KEY) === "1";
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function setPreferredModelIdDisplay(
|
|
37
|
+
enabled: boolean,
|
|
38
|
+
storage: StorageLike | null = getBrowserStorage(),
|
|
39
|
+
): void {
|
|
40
|
+
if (!storage) return;
|
|
41
|
+
try {
|
|
42
|
+
storage.setItem(STORAGE_KEY, enabled ? "1" : "0");
|
|
43
|
+
} catch {
|
|
44
|
+
// Browser storage is best-effort.
|
|
45
|
+
}
|
|
46
|
+
}
|