@oh-my-pi/pi-catalog 18.13.2 → 18.13.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/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/dist/types/discovery/codex.d.ts +4 -5
- package/package.json +5 -5
- package/src/compat/rules/classes/openai.kdl +2 -2
- package/src/compat/rules/providers/factory-droid.kdl +24 -0
- package/src/compat/rules/providers/kimi-code.kdl +6 -0
- package/src/compat/rules/runtime/behavior.kdl +4 -4
- package/src/compat/rules.json +1 -1
- package/src/discovery/codex.ts +5 -6
- package/src/models.json +1 -1
- package/src/provider-models/cache-provider-id.ts +23 -6
- package/src/provider-models/openai-compat.ts +83 -23
- package/src/provider-models/special.ts +5 -6
|
@@ -26,6 +26,9 @@ const CREDENTIAL_SCOPED_MODEL_CACHE_PROVIDERS: Readonly<Record<string, true>> =
|
|
|
26
26
|
// than from the synchronous, credential-less startup read.
|
|
27
27
|
"singularityapi-dev": true,
|
|
28
28
|
"singularityapi-tech": true,
|
|
29
|
+
// Kimi Code's roster is plan-gated and discovery is authoritative, so a
|
|
30
|
+
// shared namespace would serve one plan's ids to another plan's credential.
|
|
31
|
+
"kimi-code": true,
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
/** Whether a provider's model-cache namespace requires its resolved credential. */
|
|
@@ -37,6 +40,8 @@ export function getDefaultModelDiscoveryBaseUrl(providerId: string): string | un
|
|
|
37
40
|
switch (providerId) {
|
|
38
41
|
case "charm-hyper":
|
|
39
42
|
return CHARM_HYPER_API_BASE_URL;
|
|
43
|
+
case "kimi-code":
|
|
44
|
+
return "https://api.kimi.com/coding/v1";
|
|
40
45
|
case "meta":
|
|
41
46
|
case "muse-code":
|
|
42
47
|
return "https://api.meta.ai/v1";
|
|
@@ -71,11 +76,11 @@ export function resolveOllamaModelCacheProviderId(providerId: string, baseUrl?:
|
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/**
|
|
74
|
-
* The `sub` claim of a
|
|
75
|
-
* that
|
|
79
|
+
* The `sub` claim of a JWT access token. Cursor and Kimi Code access tokens
|
|
80
|
+
* are JWTs that refresh rotates (new `exp`/`iat`/`jti`) for the same account,
|
|
76
81
|
* so the subject is the stable account scope. Non-JWT keys return undefined.
|
|
77
82
|
*/
|
|
78
|
-
function
|
|
83
|
+
function jwtCredentialSubject(apiKey: string): string | undefined {
|
|
79
84
|
const parts = apiKey.split(".");
|
|
80
85
|
if (parts.length !== 3 || !parts[1]) return undefined;
|
|
81
86
|
try {
|
|
@@ -112,7 +117,7 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
|
|
|
112
117
|
// mark the account default `isProviderDefault`, which startup now honors.
|
|
113
118
|
const baseUrl = (options.baseUrl ?? CURSOR_DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
114
119
|
const apiKey = options.apiKey ?? "";
|
|
115
|
-
const scope = `${
|
|
120
|
+
const scope = `${jwtCredentialSubject(apiKey) ?? apiKey}\u0000${baseUrl}`;
|
|
116
121
|
return `cursor:rich-models-v5:${Bun.hash(scope).toString(36)}`;
|
|
117
122
|
}
|
|
118
123
|
case "charm-hyper": {
|
|
@@ -132,6 +137,17 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
|
|
|
132
137
|
// `/v1`-suffixed one; both must land on one namespace.
|
|
133
138
|
return `charm-hyper:models-v1:${Bun.hash(normalizeCharmHyperBaseUrl(options.baseUrl)).toString(36)}`;
|
|
134
139
|
}
|
|
140
|
+
case "kimi-code": {
|
|
141
|
+
// The `/coding/v1/models` roster depends on the subscription plan behind
|
|
142
|
+
// the credential, and discovery is authoritative: a namespace without the
|
|
143
|
+
// credential would serve the previous plan's roster for the full TTL,
|
|
144
|
+
// including ids the current plan cannot call. OAuth access tokens rotate on
|
|
145
|
+
// refresh, so the scope is the token's account subject, not the token.
|
|
146
|
+
const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
|
|
147
|
+
const apiKey = options.apiKey ?? "";
|
|
148
|
+
const scope = `${jwtCredentialSubject(apiKey) ?? apiKey}\u0000${baseUrl}`;
|
|
149
|
+
return `kimi-code:models-v1:${Bun.hash(scope).toString(36)}`;
|
|
150
|
+
}
|
|
135
151
|
case "muse-code": {
|
|
136
152
|
const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
|
|
137
153
|
const scope = `${options.apiKey ?? ""}\u0000${baseUrl}`;
|
|
@@ -160,7 +176,8 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
|
|
|
160
176
|
}
|
|
161
177
|
case "litellm": {
|
|
162
178
|
const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
|
|
163
|
-
// rich-
|
|
179
|
+
// rich-v14 fetches every /v2/model/info page rather than caching only
|
|
180
|
+
// its first 50 models. rich-v13 moves `mode: decisions` groups off chat.
|
|
164
181
|
// rich-v12 invalidates namespaced ids that missed bare catalog references.
|
|
165
182
|
// rich-v11 excluded ClinePass gateway metadata (issue #10932). rich-v10
|
|
166
183
|
// filtered known non-conversational LiteLLM modes, unioned compat across
|
|
@@ -168,7 +185,7 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
|
|
|
168
185
|
// declaration into it; earlier versions invalidated rows whose
|
|
169
186
|
// `compatConfig` retained a colliding bundled model's provider-specific
|
|
170
187
|
// transport (e.g. Fireworks `wireModelIdMode`) (issue #9938).
|
|
171
|
-
return `litellm:rich-
|
|
188
|
+
return `litellm:rich-v14:${Bun.hash(baseUrl).toString(36)}`;
|
|
172
189
|
}
|
|
173
190
|
case "gmi-cloud":
|
|
174
191
|
case "siliconflow":
|
|
@@ -4221,9 +4221,11 @@ export function kimiCodeModelManagerOptions(
|
|
|
4221
4221
|
config?: KimiCodeModelManagerConfig,
|
|
4222
4222
|
): ModelManagerOptions<"openai-completions"> {
|
|
4223
4223
|
const apiKey = config?.apiKey;
|
|
4224
|
-
const baseUrl = config?.baseUrl ?? "
|
|
4224
|
+
const baseUrl = config?.baseUrl ?? getDefaultModelDiscoveryBaseUrl("kimi-code")!;
|
|
4225
4225
|
return {
|
|
4226
4226
|
providerId: "kimi-code",
|
|
4227
|
+
cacheProviderId: resolveModelCacheProviderId("kimi-code", { apiKey, baseUrl }),
|
|
4228
|
+
dynamicModelsAuthoritative: true,
|
|
4227
4229
|
...(apiKey && {
|
|
4228
4230
|
fetchDynamicModels: () =>
|
|
4229
4231
|
fetchOpenAICompatibleModels({
|
|
@@ -5678,7 +5680,7 @@ type LiteLLMRichEndpointModel<TApi extends Api> = {
|
|
|
5678
5680
|
};
|
|
5679
5681
|
type LiteLLMRichEndpointFailure = {
|
|
5680
5682
|
endpoint: string;
|
|
5681
|
-
reason: "http-status" | "invalid-json" | "network-error";
|
|
5683
|
+
reason: "http-status" | "invalid-json" | "invalid-pagination" | "network-error";
|
|
5682
5684
|
status?: number;
|
|
5683
5685
|
error?: unknown;
|
|
5684
5686
|
};
|
|
@@ -5691,6 +5693,8 @@ type LiteLLMRichEndpointResult<TApi extends Api> =
|
|
|
5691
5693
|
| { failure: LiteLLMRichEndpointFailure };
|
|
5692
5694
|
|
|
5693
5695
|
const LITELLM_RICH_ENDPOINTS = ["/model_group/info", "/v2/model/info", "/model/info", "/v1/model/info"] as const;
|
|
5696
|
+
// Bound paginated management responses before trusting their advertised page count.
|
|
5697
|
+
const LITELLM_MAX_RICH_PAGES = 100;
|
|
5694
5698
|
export const OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
5695
5699
|
export const OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS = 32_768;
|
|
5696
5700
|
const UNKNOWN_PROXY_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;
|
|
@@ -6170,29 +6174,83 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
|
6170
6174
|
if (options.apiKey) {
|
|
6171
6175
|
requestHeaders.Authorization = `Bearer ${options.apiKey}`;
|
|
6172
6176
|
}
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6177
|
+
const fetchPage = async (
|
|
6178
|
+
page?: number,
|
|
6179
|
+
): Promise<{ payload: unknown; status: number } | { failure: LiteLLMRichEndpointFailure } | null> => {
|
|
6180
|
+
let response: Response;
|
|
6181
|
+
try {
|
|
6182
|
+
response = await fetchImpl(`${managementBaseUrl}${endpoint}${page === undefined ? "" : `?page=${page}`}`, {
|
|
6183
|
+
method: "GET",
|
|
6184
|
+
headers: requestHeaders,
|
|
6185
|
+
signal,
|
|
6186
|
+
});
|
|
6187
|
+
} catch (error) {
|
|
6188
|
+
return { failure: { endpoint, reason: "network-error", error } };
|
|
6189
|
+
}
|
|
6190
|
+
if (!response.ok) {
|
|
6191
|
+
return response.status === 404 && page === undefined
|
|
6192
|
+
? null
|
|
6193
|
+
: { failure: { endpoint, reason: "http-status", status: response.status } };
|
|
6194
|
+
}
|
|
6195
|
+
try {
|
|
6196
|
+
return { payload: await response.json(), status: response.status };
|
|
6197
|
+
} catch (error) {
|
|
6198
|
+
return { failure: { endpoint, reason: "invalid-json", status: response.status, error } };
|
|
6199
|
+
}
|
|
6200
|
+
};
|
|
6201
|
+
const firstPage = await fetchPage();
|
|
6202
|
+
if (!firstPage || "failure" in firstPage) {
|
|
6203
|
+
return firstPage;
|
|
6191
6204
|
}
|
|
6192
|
-
const entries = extractLiteLLMRichEntries(payload);
|
|
6205
|
+
const entries = extractLiteLLMRichEntries(firstPage.payload);
|
|
6193
6206
|
if (!entries || entries.length === 0) {
|
|
6194
6207
|
return null;
|
|
6195
6208
|
}
|
|
6209
|
+
// /v2/model/info returns {current_page, total_pages, data}; older
|
|
6210
|
+
// endpoints and unpaginated /v2 proxies must keep their existing behavior.
|
|
6211
|
+
if (
|
|
6212
|
+
endpoint === "/v2/model/info" &&
|
|
6213
|
+
isRecord(firstPage.payload) &&
|
|
6214
|
+
("current_page" in firstPage.payload || "total_pages" in firstPage.payload)
|
|
6215
|
+
) {
|
|
6216
|
+
const { current_page: currentPage, total_pages: totalPages, total_count: totalCount } = firstPage.payload;
|
|
6217
|
+
const invalidPage = (status: number): LiteLLMRichEndpointResult<TApi> => ({
|
|
6218
|
+
failure: { endpoint, reason: "invalid-pagination", status },
|
|
6219
|
+
});
|
|
6220
|
+
if (
|
|
6221
|
+
currentPage !== 1 ||
|
|
6222
|
+
typeof totalPages !== "number" ||
|
|
6223
|
+
!Number.isSafeInteger(totalPages) ||
|
|
6224
|
+
totalPages < 1 ||
|
|
6225
|
+
totalPages > LITELLM_MAX_RICH_PAGES ||
|
|
6226
|
+
(totalCount !== undefined &&
|
|
6227
|
+
(typeof totalCount !== "number" || !Number.isSafeInteger(totalCount) || totalCount < entries.length))
|
|
6228
|
+
) {
|
|
6229
|
+
return invalidPage(firstPage.status);
|
|
6230
|
+
}
|
|
6231
|
+
const remainingPages = await Promise.all(
|
|
6232
|
+
Array.from({ length: totalPages - 1 }, (_, index) => fetchPage(index + 2)),
|
|
6233
|
+
);
|
|
6234
|
+
for (const [index, nextPage] of remainingPages.entries()) {
|
|
6235
|
+
if (!nextPage || "failure" in nextPage) {
|
|
6236
|
+
return nextPage ?? invalidPage(firstPage.status);
|
|
6237
|
+
}
|
|
6238
|
+
const nextEntries = extractLiteLLMRichEntries(nextPage.payload);
|
|
6239
|
+
if (
|
|
6240
|
+
!isRecord(nextPage.payload) ||
|
|
6241
|
+
nextPage.payload.current_page !== index + 2 ||
|
|
6242
|
+
nextPage.payload.total_pages !== totalPages ||
|
|
6243
|
+
(totalCount !== undefined && nextPage.payload.total_count !== totalCount) ||
|
|
6244
|
+
!nextEntries?.length
|
|
6245
|
+
) {
|
|
6246
|
+
return invalidPage(nextPage.status);
|
|
6247
|
+
}
|
|
6248
|
+
entries.push(...nextEntries);
|
|
6249
|
+
}
|
|
6250
|
+
if (totalCount !== undefined && entries.length !== totalCount) {
|
|
6251
|
+
return invalidPage(firstPage.status);
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6196
6254
|
const deduped = new Map<string, LiteLLMRichEndpointModel<TApi>>();
|
|
6197
6255
|
const excludedModelIds = new Set<string>();
|
|
6198
6256
|
for (const entry of entries) {
|
|
@@ -6349,8 +6407,10 @@ export function litellmModelManagerOptions(config?: LiteLLMModelManagerConfig):
|
|
|
6349
6407
|
const baseUrl = config?.baseUrl ?? getDefaultModelDiscoveryBaseUrl("litellm")!;
|
|
6350
6408
|
return {
|
|
6351
6409
|
providerId: "litellm",
|
|
6352
|
-
// rich-
|
|
6353
|
-
//
|
|
6410
|
+
// rich-v14 fetches all /v2/model/info pages so cached proxy rosters
|
|
6411
|
+
// include models beyond page one. rich-v13 routes `mode: decisions`
|
|
6412
|
+
// groups to the Decisions runner as judge rows.
|
|
6413
|
+
// rich-v12 invalidates namespaced proxy ids that missed bare catalog
|
|
6354
6414
|
// references. rich-v11 excluded ClinePass gateway metadata (issue #10932).
|
|
6355
6415
|
// rich-v10 filtered known non-conversational LiteLLM modes, keyed the
|
|
6356
6416
|
// deployment's `supports_vision` declaration into cached compat, and
|
|
@@ -94,11 +94,10 @@ export function openaiCodexModelManagerOptions(
|
|
|
94
94
|
* {@link unionAccountCatalogs}.
|
|
95
95
|
*
|
|
96
96
|
* Returns `null` when any account's fetch failed transiently, so a partial list
|
|
97
|
-
* cannot replace the previous or bundled authoritative catalog.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* produced a catalog, discovery aborts the same way.
|
|
97
|
+
* cannot replace the previous or bundled authoritative catalog. A definitive
|
|
98
|
+
* account denial (401/403 auth or 402 payment) contributes nothing and is skipped
|
|
99
|
+
* instead: it would otherwise veto every sibling account's models until the
|
|
100
|
+
* account recovers. If no account produced a catalog, discovery aborts.
|
|
102
101
|
*/
|
|
103
102
|
function unionCodexModels(
|
|
104
103
|
results: readonly { accountId: string | undefined; result: CodexModelDiscoveryResult | null }[],
|
|
@@ -107,7 +106,7 @@ function unionCodexModels(
|
|
|
107
106
|
for (const { accountId, result } of results) {
|
|
108
107
|
if (!result) return null;
|
|
109
108
|
if (result.rejectedStatus !== undefined) {
|
|
110
|
-
logger.warn("Codex model discovery skipped an account
|
|
109
|
+
logger.warn("Codex model discovery skipped an account denied by the backend", {
|
|
111
110
|
accountId,
|
|
112
111
|
status: result.rejectedStatus,
|
|
113
112
|
});
|