@oh-my-pi/pi-catalog 17.0.2 → 17.0.4
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 +14 -0
- package/dist/types/types.d.ts +13 -6
- package/package.json +3 -3
- package/src/compat/openai.ts +9 -1
- package/src/model-cache.ts +3 -2
- package/src/provider-models/openai-compat.ts +134 -17
- package/src/types.ts +13 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.0.4] - 2026-07-18
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Kimi-family models now use MFJS tool schema on all hosts, including proxies like OpenRouter that forward schemas to Moonshot
|
|
10
|
+
|
|
11
|
+
## [17.0.3] - 2026-07-17
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Logged LiteLLM rich-metadata endpoint failures once with their endpoint and status before falling back to incomplete `/v1/models` data ([#5801](https://github.com/can1357/oh-my-pi/issues/5801)).
|
|
16
|
+
- Fixed authenticated Kimi Code discovery to preserve live effort levels, default effort, mandatory-thinking state, and per-model protocol metadata ([#5893](https://github.com/can1357/oh-my-pi/issues/5893)).
|
|
17
|
+
- Fixed LiteLLM provider ignoring per-model pricing: `mapLiteLLMRichEntry` now reads `input_cost_per_token` / `output_cost_per_token` (plus cache costs) from LiteLLM rich metadata and maps them to `cost.input` / `cost.output`, falling back to the bundled reference only when LiteLLM omits cost, so proxied models no longer display as free ([#5818](https://github.com/can1357/oh-my-pi/issues/5818)).
|
|
18
|
+
|
|
5
19
|
## [17.0.2] - 2026-07-17
|
|
6
20
|
|
|
7
21
|
### Changed
|
package/dist/types/types.d.ts
CHANGED
|
@@ -119,7 +119,7 @@ export interface Usage {
|
|
|
119
119
|
total: number;
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
|
-
export type OpenAIReasoningFormat = "openai" | "openrouter" | "zai" | "qwen" | "qwen-chat-template";
|
|
122
|
+
export type OpenAIReasoningFormat = "openai" | "openrouter" | "zai" | "kimi" | "qwen" | "qwen-chat-template";
|
|
123
123
|
export type OpenAIReasoningDisableMode = "omit" | "lowest-effort" | "openrouter-enabled-false" | "zai-thinking-disabled" | "qwen-enable-thinking-false" | "qwen-template-false";
|
|
124
124
|
export type OpenAIStreamMarkupHealingPattern = "kimi" | "dsml" | "thinking";
|
|
125
125
|
/**
|
|
@@ -167,8 +167,10 @@ export interface OpenAICompat {
|
|
|
167
167
|
requiresThinkingAsText?: boolean;
|
|
168
168
|
/** Whether tool call IDs must be normalized to Mistral format (exactly 9 alphanumeric chars). Default: auto-detected from URL. */
|
|
169
169
|
requiresMistralToolIds?: boolean;
|
|
170
|
-
/** Format for reasoning/thinking parameter. "
|
|
170
|
+
/** Format for reasoning/thinking parameter. `"kimi"` uses `thinking: { type, effort }`; other values select their provider-native reasoning fields. Default: `"openai"`. */
|
|
171
171
|
thinkingFormat?: OpenAIReasoningFormat;
|
|
172
|
+
/** Kimi Code transport selected by live per-model protocol metadata. User settings take precedence. */
|
|
173
|
+
kimiApiFormat?: "openai" | "anthropic";
|
|
172
174
|
/** Request-time disable encoding for the selected reasoning/thinking format. Default: derived from `thinkingFormat`. */
|
|
173
175
|
reasoningDisableMode?: OpenAIReasoningDisableMode;
|
|
174
176
|
/** Whether the provider rejects `reasoning.effort`/`reasoning_effort` even when the model reasons natively. Default: false unless reasoning effort is unsupported. */
|
|
@@ -274,8 +276,10 @@ export interface OpenAICompat {
|
|
|
274
276
|
* normalization (collapse `const`→`enum`, infer `type` on bare enums, strip
|
|
275
277
|
* unsupported validators/`prefixItems`) because Moonshot/Kimi native hosts
|
|
276
278
|
* reject standard JSON Schema constructs with HTTP 400. Default:
|
|
277
|
-
* auto-detected
|
|
278
|
-
*
|
|
279
|
+
* auto-detected — Moonshot native hosts (api.moonshot.ai / api.kimi.com)
|
|
280
|
+
* and Kimi-family model ids on any host, since proxies (OpenRouter, custom
|
|
281
|
+
* gateways) forward schemas to Moonshot verbatim. Set `"none"` to opt a
|
|
282
|
+
* host out.
|
|
279
283
|
*/
|
|
280
284
|
toolSchemaFlavor?: "moonshot-mfjs" | "none";
|
|
281
285
|
/**
|
|
@@ -432,6 +436,8 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
432
436
|
supportsReasoningParams: boolean;
|
|
433
437
|
supportsSamplingParams: boolean;
|
|
434
438
|
thinkingFormat: OpenAIReasoningFormat;
|
|
439
|
+
/** Kimi Code transport selected by live per-model protocol metadata. */
|
|
440
|
+
kimiApiFormat?: OpenAICompat["kimiApiFormat"];
|
|
435
441
|
reasoningDisableMode: OpenAIReasoningDisableMode;
|
|
436
442
|
omitReasoningEffort: boolean;
|
|
437
443
|
includeEncryptedReasoning: boolean;
|
|
@@ -467,6 +473,8 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
467
473
|
openRouterRouting?: OpenAICompat["openRouterRouting"];
|
|
468
474
|
/** Provider-specific wire model-id transform applied to the base id. */
|
|
469
475
|
wireModelIdMode: "raw" | "firepass" | "fireworks" | "openrouter";
|
|
476
|
+
/** See {@link OpenAICompat.toolSchemaFlavor}. Read by both wire paths when converting tools. */
|
|
477
|
+
toolSchemaFlavor?: OpenAICompat["toolSchemaFlavor"];
|
|
470
478
|
}
|
|
471
479
|
/**
|
|
472
480
|
* Fully-resolved chat-completions compat view: every detected default
|
|
@@ -474,14 +482,13 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
474
482
|
* `buildModel`; request handlers read fields and never detect, resolve, or
|
|
475
483
|
* allocate.
|
|
476
484
|
*/
|
|
477
|
-
export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "supportsSamplingParams" | "thinkingFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "qwenPreserveThinking" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "enableGeminiThinkingLoopGuard" | "whenThinking">> & {
|
|
485
|
+
export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat & Required<Omit<OpenAICompat, "supportsDeveloperRole" | "supportsReasoningEffort" | "reasoningEffortMap" | "supportsReasoningParams" | "supportsSamplingParams" | "thinkingFormat" | "kimiApiFormat" | "reasoningDisableMode" | "omitReasoningEffort" | "includeEncryptedReasoning" | "filterReasoningHistory" | "disableReasoningOnForcedToolChoice" | "disableReasoningOnToolChoice" | "supportsToolChoice" | "supportsForcedToolChoice" | "supportsNamedToolChoice" | "reasoningContentField" | "requiresReasoningContentForToolCalls" | "requiresReasoningContentForAllAssistantTurns" | "allowsSyntheticReasoningContentForToolCalls" | "replayReasoningContent" | "qwenPreserveThinking" | "requiresThinkingAsText" | "requiresMistralToolIds" | "requiresToolResultName" | "requiresAssistantAfterToolResult" | "requiresAssistantContentForToolCalls" | "stripDeepseekSpecialTokens" | "streamMarkupHealingPattern" | "reasoningDeltasMayBeCumulative" | "emptyLengthFinishIsContextError" | "usesOpenAIToolCallIdLimit" | "promptCacheSessionHeader" | "openRouterRouting" | "isOpenRouterHost" | "supportsStrictMode" | "supportsLongPromptCacheRetention" | "alwaysSendMaxTokens" | "wireModelIdMode" | "vercelGatewayRouting" | "extraBody" | "toolStrictMode" | "toolSchemaFlavor" | "streamIdleTimeoutMs" | "cacheControlFormat" | "thinkingKeep" | "strictResponsesPairing" | "supportsImageDetailOriginal" | "enableGeminiThinkingLoopGuard" | "whenThinking">> & {
|
|
478
486
|
vercelGatewayRouting?: OpenAICompat["vercelGatewayRouting"];
|
|
479
487
|
extraBody?: OpenAICompat["extraBody"];
|
|
480
488
|
cacheControlFormat?: OpenAICompat["cacheControlFormat"];
|
|
481
489
|
thinkingKeep?: OpenAICompat["thinkingKeep"];
|
|
482
490
|
streamIdleTimeoutMs?: number;
|
|
483
491
|
toolStrictMode: ResolvedToolStrictMode;
|
|
484
|
-
toolSchemaFlavor?: OpenAICompat["toolSchemaFlavor"];
|
|
485
492
|
/** The model sits behind Vercel AI Gateway. */
|
|
486
493
|
isVercelGatewayHost: boolean;
|
|
487
494
|
dropThinkingWhenReasoningEffort: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-catalog",
|
|
4
|
-
"version": "17.0.
|
|
4
|
+
"version": "17.0.4",
|
|
5
5
|
"description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@bufbuild/protobuf": "^2.12.1",
|
|
37
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
37
|
+
"@oh-my-pi/pi-utils": "17.0.4",
|
|
38
38
|
"arktype": "2.2.3",
|
|
39
39
|
"zod": "^4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "17.0.
|
|
42
|
+
"@oh-my-pi/pi-ai": "17.0.4",
|
|
43
43
|
"@types/bun": "^1.3.14"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
package/src/compat/openai.ts
CHANGED
|
@@ -86,6 +86,7 @@ function resolveReasoningDisableMode(
|
|
|
86
86
|
case "openrouter":
|
|
87
87
|
return "openrouter-enabled-false";
|
|
88
88
|
case "zai":
|
|
89
|
+
case "kimi":
|
|
89
90
|
return "zai-thinking-disabled";
|
|
90
91
|
case "qwen":
|
|
91
92
|
return "qwen-enable-thinking-false";
|
|
@@ -460,6 +461,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
460
461
|
// is rejected by NIM's `additionalProperties: false` request schema
|
|
461
462
|
// (issue #2299).
|
|
462
463
|
thinkingFormat,
|
|
464
|
+
kimiApiFormat: undefined,
|
|
463
465
|
reasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),
|
|
464
466
|
omitReasoningEffort: false,
|
|
465
467
|
includeEncryptedReasoning: true,
|
|
@@ -530,7 +532,10 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
530
532
|
supportsStrictMode: detectStrictModeSupport(provider, baseUrl),
|
|
531
533
|
extraBody: isDirectDeepseekReasoning ? { thinking: { type: "enabled" } } : undefined,
|
|
532
534
|
toolStrictMode: isCerebras ? "all_strict" : "mixed",
|
|
533
|
-
|
|
535
|
+
// Kimi-family ids trigger MFJS on any host, not just native base URLs:
|
|
536
|
+
// proxies (OpenRouter, custom gateways) forward `tools.function.parameters`
|
|
537
|
+
// to Moonshot verbatim, which 400s on non-MFJS constructs.
|
|
538
|
+
toolSchemaFlavor: isMoonshotNative || isKimiModel ? "moonshot-mfjs" : undefined,
|
|
534
539
|
streamIdleTimeoutMs,
|
|
535
540
|
stripDeepseekSpecialTokens:
|
|
536
541
|
isDeepseekModelIdOrName(spec.id) && (provider === "nvidia" || provider === "deepseek"),
|
|
@@ -656,6 +661,9 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
|
|
|
656
661
|
openRouterRouting: undefined,
|
|
657
662
|
isOpenRouterHost: isOpenRouter,
|
|
658
663
|
wireModelIdMode: isOpenRouter ? "openrouter" : "raw",
|
|
664
|
+
// Mirrors buildOpenAICompat: Kimi behind a Responses-capable proxy still
|
|
665
|
+
// lands on Moonshot's MFJS validator.
|
|
666
|
+
toolSchemaFlavor: isKimiModel ? "moonshot-mfjs" : undefined,
|
|
659
667
|
alwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,
|
|
660
668
|
enableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? "") === "gemini",
|
|
661
669
|
supportsObfuscationOptOut: isOpenAIUrl || spec.provider === "openai",
|
package/src/model-cache.ts
CHANGED
|
@@ -7,7 +7,8 @@ import { getModelDbPath } from "@oh-my-pi/pi-utils";
|
|
|
7
7
|
import type { Api, Model, ModelSpec } from "./types";
|
|
8
8
|
|
|
9
9
|
// Rows persist ModelSpec JSON (sparse `compat`, never the resolved record);
|
|
10
|
-
// the model manager rebuilds via `buildModel` on load.
|
|
10
|
+
// the model manager rebuilds via `buildModel` on load. v9 invalidates Kimi
|
|
11
|
+
// Code rows predating live effort and protocol metadata; v8 invalidated Codex
|
|
11
12
|
// discovery rows predating provider-native V2 compaction metadata; v7
|
|
12
13
|
// invalidated rows predating the Antigravity Gemini budget-mode migration
|
|
13
14
|
// (cached specs still carrying `thinking.mode: "google-level"` and the old
|
|
@@ -15,7 +16,7 @@ import type { Api, Model, ModelSpec } from "./types";
|
|
|
15
16
|
// unknown-limit sentinels (222222/8888); v5 invalidated rows predating
|
|
16
17
|
// effort-tier variant collapsing (raw `-low`/`-high`/`-thinking` member ids);
|
|
17
18
|
// v4 dropped the pre-efforts ThinkingConfig shape.
|
|
18
|
-
const CACHE_SCHEMA_VERSION =
|
|
19
|
+
const CACHE_SCHEMA_VERSION = 9;
|
|
19
20
|
|
|
20
21
|
interface CacheRow {
|
|
21
22
|
provider_id: string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import * as logger from "@oh-my-pi/pi-utils/logger";
|
|
1
2
|
import {
|
|
2
3
|
fetchOpenAICompatibleModels,
|
|
3
4
|
type OpenAICompatibleModelMapperContext,
|
|
4
5
|
type OpenAICompatibleModelRecord,
|
|
5
6
|
} from "../discovery/openai-compatible";
|
|
6
|
-
import { Effort } from "../effort";
|
|
7
|
+
import { Effort, THINKING_EFFORTS } from "../effort";
|
|
7
8
|
import { FIREWORKS_FAST_SUFFIX, toFireworksPublicModelId } from "../fireworks-model-id";
|
|
8
9
|
import {
|
|
9
10
|
isGlmVisionModelId,
|
|
@@ -2504,6 +2505,45 @@ export interface KimiCodeModelManagerConfig {
|
|
|
2504
2505
|
fetch?: FetchImpl;
|
|
2505
2506
|
}
|
|
2506
2507
|
|
|
2508
|
+
function mapKimiThinking(entry: OpenAICompatibleModelRecord): ThinkingConfig | undefined {
|
|
2509
|
+
const raw = entry.think_efforts;
|
|
2510
|
+
if (!isRecord(raw) || raw.support !== true) return undefined;
|
|
2511
|
+
const validEfforts = raw.valid_efforts;
|
|
2512
|
+
if (!Array.isArray(validEfforts)) return undefined;
|
|
2513
|
+
const efforts = THINKING_EFFORTS.filter(effort => validEfforts.includes(effort));
|
|
2514
|
+
if (efforts.length === 0) return undefined;
|
|
2515
|
+
|
|
2516
|
+
const thinking: ThinkingConfig = { mode: "effort", efforts };
|
|
2517
|
+
if (entry.supports_thinking_type === "only") {
|
|
2518
|
+
thinking.requiresEffort = true;
|
|
2519
|
+
}
|
|
2520
|
+
if (typeof raw.default_effort === "string") {
|
|
2521
|
+
const defaultLevel = THINKING_EFFORTS.find(effort => effort === raw.default_effort);
|
|
2522
|
+
if (defaultLevel !== undefined && efforts.includes(defaultLevel)) {
|
|
2523
|
+
thinking.defaultLevel = defaultLevel;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
return thinking;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
function kimiSupportsReasoning(entry: OpenAICompatibleModelRecord, modelId: string): boolean {
|
|
2530
|
+
switch (entry.supports_thinking_type) {
|
|
2531
|
+
case "only":
|
|
2532
|
+
case "both":
|
|
2533
|
+
return true;
|
|
2534
|
+
case "no":
|
|
2535
|
+
return false;
|
|
2536
|
+
default:
|
|
2537
|
+
return entry.supports_reasoning === true || modelId.includes("thinking");
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
function mapKimiApiFormat(protocol: unknown): OpenAICompat["kimiApiFormat"] {
|
|
2542
|
+
if (protocol === "anthropic") return "anthropic";
|
|
2543
|
+
if (protocol === null) return "openai";
|
|
2544
|
+
return undefined;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2507
2547
|
export function kimiCodeModelManagerOptions(
|
|
2508
2548
|
config?: KimiCodeModelManagerConfig,
|
|
2509
2549
|
): ModelManagerOptions<"openai-completions"> {
|
|
@@ -2528,15 +2568,19 @@ export function kimiCodeModelManagerOptions(
|
|
|
2528
2568
|
_context: OpenAICompatibleModelMapperContext<"openai-completions">,
|
|
2529
2569
|
): ModelSpec<"openai-completions"> => {
|
|
2530
2570
|
const id = defaults.id;
|
|
2571
|
+
const reasoning = kimiSupportsReasoning(entry, id);
|
|
2572
|
+
const thinking = reasoning ? mapKimiThinking(entry) : undefined;
|
|
2531
2573
|
return {
|
|
2532
2574
|
...defaults,
|
|
2533
2575
|
name: typeof entry.display_name === "string" ? entry.display_name : defaults.name,
|
|
2534
|
-
reasoning
|
|
2576
|
+
reasoning,
|
|
2535
2577
|
input: entry.supports_image_in === true || id.includes("k2.5") ? ["text", "image"] : ["text"],
|
|
2536
2578
|
contextWindow: typeof entry.context_length === "number" ? entry.context_length : 262144,
|
|
2537
2579
|
maxTokens: 32000,
|
|
2580
|
+
thinking,
|
|
2538
2581
|
compat: {
|
|
2539
|
-
thinkingFormat: "zai",
|
|
2582
|
+
thinkingFormat: thinking ? "kimi" : "zai",
|
|
2583
|
+
kimiApiFormat: mapKimiApiFormat(entry.protocol),
|
|
2540
2584
|
reasoningContentField: "reasoning_content",
|
|
2541
2585
|
supportsDeveloperRole: false,
|
|
2542
2586
|
},
|
|
@@ -3266,17 +3310,43 @@ type LiteLLMRichEndpointModel<TApi extends Api> = {
|
|
|
3266
3310
|
hasMaxTokens: boolean;
|
|
3267
3311
|
hasToolMetadata: boolean;
|
|
3268
3312
|
hasSupportedOpenAIParams: boolean;
|
|
3313
|
+
hasCost: boolean;
|
|
3269
3314
|
};
|
|
3315
|
+
type LiteLLMRichEndpointFailure = {
|
|
3316
|
+
endpoint: string;
|
|
3317
|
+
reason: "http-status" | "invalid-json" | "network-error";
|
|
3318
|
+
status?: number;
|
|
3319
|
+
error?: unknown;
|
|
3320
|
+
};
|
|
3321
|
+
type LiteLLMRichEndpointResult<TApi extends Api> =
|
|
3322
|
+
| { models: LiteLLMRichEndpointModel<TApi>[]; incompleteVisionMetadata: boolean }
|
|
3323
|
+
| { failure: LiteLLMRichEndpointFailure };
|
|
3270
3324
|
|
|
3271
3325
|
const LITELLM_RICH_ENDPOINTS = ["/model_group/info", "/v2/model/info", "/model/info", "/v1/model/info"] as const;
|
|
3272
3326
|
export const OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
3273
3327
|
export const OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS = 32_768;
|
|
3274
3328
|
const UNKNOWN_PROXY_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;
|
|
3329
|
+
const warnedLiteLLMMetadataBases = new Set<string>();
|
|
3275
3330
|
const LITELLM_UNUSABLE_SENTINEL_IDS: Record<string, true> = {
|
|
3276
3331
|
"all-team-models": true,
|
|
3277
3332
|
"all-proxy-models": true,
|
|
3278
3333
|
"no-default-models": true,
|
|
3279
3334
|
};
|
|
3335
|
+
function warnLiteLLMMetadataFallback(managementBaseUrl: string, failure: LiteLLMRichEndpointFailure): void {
|
|
3336
|
+
if (warnedLiteLLMMetadataBases.has(managementBaseUrl)) {
|
|
3337
|
+
return;
|
|
3338
|
+
}
|
|
3339
|
+
warnedLiteLLMMetadataBases.add(managementBaseUrl);
|
|
3340
|
+
logger.warn("LiteLLM rich model metadata unavailable; falling back to /v1/models", {
|
|
3341
|
+
endpoint: `${managementBaseUrl}${failure.endpoint}`,
|
|
3342
|
+
status: failure.status ?? "unavailable",
|
|
3343
|
+
reason: failure.reason,
|
|
3344
|
+
...(failure.status === 403
|
|
3345
|
+
? { requiredPermission: "Grant this LiteLLM key access to the model metadata endpoints" }
|
|
3346
|
+
: {}),
|
|
3347
|
+
...(failure.error !== undefined ? { error: failure.error } : {}),
|
|
3348
|
+
});
|
|
3349
|
+
}
|
|
3280
3350
|
|
|
3281
3351
|
export function normalizeLiteLLMManagementBaseUrl(baseUrl: string): string {
|
|
3282
3352
|
const trimmed = baseUrl.trim().replace(/\/+$/g, "");
|
|
@@ -3365,6 +3435,32 @@ function getLiteLLMMetadataValue(entry: LiteLLMRichModelEntry, key: string): unk
|
|
|
3365
3435
|
return entry[key] ?? getLiteLLMModelInfo(entry)?.[key];
|
|
3366
3436
|
}
|
|
3367
3437
|
|
|
3438
|
+
/** Per-million USD cost from a `*_per_token` LiteLLM field, or `undefined` when absent/non-positive. */
|
|
3439
|
+
function getLiteLLMPerMillionCost(entry: LiteLLMRichModelEntry, key: string): number | undefined {
|
|
3440
|
+
const perToken = toNumber(getLiteLLMMetadataValue(entry, key));
|
|
3441
|
+
return perToken !== undefined && perToken > 0 ? perToken * 1_000_000 : undefined;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
/**
|
|
3445
|
+
* Map LiteLLM's per-token pricing (`input_cost_per_token`, `output_cost_per_token`,
|
|
3446
|
+
* cache costs) onto {@link ModelSpec.cost} in $/million tokens. Returns `undefined`
|
|
3447
|
+
* when LiteLLM reports neither an input nor an output price so callers keep the
|
|
3448
|
+
* bundled reference cost.
|
|
3449
|
+
*/
|
|
3450
|
+
function getLiteLLMCost(entry: LiteLLMRichModelEntry): ModelSpec<Api>["cost"] | undefined {
|
|
3451
|
+
const input = getLiteLLMPerMillionCost(entry, "input_cost_per_token");
|
|
3452
|
+
const output = getLiteLLMPerMillionCost(entry, "output_cost_per_token");
|
|
3453
|
+
if (input === undefined && output === undefined) {
|
|
3454
|
+
return undefined;
|
|
3455
|
+
}
|
|
3456
|
+
return {
|
|
3457
|
+
input: input ?? 0,
|
|
3458
|
+
output: output ?? 0,
|
|
3459
|
+
cacheRead: getLiteLLMPerMillionCost(entry, "cache_read_input_token_cost") ?? 0,
|
|
3460
|
+
cacheWrite: getLiteLLMPerMillionCost(entry, "cache_creation_input_token_cost") ?? 0,
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3368
3464
|
function getLiteLLMRichModelId(entry: LiteLLMRichModelEntry): string | undefined {
|
|
3369
3465
|
return (
|
|
3370
3466
|
toNonEmptyString(entry.model_group) ??
|
|
@@ -3487,7 +3583,7 @@ function mapLiteLLMRichEntry<TApi extends Api>(
|
|
|
3487
3583
|
: (reference?.input ?? ["text"]),
|
|
3488
3584
|
reasoning: typeof supportsReasoning === "boolean" ? supportsReasoning : (reference?.reasoning ?? false),
|
|
3489
3585
|
thinking: reference?.thinking,
|
|
3490
|
-
cost: reference?.cost ?? UNKNOWN_PROXY_COST,
|
|
3586
|
+
cost: getLiteLLMCost(entry) ?? reference?.cost ?? UNKNOWN_PROXY_COST,
|
|
3491
3587
|
...(supportsTools !== undefined ? { supportsTools } : {}),
|
|
3492
3588
|
compat: compat as ModelSpec<TApi>["compat"],
|
|
3493
3589
|
};
|
|
@@ -3499,7 +3595,7 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
|
3499
3595
|
managementBaseUrl: string,
|
|
3500
3596
|
runtimeBaseUrl: string,
|
|
3501
3597
|
signal?: AbortSignal,
|
|
3502
|
-
): Promise<
|
|
3598
|
+
): Promise<LiteLLMRichEndpointResult<TApi> | null> {
|
|
3503
3599
|
const fetchImpl = discoveryFetch(options.fetch);
|
|
3504
3600
|
const requestHeaders: Record<string, string> = {
|
|
3505
3601
|
Accept: "application/json",
|
|
@@ -3515,17 +3611,17 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
|
3515
3611
|
headers: requestHeaders,
|
|
3516
3612
|
signal,
|
|
3517
3613
|
});
|
|
3518
|
-
} catch {
|
|
3519
|
-
return
|
|
3614
|
+
} catch (error) {
|
|
3615
|
+
return { failure: { endpoint, reason: "network-error", error } };
|
|
3520
3616
|
}
|
|
3521
3617
|
if (!response.ok) {
|
|
3522
|
-
return null;
|
|
3618
|
+
return response.status === 404 ? null : { failure: { endpoint, reason: "http-status", status: response.status } };
|
|
3523
3619
|
}
|
|
3524
3620
|
let payload: unknown;
|
|
3525
3621
|
try {
|
|
3526
3622
|
payload = await response.json();
|
|
3527
|
-
} catch {
|
|
3528
|
-
return
|
|
3623
|
+
} catch (error) {
|
|
3624
|
+
return { failure: { endpoint, reason: "invalid-json", status: response.status, error } };
|
|
3529
3625
|
}
|
|
3530
3626
|
const entries = extractLiteLLMRichEntries(payload);
|
|
3531
3627
|
if (!entries || entries.length === 0) {
|
|
@@ -3554,6 +3650,7 @@ async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
|
3554
3650
|
supportsFunctionCalling === false ||
|
|
3555
3651
|
supportedOpenAIParams !== undefined,
|
|
3556
3652
|
hasSupportedOpenAIParams: supportedOpenAIParams !== undefined,
|
|
3653
|
+
hasCost: getLiteLLMCost(entry) !== undefined,
|
|
3557
3654
|
});
|
|
3558
3655
|
}
|
|
3559
3656
|
}
|
|
@@ -3576,11 +3673,27 @@ export async function fetchLiteLLMRichModels<TApi extends Api>(
|
|
|
3576
3673
|
}
|
|
3577
3674
|
const fetchModels = async (signal?: AbortSignal): Promise<ModelSpec<TApi>[] | null> => {
|
|
3578
3675
|
const deduped = new Map<string, LiteLLMRichEndpointModel<TApi>>();
|
|
3676
|
+
let metadataFailure: LiteLLMRichEndpointFailure | undefined;
|
|
3579
3677
|
for (const endpoint of LITELLM_RICH_ENDPOINTS) {
|
|
3580
3678
|
const result = await fetchLiteLLMRichEndpoint(endpoint, options, managementBaseUrl, runtimeBaseUrl, signal);
|
|
3581
3679
|
if (!result) {
|
|
3582
3680
|
continue;
|
|
3583
3681
|
}
|
|
3682
|
+
if ("failure" in result) {
|
|
3683
|
+
// A 401 is a retryable auth failure owned by the caller's auth-retry
|
|
3684
|
+
// path (withAuth refresh/sibling rotation in discoverLiteLLMModels);
|
|
3685
|
+
// recording it here would log a fallback warning on a stale first
|
|
3686
|
+
// credential before the refreshed retry ultimately serves rich
|
|
3687
|
+
// metadata. Forbidden (403) and other failures are never retried, so
|
|
3688
|
+
// they remain warn-worthy and keep priority.
|
|
3689
|
+
if (
|
|
3690
|
+
result.failure.status !== 401 &&
|
|
3691
|
+
(!metadataFailure || (metadataFailure.status !== 403 && result.failure.status === 403))
|
|
3692
|
+
) {
|
|
3693
|
+
metadataFailure = result.failure;
|
|
3694
|
+
}
|
|
3695
|
+
continue;
|
|
3696
|
+
}
|
|
3584
3697
|
const hadPriorModels = deduped.size > 0;
|
|
3585
3698
|
for (const next of result.models) {
|
|
3586
3699
|
const existing = deduped.get(next.model.id);
|
|
@@ -3600,6 +3713,7 @@ export async function fetchLiteLLMRichModels<TApi extends Api>(
|
|
|
3600
3713
|
? next.model.input
|
|
3601
3714
|
: existing.model.input,
|
|
3602
3715
|
reasoning: typeof next.supportsReasoning === "boolean" ? next.model.reasoning : existing.model.reasoning,
|
|
3716
|
+
cost: next.hasCost ? next.model.cost : existing.model.cost,
|
|
3603
3717
|
compat: next.hasSupportedOpenAIParams ? next.model.compat : existing.model.compat,
|
|
3604
3718
|
};
|
|
3605
3719
|
if (next.hasToolMetadata) {
|
|
@@ -3619,6 +3733,9 @@ export async function fetchLiteLLMRichModels<TApi extends Api>(
|
|
|
3619
3733
|
}
|
|
3620
3734
|
}
|
|
3621
3735
|
if (deduped.size === 0) {
|
|
3736
|
+
if (metadataFailure) {
|
|
3737
|
+
warnLiteLLMMetadataFallback(managementBaseUrl, metadataFailure);
|
|
3738
|
+
}
|
|
3622
3739
|
return null;
|
|
3623
3740
|
}
|
|
3624
3741
|
return Array.from(deduped.values())
|
|
@@ -3638,13 +3755,13 @@ export function litellmModelManagerOptions(
|
|
|
3638
3755
|
const baseUrl = config?.baseUrl ?? Bun.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1";
|
|
3639
3756
|
return {
|
|
3640
3757
|
providerId: "litellm",
|
|
3641
|
-
// rich-
|
|
3642
|
-
//
|
|
3643
|
-
//
|
|
3644
|
-
//
|
|
3645
|
-
//
|
|
3646
|
-
//
|
|
3647
|
-
cacheProviderId: `litellm:rich-
|
|
3758
|
+
// rich-v5 invalidates rows cached before rich metadata pricing was mapped.
|
|
3759
|
+
// Earlier versions added bundled reference fallback, continued discovery
|
|
3760
|
+
// past incomplete `/model_group/info`, stripped reseller usage suffixes,
|
|
3761
|
+
// and filtered placeholder-only `all-team-models` rows. Bump the version
|
|
3762
|
+
// whenever the mappers below change, or warm authoritative caches keep
|
|
3763
|
+
// serving pre-change rows for the full TTL.
|
|
3764
|
+
cacheProviderId: `litellm:rich-v5:${Bun.hash(baseUrl).toString(36)}`,
|
|
3648
3765
|
// litellm is a local-only proxy and is never bundled in models.json (that
|
|
3649
3766
|
// would leak the machine's localhost catalog). Prefer the proxy's richer
|
|
3650
3767
|
// management metadata, then enrich ids against models.dev with the bundled
|
package/src/types.ts
CHANGED
|
@@ -148,7 +148,7 @@ export interface Usage {
|
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
export type OpenAIReasoningFormat = "openai" | "openrouter" | "zai" | "qwen" | "qwen-chat-template";
|
|
151
|
+
export type OpenAIReasoningFormat = "openai" | "openrouter" | "zai" | "kimi" | "qwen" | "qwen-chat-template";
|
|
152
152
|
|
|
153
153
|
export type OpenAIReasoningDisableMode =
|
|
154
154
|
| "omit"
|
|
@@ -205,8 +205,10 @@ export interface OpenAICompat {
|
|
|
205
205
|
requiresThinkingAsText?: boolean;
|
|
206
206
|
/** Whether tool call IDs must be normalized to Mistral format (exactly 9 alphanumeric chars). Default: auto-detected from URL. */
|
|
207
207
|
requiresMistralToolIds?: boolean;
|
|
208
|
-
/** Format for reasoning/thinking parameter. "
|
|
208
|
+
/** Format for reasoning/thinking parameter. `"kimi"` uses `thinking: { type, effort }`; other values select their provider-native reasoning fields. Default: `"openai"`. */
|
|
209
209
|
thinkingFormat?: OpenAIReasoningFormat;
|
|
210
|
+
/** Kimi Code transport selected by live per-model protocol metadata. User settings take precedence. */
|
|
211
|
+
kimiApiFormat?: "openai" | "anthropic";
|
|
210
212
|
/** Request-time disable encoding for the selected reasoning/thinking format. Default: derived from `thinkingFormat`. */
|
|
211
213
|
reasoningDisableMode?: OpenAIReasoningDisableMode;
|
|
212
214
|
/** Whether the provider rejects `reasoning.effort`/`reasoning_effort` even when the model reasons natively. Default: false unless reasoning effort is unsupported. */
|
|
@@ -312,8 +314,10 @@ export interface OpenAICompat {
|
|
|
312
314
|
* normalization (collapse `const`→`enum`, infer `type` on bare enums, strip
|
|
313
315
|
* unsupported validators/`prefixItems`) because Moonshot/Kimi native hosts
|
|
314
316
|
* reject standard JSON Schema constructs with HTTP 400. Default:
|
|
315
|
-
* auto-detected
|
|
316
|
-
*
|
|
317
|
+
* auto-detected — Moonshot native hosts (api.moonshot.ai / api.kimi.com)
|
|
318
|
+
* and Kimi-family model ids on any host, since proxies (OpenRouter, custom
|
|
319
|
+
* gateways) forward schemas to Moonshot verbatim. Set `"none"` to opt a
|
|
320
|
+
* host out.
|
|
317
321
|
*/
|
|
318
322
|
toolSchemaFlavor?: "moonshot-mfjs" | "none";
|
|
319
323
|
/**
|
|
@@ -475,6 +479,8 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
475
479
|
supportsReasoningParams: boolean;
|
|
476
480
|
supportsSamplingParams: boolean;
|
|
477
481
|
thinkingFormat: OpenAIReasoningFormat;
|
|
482
|
+
/** Kimi Code transport selected by live per-model protocol metadata. */
|
|
483
|
+
kimiApiFormat?: OpenAICompat["kimiApiFormat"];
|
|
478
484
|
reasoningDisableMode: OpenAIReasoningDisableMode;
|
|
479
485
|
omitReasoningEffort: boolean;
|
|
480
486
|
includeEncryptedReasoning: boolean;
|
|
@@ -510,6 +516,8 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
510
516
|
openRouterRouting?: OpenAICompat["openRouterRouting"];
|
|
511
517
|
/** Provider-specific wire model-id transform applied to the base id. */
|
|
512
518
|
wireModelIdMode: "raw" | "firepass" | "fireworks" | "openrouter";
|
|
519
|
+
/** See {@link OpenAICompat.toolSchemaFlavor}. Read by both wire paths when converting tools. */
|
|
520
|
+
toolSchemaFlavor?: OpenAICompat["toolSchemaFlavor"];
|
|
513
521
|
}
|
|
514
522
|
|
|
515
523
|
/**
|
|
@@ -528,6 +536,7 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
|
|
|
528
536
|
| "supportsReasoningParams"
|
|
529
537
|
| "supportsSamplingParams"
|
|
530
538
|
| "thinkingFormat"
|
|
539
|
+
| "kimiApiFormat"
|
|
531
540
|
| "reasoningDisableMode"
|
|
532
541
|
| "omitReasoningEffort"
|
|
533
542
|
| "includeEncryptedReasoning"
|
|
@@ -579,7 +588,6 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
|
|
|
579
588
|
thinkingKeep?: OpenAICompat["thinkingKeep"];
|
|
580
589
|
streamIdleTimeoutMs?: number;
|
|
581
590
|
toolStrictMode: ResolvedToolStrictMode;
|
|
582
|
-
toolSchemaFlavor?: OpenAICompat["toolSchemaFlavor"];
|
|
583
591
|
/** The model sits behind Vercel AI Gateway. */
|
|
584
592
|
isVercelGatewayHost: boolean;
|
|
585
593
|
dropThinkingWhenReasoningEffort: boolean;
|