@hyav/pi-provider 0.1.0-oidc-bootstrap.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/CHANGELOG.md +11 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/README.zh-CN.md +61 -0
- package/SECURITY.md +36 -0
- package/SUPPORT.md +25 -0
- package/core/adapter-extensions.ts +175 -0
- package/core/adapter-protocol.ts +120 -0
- package/core/adapter-validation.ts +241 -0
- package/core/deadline.ts +78 -0
- package/core/definition.ts +64 -0
- package/core/errors.ts +38 -0
- package/core/extension.ts +20 -0
- package/core/host.ts +462 -0
- package/core/live-check-manager.ts +263 -0
- package/core/official-pricing.ts +881 -0
- package/core/opencode-preflight.ts +66 -0
- package/core/preflight-manager.ts +251 -0
- package/core/pricing-adjustments.ts +118 -0
- package/core/provider-registration.ts +261 -0
- package/core/retry-after.ts +24 -0
- package/core/runtime-config.ts +95 -0
- package/core/runtime.ts +473 -0
- package/core/status-manager.ts +332 -0
- package/core/status-report.ts +592 -0
- package/core/tuner-manager.ts +34 -0
- package/core/types.ts +175 -0
- package/index.ts +108 -0
- package/package.json +81 -0
- package/preflight/charm-hyper.ts +62 -0
- package/preflight/deepseek.ts +73 -0
- package/preflight/google.ts +89 -0
- package/preflight/openai-codex.ts +88 -0
- package/preflight/opencode-go.ts +27 -0
- package/preflight/opencode.ts +27 -0
- package/providers/charm-hyper/constants.ts +31 -0
- package/providers/charm-hyper/oauth.ts +360 -0
- package/providers/charm-hyper.ts +536 -0
- package/status/charm-hyper.ts +76 -0
- package/status/deepseek.ts +102 -0
- package/status/openai-codex.ts +224 -0
- package/status/opencode-go.ts +133 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const MAX_RETRY_AFTER_MS = 24 * 60 * 60 * 1_000;
|
|
2
|
+
|
|
3
|
+
function cappedRetryAt(now: number, delayMs: number): number | undefined {
|
|
4
|
+
const retryAt = now + Math.min(delayMs, MAX_RETRY_AFTER_MS);
|
|
5
|
+
return Number.isFinite(retryAt) ? retryAt : undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parseRetryAfter(value: string | null, now: number): number | undefined {
|
|
9
|
+
if (value === null || !Number.isFinite(now)) return undefined;
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
if (trimmed === "") return undefined;
|
|
12
|
+
|
|
13
|
+
if (/^\d+(?:\.\d+)?$/.test(trimmed)) {
|
|
14
|
+
const seconds = Number(trimmed);
|
|
15
|
+
if (!Number.isFinite(seconds)) return cappedRetryAt(now, MAX_RETRY_AFTER_MS);
|
|
16
|
+
return cappedRetryAt(now, seconds * 1_000);
|
|
17
|
+
}
|
|
18
|
+
if (/^[+-]?\d+(?:\.\d+)?$/.test(trimmed)) return undefined;
|
|
19
|
+
|
|
20
|
+
const timestamp = Date.parse(trimmed);
|
|
21
|
+
if (!Number.isFinite(timestamp)) return undefined;
|
|
22
|
+
const maxRetryAt = cappedRetryAt(now, MAX_RETRY_AFTER_MS);
|
|
23
|
+
return maxRetryAt === undefined ? undefined : Math.min(Math.max(now, timestamp), maxRetryAt);
|
|
24
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { isValidTimeoutMs } from "./deadline.ts";
|
|
2
|
+
import type { ProviderKitDefinition } from "./definition.ts";
|
|
3
|
+
import { getDefaultOpenRouterMetadataCachePath, OPENROUTER_MODELS_URL } from "./official-pricing.ts";
|
|
4
|
+
import { validatePricingPolicy } from "./pricing-adjustments.ts";
|
|
5
|
+
import type { ProviderPricingPolicy } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
export interface ProviderKitDependencies {
|
|
8
|
+
fetch: typeof globalThis.fetch;
|
|
9
|
+
now: () => number;
|
|
10
|
+
modelDiscoveryTimeoutMs: number;
|
|
11
|
+
statusRequestTimeoutMs: number;
|
|
12
|
+
liveCheckRequestTimeoutMs: number;
|
|
13
|
+
officialPricingUrl: string;
|
|
14
|
+
officialPricingTimeoutMs: number;
|
|
15
|
+
officialPricingCacheTtlMs: number;
|
|
16
|
+
officialPricingMaxStaleMs: number;
|
|
17
|
+
/** Persistent cache for OpenRouter metadata used by the pricing fallback. */
|
|
18
|
+
openRouterMetadataCachePath: string;
|
|
19
|
+
enableOfficialPricingFallback: boolean;
|
|
20
|
+
/** Optional Provider Kit-level price policies keyed by Provider ID. */
|
|
21
|
+
pricingPolicies?: Record<string, ProviderPricingPolicy>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ProviderKitLoader = (runtime: ProviderKitDependencies) => Promise<ProviderKitDefinition>;
|
|
25
|
+
|
|
26
|
+
const defaultDependencies: ProviderKitDependencies = {
|
|
27
|
+
fetch: globalThis.fetch,
|
|
28
|
+
now: Date.now,
|
|
29
|
+
modelDiscoveryTimeoutMs: 3_000,
|
|
30
|
+
statusRequestTimeoutMs: 8_000,
|
|
31
|
+
liveCheckRequestTimeoutMs: 8_000,
|
|
32
|
+
officialPricingUrl: OPENROUTER_MODELS_URL,
|
|
33
|
+
officialPricingTimeoutMs: 3_000,
|
|
34
|
+
officialPricingCacheTtlMs: 60 * 60 * 1_000,
|
|
35
|
+
officialPricingMaxStaleMs: 24 * 60 * 60 * 1_000,
|
|
36
|
+
openRouterMetadataCachePath: getDefaultOpenRouterMetadataCachePath(),
|
|
37
|
+
enableOfficialPricingFallback: true,
|
|
38
|
+
pricingPolicies: {},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function getDefaultProviderKitDependencies(): ProviderKitDependencies {
|
|
42
|
+
return { ...defaultDependencies };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validateProviderKitDependencies(runtime: ProviderKitDependencies): void {
|
|
46
|
+
if (typeof runtime.fetch !== "function") throw new Error("Provider Kit fetch must be a function");
|
|
47
|
+
if (typeof runtime.now !== "function") throw new Error("Provider Kit now must be a function");
|
|
48
|
+
for (const [name, value] of [
|
|
49
|
+
["modelDiscoveryTimeoutMs", runtime.modelDiscoveryTimeoutMs],
|
|
50
|
+
["statusRequestTimeoutMs", runtime.statusRequestTimeoutMs],
|
|
51
|
+
["liveCheckRequestTimeoutMs", runtime.liveCheckRequestTimeoutMs],
|
|
52
|
+
["officialPricingTimeoutMs", runtime.officialPricingTimeoutMs],
|
|
53
|
+
] as const) {
|
|
54
|
+
if (!isValidTimeoutMs(value)) throw new Error(`Provider Kit ${name} must be a valid timeout`);
|
|
55
|
+
}
|
|
56
|
+
for (const [name, value] of [
|
|
57
|
+
["officialPricingCacheTtlMs", runtime.officialPricingCacheTtlMs],
|
|
58
|
+
["officialPricingMaxStaleMs", runtime.officialPricingMaxStaleMs],
|
|
59
|
+
] as const) {
|
|
60
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
61
|
+
throw new Error(`Provider Kit ${name} must be a finite non-negative number`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (typeof runtime.officialPricingUrl !== "string" || runtime.officialPricingUrl.trim() === "") {
|
|
65
|
+
throw new Error("Provider Kit officialPricingUrl must be a non-empty string");
|
|
66
|
+
}
|
|
67
|
+
if (typeof runtime.openRouterMetadataCachePath !== "string" || runtime.openRouterMetadataCachePath.trim() === "") {
|
|
68
|
+
throw new Error("Provider Kit openRouterMetadataCachePath must be a non-empty path");
|
|
69
|
+
}
|
|
70
|
+
if (typeof runtime.enableOfficialPricingFallback !== "boolean") {
|
|
71
|
+
throw new Error("Provider Kit enableOfficialPricingFallback must be a boolean");
|
|
72
|
+
}
|
|
73
|
+
if (runtime.pricingPolicies !== undefined) {
|
|
74
|
+
if (
|
|
75
|
+
runtime.pricingPolicies === null ||
|
|
76
|
+
typeof runtime.pricingPolicies !== "object" ||
|
|
77
|
+
Array.isArray(runtime.pricingPolicies)
|
|
78
|
+
) {
|
|
79
|
+
throw new Error("Provider Kit pricingPolicies must be an object");
|
|
80
|
+
}
|
|
81
|
+
for (const [providerId, policy] of Object.entries(runtime.pricingPolicies)) {
|
|
82
|
+
if (providerId.trim() === "") throw new Error("Provider Kit pricingPolicies has an empty Provider ID");
|
|
83
|
+
validatePricingPolicy(policy, `Provider Kit pricingPolicies.${providerId}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function resolveProviderKitDependencies(
|
|
89
|
+
dependencies: Partial<ProviderKitDependencies> = {},
|
|
90
|
+
): ProviderKitDependencies {
|
|
91
|
+
const runtime = { ...defaultDependencies, ...dependencies };
|
|
92
|
+
if (runtime.pricingPolicies === undefined) runtime.pricingPolicies = {};
|
|
93
|
+
validateProviderKitDependencies(runtime);
|
|
94
|
+
return runtime;
|
|
95
|
+
}
|
package/core/runtime.ts
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
4
|
+
import type { ProviderKitDefinition } from "./definition.ts";
|
|
5
|
+
import { validateProviderKitDefinition } from "./definition.ts";
|
|
6
|
+
import { LiveCheckManager, type LiveCheckResult } from "./live-check-manager.ts";
|
|
7
|
+
import {
|
|
8
|
+
fetchOfficialPricing,
|
|
9
|
+
findOfficialMeta,
|
|
10
|
+
getPricingCacheAge,
|
|
11
|
+
type OfficialModelMeta,
|
|
12
|
+
OPENROUTER_MODELS_URL,
|
|
13
|
+
} from "./official-pricing.ts";
|
|
14
|
+
import type { PreflightContextLike } from "./preflight-manager.ts";
|
|
15
|
+
import { PreflightManager } from "./preflight-manager.ts";
|
|
16
|
+
import { refreshProviderRegistrations, registerProviderAdapter } from "./provider-registration.ts";
|
|
17
|
+
import type { ProviderKitDependencies, ProviderKitLoader } from "./runtime-config.ts";
|
|
18
|
+
import { resolveProviderKitDependencies } from "./runtime-config.ts";
|
|
19
|
+
import type { StatusContextLike } from "./status-manager.ts";
|
|
20
|
+
import { StatusManager } from "./status-manager.ts";
|
|
21
|
+
import {
|
|
22
|
+
formatProviderStatus,
|
|
23
|
+
getStatusModeCompletions,
|
|
24
|
+
type NativeProviderRegistry,
|
|
25
|
+
nativeModelMatches,
|
|
26
|
+
parseStatusMode,
|
|
27
|
+
resolveNativeProvider,
|
|
28
|
+
} from "./status-report.ts";
|
|
29
|
+
import { applyTunerAdapters, sortTunerAdapters } from "./tuner-manager.ts";
|
|
30
|
+
import type {
|
|
31
|
+
ModelMetadataStatus,
|
|
32
|
+
ProviderAdapter,
|
|
33
|
+
ProviderCost,
|
|
34
|
+
ProviderModelDraft,
|
|
35
|
+
ProviderModelMetadata,
|
|
36
|
+
} from "./types.ts";
|
|
37
|
+
|
|
38
|
+
type ActiveModel = NonNullable<ExtensionContext["model"]>;
|
|
39
|
+
type StatusNotificationContext = Pick<ExtensionContext, "modelRegistry" | "ui"> & {
|
|
40
|
+
mode?: ExtensionContext["mode"];
|
|
41
|
+
};
|
|
42
|
+
const STATUS_WIDGET_KEY = "provider-kit-status";
|
|
43
|
+
|
|
44
|
+
function readProviderCredentialMetadata(provider: string): unknown {
|
|
45
|
+
try {
|
|
46
|
+
const credential = readStoredCredential(provider);
|
|
47
|
+
if (credential?.type !== "oauth") return undefined;
|
|
48
|
+
return {
|
|
49
|
+
type: "oauth",
|
|
50
|
+
...(typeof credential.teamName === "string" ? { teamName: credential.teamName } : {}),
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function clearTransientStatus(ctx: Pick<ExtensionContext, "ui">): void {
|
|
58
|
+
if (typeof ctx.ui.setWidget !== "function") return;
|
|
59
|
+
ctx.ui.setWidget(STATUS_WIDGET_KEY, undefined);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function showTransientStatus(message: string, ctx: StatusNotificationContext): boolean {
|
|
63
|
+
if ((ctx.mode !== "tui" && ctx.mode !== "rpc") || typeof ctx.ui.setWidget !== "function") return false;
|
|
64
|
+
// RPC cannot render component factories, so keep its plain text protocol unchanged.
|
|
65
|
+
if (ctx.mode === "rpc") {
|
|
66
|
+
ctx.ui.setWidget(STATUS_WIDGET_KEY, [message], { placement: "aboveEditor" });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
// Use the active Pi theme's dim status text instead of the terminal's foreground.
|
|
70
|
+
// Keep the report in one component so Pi's widget line cap does not truncate it.
|
|
71
|
+
const widgetMessage = `${message}\n`;
|
|
72
|
+
ctx.ui.setWidget(
|
|
73
|
+
STATUS_WIDGET_KEY,
|
|
74
|
+
(_tui, theme) => ({
|
|
75
|
+
render(width: number): string[] {
|
|
76
|
+
if (width <= 0) return [];
|
|
77
|
+
const padding = width > 2 ? " " : "";
|
|
78
|
+
const contentWidth = Math.max(1, width - padding.length * 2);
|
|
79
|
+
return widgetMessage.split("\n").flatMap((line) => {
|
|
80
|
+
if (line === "") return [""];
|
|
81
|
+
return wrapTextWithAnsi(theme.fg("dim", line), contentWidth).map(
|
|
82
|
+
(chunk) => `${padding}${chunk}${padding}`,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
invalidate() {},
|
|
87
|
+
}),
|
|
88
|
+
{ placement: "aboveEditor" },
|
|
89
|
+
);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function compareAdapterIds(left: { id: string }, right: { id: string }): number {
|
|
94
|
+
return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function scheduleModelCatalogRefresh(ctx: Pick<ExtensionContext, "modelRegistry">, reason: string): void {
|
|
98
|
+
if (reason !== "startup" && reason !== "reload") return;
|
|
99
|
+
const refresh = ctx.modelRegistry?.refresh;
|
|
100
|
+
if (typeof refresh !== "function") return;
|
|
101
|
+
void Promise.resolve()
|
|
102
|
+
.then(() => refresh.call(ctx.modelRegistry))
|
|
103
|
+
.catch(() => undefined);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ProviderKitRuntimeController {
|
|
107
|
+
resetForSession(): void;
|
|
108
|
+
updateOfficialPricing?(snapshot: Record<string, OfficialModelMeta>): void;
|
|
109
|
+
shutdown(): void;
|
|
110
|
+
clearStatusPresentation(ctx: Pick<ExtensionContext, "ui">): void;
|
|
111
|
+
applyTunerPayload(payload: unknown, model: ActiveModel): unknown | undefined | Promise<unknown | undefined>;
|
|
112
|
+
handleModelSelect(model: ActiveModel, ctx: Pick<ExtensionContext, "modelRegistry" | "ui">): void;
|
|
113
|
+
handleStatusCommand(args: string, ctx: ExtensionContext): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createNativeProviderRegistry(modelRegistry: ExtensionContext["modelRegistry"]): NativeProviderRegistry {
|
|
117
|
+
return modelRegistry as unknown as NativeProviderRegistry;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cloneProviderCost(cost: ProviderCost): ProviderCost {
|
|
121
|
+
return {
|
|
122
|
+
...cost,
|
|
123
|
+
...(cost.tiers ? { tiers: cost.tiers.map((tier) => ({ ...tier })) } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getOfficialMetadataStatus(
|
|
128
|
+
snapshot: Record<string, OfficialModelMeta>,
|
|
129
|
+
runtime: ProviderKitDependencies,
|
|
130
|
+
): ModelMetadataStatus | undefined {
|
|
131
|
+
const source = runtime.officialPricingUrl === OPENROUTER_MODELS_URL ? "AA/OpenRouter" : "Official metadata";
|
|
132
|
+
if (!runtime.enableOfficialPricingFallback && Object.keys(snapshot).length === 0) return undefined;
|
|
133
|
+
if (Object.keys(snapshot).length === 0) return { state: "unavailable", source };
|
|
134
|
+
const now = runtime.now();
|
|
135
|
+
const age = getPricingCacheAge(runtime.officialPricingUrl, now);
|
|
136
|
+
const updatedAt = age === undefined ? now : now - age;
|
|
137
|
+
return {
|
|
138
|
+
state: age !== undefined && age >= runtime.officialPricingCacheTtlMs ? "stale" : "fresh",
|
|
139
|
+
updatedAt,
|
|
140
|
+
source,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function hasKnownNativeCost(cost: ProviderCost | undefined): cost is ProviderCost {
|
|
145
|
+
if (!cost) return false;
|
|
146
|
+
return (
|
|
147
|
+
[cost.input, cost.output, cost.cacheRead, cost.cacheWrite].some((rate) => Number.isFinite(rate) && rate > 0) ||
|
|
148
|
+
(cost.tiers?.some((tier) =>
|
|
149
|
+
[tier.input, tier.output, tier.cacheRead, tier.cacheWrite].some((rate) => Number.isFinite(rate) && rate > 0),
|
|
150
|
+
) ??
|
|
151
|
+
false)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Build report-only metadata; never merge external fields into Pi's native model. */
|
|
156
|
+
function getNativeModelMetadata(
|
|
157
|
+
model: ActiveModel,
|
|
158
|
+
officialPricing: Record<string, OfficialModelMeta>,
|
|
159
|
+
): ProviderModelMetadata {
|
|
160
|
+
const officialMeta =
|
|
161
|
+
findOfficialMeta(`${model.provider}/${model.id}`, officialPricing) ?? findOfficialMeta(model.id, officialPricing);
|
|
162
|
+
const knownPrice = hasKnownNativeCost(model.cost);
|
|
163
|
+
return {
|
|
164
|
+
pricing: {
|
|
165
|
+
known: knownPrice,
|
|
166
|
+
source: "native",
|
|
167
|
+
...(knownPrice
|
|
168
|
+
? { baseCost: cloneProviderCost(model.cost), effectiveCost: cloneProviderCost(model.cost) }
|
|
169
|
+
: {}),
|
|
170
|
+
},
|
|
171
|
+
fieldSources: {
|
|
172
|
+
contextWindow: "native",
|
|
173
|
+
maxTokens: "native",
|
|
174
|
+
input: "native",
|
|
175
|
+
reasoning: "native",
|
|
176
|
+
},
|
|
177
|
+
...(officialMeta?.quality
|
|
178
|
+
? {
|
|
179
|
+
quality: officialMeta.quality.map((score) => ({
|
|
180
|
+
...score,
|
|
181
|
+
...(score.confidenceInterval ? { confidenceInterval: { ...score.confidenceInterval } } : {}),
|
|
182
|
+
})),
|
|
183
|
+
}
|
|
184
|
+
: {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function installProviderKitRuntime(
|
|
189
|
+
pi: ExtensionAPI,
|
|
190
|
+
runtime: ProviderKitDependencies,
|
|
191
|
+
definition: ProviderKitDefinition,
|
|
192
|
+
officialPricing: Record<string, OfficialModelMeta> = {},
|
|
193
|
+
options: {
|
|
194
|
+
registerHandlers?: boolean;
|
|
195
|
+
providerDrafts?: ReadonlyMap<ProviderAdapter, ProviderModelDraft[]>;
|
|
196
|
+
} = {},
|
|
197
|
+
): ProviderKitRuntimeController {
|
|
198
|
+
validateProviderKitDefinition(definition);
|
|
199
|
+
const registerHandlers = options.registerHandlers ?? true;
|
|
200
|
+
let currentOfficialPricing = officialPricing;
|
|
201
|
+
let currentOfficialMetadataStatus = getOfficialMetadataStatus(officialPricing, runtime);
|
|
202
|
+
const providers = [...definition.providers].sort(compareAdapterIds);
|
|
203
|
+
const statuses = [...(definition.statuses ?? [])].sort(compareAdapterIds);
|
|
204
|
+
const preflights = [...(definition.preflights ?? [])].sort(compareAdapterIds);
|
|
205
|
+
const tuners = sortTunerAdapters(definition.tuners ?? []);
|
|
206
|
+
|
|
207
|
+
for (const adapter of providers) {
|
|
208
|
+
registerProviderAdapter(pi, adapter, runtime, officialPricing, options.providerDrafts?.get(adapter));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const statusManager = new StatusManager(statuses, runtime.fetch, runtime.now);
|
|
212
|
+
const preflightManager = new PreflightManager(preflights, runtime.fetch, runtime.now);
|
|
213
|
+
const liveCheckManager = new LiveCheckManager(runtime.liveCheckRequestTimeoutMs, runtime.fetch, runtime.now);
|
|
214
|
+
let lifecycleGeneration = 0;
|
|
215
|
+
let statusPresentationGeneration = 0;
|
|
216
|
+
let statusPresentationVisible = false;
|
|
217
|
+
const clearStatusPresentation = (ctx: Pick<ExtensionContext, "ui">): void => {
|
|
218
|
+
statusPresentationGeneration++;
|
|
219
|
+
if (!statusPresentationVisible) return;
|
|
220
|
+
statusPresentationVisible = false;
|
|
221
|
+
clearTransientStatus(ctx);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const getStatusDetails = (model: ActiveModel, ctx: Pick<ExtensionContext, "modelRegistry">) => {
|
|
225
|
+
const provider = providers.find(({ id }) => id === model.provider);
|
|
226
|
+
const native = resolveNativeProvider(createNativeProviderRegistry(ctx.modelRegistry), model.provider);
|
|
227
|
+
return {
|
|
228
|
+
provider,
|
|
229
|
+
metadataStatus: currentOfficialMetadataStatus,
|
|
230
|
+
modelMetadata:
|
|
231
|
+
provider?.registration?.modelMetadata?.[model.id] ??
|
|
232
|
+
(provider === undefined ? getNativeModelMetadata(model, currentOfficialPricing) : undefined),
|
|
233
|
+
status: statuses.find(({ providerId }) => providerId === model.provider),
|
|
234
|
+
preflight: preflights.find(({ providerId }) => providerId === model.provider),
|
|
235
|
+
nativeProvider: native.provider,
|
|
236
|
+
nativeLookupAvailable: native.available,
|
|
237
|
+
nativePreflight:
|
|
238
|
+
provider === undefined && native.available
|
|
239
|
+
? {
|
|
240
|
+
providerAvailable: native.provider !== undefined,
|
|
241
|
+
modelMatched: nativeModelMatches(native.provider, model.id),
|
|
242
|
+
}
|
|
243
|
+
: undefined,
|
|
244
|
+
auth: ctx.modelRegistry.getProviderAuthStatus(model.provider),
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const notifyProviderStatus = (
|
|
249
|
+
model: ActiveModel,
|
|
250
|
+
ctx: StatusNotificationContext,
|
|
251
|
+
generation: number,
|
|
252
|
+
options: {
|
|
253
|
+
presentationGeneration?: number;
|
|
254
|
+
liveCheckRequested?: boolean;
|
|
255
|
+
showLiveCheckScope?: boolean;
|
|
256
|
+
} = {},
|
|
257
|
+
): void => {
|
|
258
|
+
if (
|
|
259
|
+
generation !== lifecycleGeneration ||
|
|
260
|
+
(options.presentationGeneration !== undefined &&
|
|
261
|
+
options.presentationGeneration !== statusPresentationGeneration)
|
|
262
|
+
)
|
|
263
|
+
return;
|
|
264
|
+
const {
|
|
265
|
+
provider,
|
|
266
|
+
modelMetadata,
|
|
267
|
+
status,
|
|
268
|
+
preflight,
|
|
269
|
+
nativeProvider,
|
|
270
|
+
nativeLookupAvailable,
|
|
271
|
+
nativePreflight,
|
|
272
|
+
auth,
|
|
273
|
+
metadataStatus,
|
|
274
|
+
} = getStatusDetails(model, ctx);
|
|
275
|
+
const diagnostics = status ? statusManager.getDiagnostics(model.provider) : undefined;
|
|
276
|
+
const preflightDiagnostics = preflightManager.getDiagnostics(model.provider, model.id);
|
|
277
|
+
const liveCheckDiagnostics = liveCheckManager.getDiagnostics(model.provider, model.id);
|
|
278
|
+
const report = formatProviderStatus(
|
|
279
|
+
model,
|
|
280
|
+
provider,
|
|
281
|
+
status,
|
|
282
|
+
preflight,
|
|
283
|
+
nativePreflight,
|
|
284
|
+
auth,
|
|
285
|
+
diagnostics,
|
|
286
|
+
preflightDiagnostics,
|
|
287
|
+
liveCheckDiagnostics,
|
|
288
|
+
nativeProvider,
|
|
289
|
+
nativeLookupAvailable,
|
|
290
|
+
runtime.now(),
|
|
291
|
+
{
|
|
292
|
+
liveCheckRequested: options.liveCheckRequested,
|
|
293
|
+
showLiveCheckScope:
|
|
294
|
+
options.showLiveCheckScope ??
|
|
295
|
+
(liveCheckDiagnostics?.snapshot !== undefined ||
|
|
296
|
+
liveCheckDiagnostics?.pending === true ||
|
|
297
|
+
liveCheckDiagnostics?.lastError !== undefined),
|
|
298
|
+
modelMetadata,
|
|
299
|
+
metadataStatus,
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
const message = report.report;
|
|
303
|
+
if (report.warningLevel !== "hard") {
|
|
304
|
+
statusPresentationVisible = showTransientStatus(message, ctx);
|
|
305
|
+
if (!statusPresentationVisible) ctx.ui.notify(message, "info");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
statusPresentationVisible = false;
|
|
309
|
+
ctx.ui.notify(report.report, "warning");
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const handleModelSelect = (_model: ActiveModel, ctx: Pick<ExtensionContext, "modelRegistry" | "ui">): void => {
|
|
313
|
+
lifecycleGeneration++;
|
|
314
|
+
clearStatusPresentation(ctx);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const handleStatusCommand = async (args: string, ctx: ExtensionContext): Promise<void> => {
|
|
318
|
+
clearStatusPresentation(ctx);
|
|
319
|
+
const generation = lifecycleGeneration;
|
|
320
|
+
const presentationGeneration = statusPresentationGeneration;
|
|
321
|
+
const model = ctx.model;
|
|
322
|
+
if (!model) {
|
|
323
|
+
ctx.ui.notify("No active model", "warning");
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const mode = parseStatusMode(args);
|
|
327
|
+
if (mode === undefined) {
|
|
328
|
+
ctx.ui.notify("Usage: /status [refresh|check]", "warning");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const { status, preflight, auth } = getStatusDetails(model, ctx);
|
|
332
|
+
let liveCheckRequested = false;
|
|
333
|
+
if ((mode === "refresh" || mode === "check") && auth.configured) {
|
|
334
|
+
const statusContext: StatusContextLike = {
|
|
335
|
+
model,
|
|
336
|
+
modelRegistry: ctx.modelRegistry,
|
|
337
|
+
getCredentialKey: () => ctx.modelRegistry.getApiKeyForProvider(model.provider),
|
|
338
|
+
getCredentialMetadata: () => readProviderCredentialMetadata(model.provider),
|
|
339
|
+
};
|
|
340
|
+
const preflightContext: PreflightContextLike = { model, modelRegistry: ctx.modelRegistry };
|
|
341
|
+
const refreshChecks: Array<Promise<unknown>> = [];
|
|
342
|
+
if (status) refreshChecks.push(statusManager.update(statusContext, { force: true }));
|
|
343
|
+
if (preflight) refreshChecks.push(preflightManager.update(preflightContext, { force: true }));
|
|
344
|
+
|
|
345
|
+
let liveCheck: Promise<LiveCheckResult | undefined> = Promise.resolve(undefined);
|
|
346
|
+
if (mode === "check") {
|
|
347
|
+
liveCheckRequested = true;
|
|
348
|
+
liveCheck = liveCheckManager.check({
|
|
349
|
+
model,
|
|
350
|
+
modelRegistry: ctx.modelRegistry,
|
|
351
|
+
...(tuners.length > 0
|
|
352
|
+
? {
|
|
353
|
+
onPayload: (payload, liveCheckModel) =>
|
|
354
|
+
applyTunerAdapters(payload, { model: liveCheckModel }, tuners),
|
|
355
|
+
}
|
|
356
|
+
: {}),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
// Account status, free preflight, and the live check are independent requests.
|
|
360
|
+
// Start all of them before awaiting any result so /status check waits only for the slowest one.
|
|
361
|
+
await Promise.all([Promise.all(refreshChecks), liveCheck]);
|
|
362
|
+
if (generation !== lifecycleGeneration) return;
|
|
363
|
+
}
|
|
364
|
+
if (generation !== lifecycleGeneration) return;
|
|
365
|
+
notifyProviderStatus(model, ctx, generation, {
|
|
366
|
+
presentationGeneration,
|
|
367
|
+
liveCheckRequested,
|
|
368
|
+
showLiveCheckScope: mode === "check",
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const resetForSession = (): void => {
|
|
373
|
+
lifecycleGeneration++;
|
|
374
|
+
statusPresentationGeneration++;
|
|
375
|
+
statusPresentationVisible = false;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const shutdown = (): void => {
|
|
379
|
+
lifecycleGeneration++;
|
|
380
|
+
statusPresentationGeneration++;
|
|
381
|
+
statusPresentationVisible = false;
|
|
382
|
+
statusManager.cancelAll();
|
|
383
|
+
statusManager.clear();
|
|
384
|
+
preflightManager.cancelAll();
|
|
385
|
+
preflightManager.clear();
|
|
386
|
+
liveCheckManager.cancelAll();
|
|
387
|
+
liveCheckManager.clear();
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const controller: ProviderKitRuntimeController = {
|
|
391
|
+
resetForSession,
|
|
392
|
+
updateOfficialPricing(snapshot) {
|
|
393
|
+
currentOfficialPricing = snapshot;
|
|
394
|
+
currentOfficialMetadataStatus = getOfficialMetadataStatus(snapshot, runtime);
|
|
395
|
+
},
|
|
396
|
+
shutdown,
|
|
397
|
+
clearStatusPresentation,
|
|
398
|
+
applyTunerPayload(payload, model) {
|
|
399
|
+
if (tuners.length === 0) return undefined;
|
|
400
|
+
return applyTunerAdapters(payload, { model }, tuners);
|
|
401
|
+
},
|
|
402
|
+
handleModelSelect,
|
|
403
|
+
handleStatusCommand,
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
if (registerHandlers) {
|
|
407
|
+
pi.on("before_provider_request", async (event, ctx) => {
|
|
408
|
+
if (!ctx.model) return;
|
|
409
|
+
return controller.applyTunerPayload(event.payload, ctx.model);
|
|
410
|
+
});
|
|
411
|
+
pi.on("input", (_event, ctx) => controller.clearStatusPresentation(ctx));
|
|
412
|
+
pi.on("session_start", (event, ctx) => {
|
|
413
|
+
resetForSession();
|
|
414
|
+
scheduleModelCatalogRefresh(ctx, event.reason);
|
|
415
|
+
});
|
|
416
|
+
pi.on("model_select", (event, ctx) => handleModelSelect(event.model, ctx));
|
|
417
|
+
pi.on("session_shutdown", () => shutdown());
|
|
418
|
+
pi.registerCommand("status", {
|
|
419
|
+
description: "Show status and diagnostics for the active provider",
|
|
420
|
+
getArgumentCompletions: getStatusModeCompletions,
|
|
421
|
+
handler: async (args, ctx) => handleStatusCommand(args, ctx),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return controller;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function createProviderKitRuntime(
|
|
429
|
+
loadDefinition: ProviderKitLoader,
|
|
430
|
+
dependencies: Partial<ProviderKitDependencies> = {},
|
|
431
|
+
): (pi: ExtensionAPI) => Promise<void> {
|
|
432
|
+
const runtime = resolveProviderKitDependencies(dependencies);
|
|
433
|
+
return async (pi) => {
|
|
434
|
+
let latestBackgroundPricing: Record<string, OfficialModelMeta> | undefined;
|
|
435
|
+
let installedDefinition: ProviderKitDefinition | undefined;
|
|
436
|
+
let installedController: ProviderKitRuntimeController | undefined;
|
|
437
|
+
let disposed = false;
|
|
438
|
+
const onBackgroundRefresh = (snapshot: Record<string, OfficialModelMeta>): void => {
|
|
439
|
+
latestBackgroundPricing = snapshot;
|
|
440
|
+
if (disposed) return;
|
|
441
|
+
installedController?.updateOfficialPricing?.(snapshot);
|
|
442
|
+
if (installedDefinition === undefined) return;
|
|
443
|
+
refreshProviderRegistrations(pi, installedDefinition.providers, runtime, snapshot);
|
|
444
|
+
};
|
|
445
|
+
const officialPricingPromise = runtime.enableOfficialPricingFallback
|
|
446
|
+
? fetchOfficialPricing(
|
|
447
|
+
runtime.fetch,
|
|
448
|
+
runtime.officialPricingUrl,
|
|
449
|
+
runtime.officialPricingTimeoutMs,
|
|
450
|
+
runtime.officialPricingCacheTtlMs,
|
|
451
|
+
runtime.officialPricingMaxStaleMs,
|
|
452
|
+
runtime.now,
|
|
453
|
+
{
|
|
454
|
+
cachePath:
|
|
455
|
+
runtime.officialPricingUrl === OPENROUTER_MODELS_URL
|
|
456
|
+
? runtime.openRouterMetadataCachePath
|
|
457
|
+
: undefined,
|
|
458
|
+
background: runtime.officialPricingUrl === OPENROUTER_MODELS_URL,
|
|
459
|
+
onBackgroundRefresh: onBackgroundRefresh,
|
|
460
|
+
},
|
|
461
|
+
)
|
|
462
|
+
: Promise.resolve({});
|
|
463
|
+
const definitionPromise = loadDefinition(runtime);
|
|
464
|
+
const [officialPricing, definition] = await Promise.all([officialPricingPromise, definitionPromise]);
|
|
465
|
+
validateProviderKitDefinition(definition);
|
|
466
|
+
installedController = installProviderKitRuntime(pi, runtime, definition, officialPricing);
|
|
467
|
+
installedDefinition = definition;
|
|
468
|
+
if (latestBackgroundPricing !== undefined) onBackgroundRefresh(latestBackgroundPricing);
|
|
469
|
+
pi.on("session_shutdown", () => {
|
|
470
|
+
disposed = true;
|
|
471
|
+
});
|
|
472
|
+
};
|
|
473
|
+
}
|