pi-orchestrator-config 4.3.8 → 4.3.9
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 +184 -165
- package/agents/git-expert.md +30 -1
- package/extensions/openai-compatible-discovery/index.ts +204 -0
- package/extensions/pidiff/pidiff-ui/package.json +1 -1
- package/extensions/shared/openai-compatible-discovery.ts +558 -0
- package/package.json +2 -1
- package/rules/20-code-review-loop.md +11 -1
- package/settings-keys.json +1 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { AuthResult, Model, Provider } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { createRuntimeProvider } from "../shared/create-runtime-provider.js";
|
|
4
|
+
import { createLogger } from "../shared/logger.js";
|
|
5
|
+
import {
|
|
6
|
+
OpenAiCompatibleDiscoveryCache,
|
|
7
|
+
buildOpenAiCompatibleModelsRequest,
|
|
8
|
+
findEligibleOpenAiCompatibleProviderConfigsResult,
|
|
9
|
+
formatOpenAiCompatibleDiscoverySummary,
|
|
10
|
+
materializeOpenAiCompatibleModels,
|
|
11
|
+
openAiCompatibleConnectionFingerprint,
|
|
12
|
+
redactOpenAiCompatibleDiagnostic,
|
|
13
|
+
resolveStaticOpenAiCompatibleHeaders,
|
|
14
|
+
type OpenAiCompatibleModelRecord,
|
|
15
|
+
type ResolvedOpenAiCompatibleConnection,
|
|
16
|
+
} from "../shared/openai-compatible-discovery.js";
|
|
17
|
+
|
|
18
|
+
const log = createLogger("openai-compatible-discovery");
|
|
19
|
+
const REQUEST_TIMEOUT_MS = 20_000;
|
|
20
|
+
|
|
21
|
+
function responseRecords(payload: unknown): OpenAiCompatibleModelRecord[] {
|
|
22
|
+
if (!payload || typeof payload !== "object" || !Array.isArray((payload as { data?: unknown }).data))
|
|
23
|
+
throw new Error("OpenAI-compatible /v1/models response must contain a data array");
|
|
24
|
+
return (payload as { data: unknown[] }).data.flatMap((record) => {
|
|
25
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) return [];
|
|
26
|
+
const model = record as OpenAiCompatibleModelRecord;
|
|
27
|
+
return typeof model.id === "string" ? [model] : [];
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolvedConnection(source: Provider, auth?: AuthResult, staticHeaders?: Record<string, string>): ResolvedOpenAiCompatibleConnection {
|
|
32
|
+
return {
|
|
33
|
+
baseUrl: auth?.auth.baseUrl ?? source.baseUrl,
|
|
34
|
+
apiKey: auth?.auth.apiKey,
|
|
35
|
+
headers: { ...(source.headers ?? {}), ...(staticHeaders ?? {}), ...(auth?.auth.headers ?? {}) },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function combineModels(sourceProviderId: string, staticModels: readonly Model[], records: readonly OpenAiCompatibleModelRecord[], baseUrl: string): Model[] {
|
|
40
|
+
const existingIds = new Set(staticModels.map((model) => model.id));
|
|
41
|
+
const discovered = materializeOpenAiCompatibleModels(records, baseUrl, sourceProviderId)
|
|
42
|
+
.filter((model) => !existingIds.has(model.id));
|
|
43
|
+
return [...staticModels, ...discovered];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function discoverProvider(
|
|
47
|
+
pi: ExtensionAPI,
|
|
48
|
+
ctx: any,
|
|
49
|
+
sourceProviderId: string,
|
|
50
|
+
rawStaticHeaders: Record<string, string> | undefined,
|
|
51
|
+
cache: OpenAiCompatibleDiscoveryCache<OpenAiCompatibleModelRecord>,
|
|
52
|
+
signal: AbortSignal,
|
|
53
|
+
isCurrent: () => boolean,
|
|
54
|
+
registeredSources: Set<string>,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
if (!isCurrent()) return;
|
|
57
|
+
const source = ctx.modelRegistry.getProvider(sourceProviderId) as Provider | undefined;
|
|
58
|
+
if (!source) {
|
|
59
|
+
log.warn(`${sourceProviderId}: configured provider was not resolved`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let auth: AuthResult | undefined;
|
|
63
|
+
let staticHeaders: Record<string, string> | undefined;
|
|
64
|
+
try {
|
|
65
|
+
auth = await ctx.modelRegistry.getProviderAuth(sourceProviderId);
|
|
66
|
+
staticHeaders = await resolveStaticOpenAiCompatibleHeaders(rawStaticHeaders);
|
|
67
|
+
} catch {
|
|
68
|
+
log.warn(`${sourceProviderId}: authentication or configured headers could not be resolved`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const connection = resolvedConnection(source, auth, staticHeaders);
|
|
72
|
+
let request;
|
|
73
|
+
try {
|
|
74
|
+
request = buildOpenAiCompatibleModelsRequest(connection);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
log.warn(`${sourceProviderId}: discovery inactive`, redactOpenAiCompatibleDiagnostic(error instanceof Error ? error.message : String(error), connection));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const records = await cache.get(async () => {
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(request.url, {
|
|
82
|
+
headers: request.headers,
|
|
83
|
+
redirect: "error",
|
|
84
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]),
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) throw new Error(`OpenAI-compatible /v1/models returned HTTP ${response.status}`);
|
|
87
|
+
return responseRecords(await response.json());
|
|
88
|
+
} catch (error) {
|
|
89
|
+
log.warn(`${sourceProviderId}: discovery refresh failed`, redactOpenAiCompatibleDiagnostic(error instanceof Error ? error.message : String(error), connection));
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}, { cacheKey: openAiCompatibleConnectionFingerprint(connection) });
|
|
93
|
+
if (!isCurrent() || !records.hasSnapshot) return;
|
|
94
|
+
if (records.stale) log.warn(`${sourceProviderId}: refresh failed; using last-known-good discovery result`);
|
|
95
|
+
|
|
96
|
+
const staticModels = (ctx.modelRegistry.getAll() as Model[])
|
|
97
|
+
.filter((model) => model.provider === sourceProviderId);
|
|
98
|
+
const models = combineModels(sourceProviderId, staticModels, records.models, request.streamBaseUrl);
|
|
99
|
+
try {
|
|
100
|
+
const provider = await createRuntimeProvider({
|
|
101
|
+
id: sourceProviderId,
|
|
102
|
+
name: source.name,
|
|
103
|
+
baseUrl: request.streamBaseUrl,
|
|
104
|
+
auth: source.auth,
|
|
105
|
+
models,
|
|
106
|
+
api: {
|
|
107
|
+
stream: source.stream.bind(source),
|
|
108
|
+
streamSimple: source.streamSimple.bind(source),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
if (!isCurrent()) return;
|
|
112
|
+
// Pi composes this native provider with the existing models.json entry. The
|
|
113
|
+
// source key is therefore both the ordinary picker label and routing key.
|
|
114
|
+
pi.registerProvider(provider);
|
|
115
|
+
registeredSources.add(sourceProviderId);
|
|
116
|
+
try {
|
|
117
|
+
pi.appendEntry<OpenAiCompatibleDiscoverySummary>(
|
|
118
|
+
"openai-compatible-discovery-summary",
|
|
119
|
+
{ summary: formatOpenAiCompatibleDiscoverySummary(sourceProviderId, records.models.length) },
|
|
120
|
+
);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
log.warn(`${sourceProviderId}: discovery summary append failed`, error instanceof Error ? error.name : typeof error);
|
|
123
|
+
}
|
|
124
|
+
log.info(`${sourceProviderId}: registered ${records.models.length} discovered model(s) on configured provider`);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
log.warn(`${sourceProviderId}: provider augmentation failed`, redactOpenAiCompatibleDiagnostic(error instanceof Error ? error.message : String(error), connection));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface OpenAiCompatibleDiscoverySummary {
|
|
131
|
+
summary: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A renderer component without a runtime dependency on Pi's TUI package. */
|
|
135
|
+
function discoverySummaryComponent(summary: string, theme: { fg: (color: "muted", text: string) => string }) {
|
|
136
|
+
log.debug("rendering discovery summary", { summary });
|
|
137
|
+
return {
|
|
138
|
+
render: (_width: number): string[] => [theme.fg("muted", summary)],
|
|
139
|
+
invalidate: (): void => {},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default function (pi: ExtensionAPI) {
|
|
144
|
+
if (typeof pi.registerEntryRenderer === "function") {
|
|
145
|
+
pi.registerEntryRenderer<OpenAiCompatibleDiscoverySummary>(
|
|
146
|
+
"openai-compatible-discovery-summary",
|
|
147
|
+
(entry, _options, theme) => discoverySummaryComponent(entry.data?.summary ?? "", theme),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const cache = new OpenAiCompatibleDiscoveryCache<OpenAiCompatibleModelRecord>();
|
|
152
|
+
const registeredSources = new Set<string>();
|
|
153
|
+
const controllers = new Set<AbortController>();
|
|
154
|
+
let active = true;
|
|
155
|
+
let generation = 0;
|
|
156
|
+
|
|
157
|
+
const restoreSources = () => {
|
|
158
|
+
for (const id of registeredSources) {
|
|
159
|
+
try {
|
|
160
|
+
// Removing our native overlay makes Pi recompose the unchanged static provider.
|
|
161
|
+
pi.unregisterProvider(id);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
log.warn("provider augmentation cleanup failed", { provider: id, cause: error instanceof Error ? error.name : typeof error });
|
|
164
|
+
}
|
|
165
|
+
registeredSources.delete(id);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
pi.on("session_shutdown", () => {
|
|
170
|
+
active = false;
|
|
171
|
+
generation += 1;
|
|
172
|
+
for (const controller of controllers) controller.abort();
|
|
173
|
+
controllers.clear();
|
|
174
|
+
cache.clear();
|
|
175
|
+
restoreSources();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
179
|
+
if (ctx?.mode !== "tui" || ctx?.hasUI !== true) return;
|
|
180
|
+
active = true;
|
|
181
|
+
generation += 1;
|
|
182
|
+
for (const controller of controllers) controller.abort();
|
|
183
|
+
controllers.clear();
|
|
184
|
+
cache.clear();
|
|
185
|
+
restoreSources();
|
|
186
|
+
const currentGeneration = generation;
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
controllers.add(controller);
|
|
189
|
+
const isCurrent = () => active && currentGeneration === generation;
|
|
190
|
+
const configResult = findEligibleOpenAiCompatibleProviderConfigsResult();
|
|
191
|
+
if (configResult.providers.length === 0) {
|
|
192
|
+
const diagnostic = { modelsConfig: configResult.status };
|
|
193
|
+
if (configResult.status === "unreadable" || configResult.status === "malformed")
|
|
194
|
+
log.warn("inactive: OpenAI-compatible discovery configuration unavailable", diagnostic);
|
|
195
|
+
else log.debug("inactive: no opted-in OpenAI-compatible providers", diagnostic);
|
|
196
|
+
controllers.delete(controller);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
await Promise.all(configResult.providers.map(({ id, headers }) =>
|
|
200
|
+
discoverProvider(pi, ctx, id, headers, cache, controller.signal, isCurrent, registeredSources),
|
|
201
|
+
));
|
|
202
|
+
controllers.delete(controller);
|
|
203
|
+
});
|
|
204
|
+
}
|