pi-cache-optimizer 2.8.16 → 2.8.18
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 +21 -17
- package/README.zh-CN.md +21 -17
- package/index.ts +260 -11412
- package/package.json +8 -6
- package/src/adapters.ts +250 -0
- package/src/atomic-fs.ts +300 -0
- package/src/command-completion.ts +114 -0
- package/src/command.ts +1012 -0
- package/src/common.ts +49 -0
- package/src/compat-advice.ts +320 -0
- package/src/compat-config.ts +99 -0
- package/src/config.ts +374 -0
- package/src/diagnostics.ts +757 -0
- package/src/fix-types.ts +66 -0
- package/src/jsonc.ts +602 -0
- package/src/model-detect.ts +66 -0
- package/src/model-identity.ts +368 -0
- package/src/models-json-fix.ts +1982 -0
- package/src/paths.ts +18 -0
- package/src/prompt-cache-key-config.ts +406 -0
- package/src/prompt-rewrite.ts +294 -0
- package/src/provider-errors.ts +328 -0
- package/src/request-payload.ts +197 -0
- package/src/request-state.ts +102 -0
- package/src/retention.ts +82 -0
- package/src/routing.ts +551 -0
- package/src/stats-report.ts +771 -0
- package/src/stats-store.ts +500 -0
- package/src/tool-ordering.ts +209 -0
- package/src/usage.ts +172 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type PiModel, asRecord, lower } from "./common.ts";
|
|
2
|
+
|
|
3
|
+
export const ASSISTANT_MESSAGE_MODEL_TOKEN_KEYS = ["model", "name"];
|
|
4
|
+
|
|
5
|
+
export function getModelIdNameTokenValues(model: PiModel | undefined): string[] {
|
|
6
|
+
if (!model) return [];
|
|
7
|
+
return [model.id, model.name].map(lower).filter(Boolean);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getAssistantMessageModelTokenValues(message: unknown): string[] {
|
|
11
|
+
const record = asRecord(message);
|
|
12
|
+
if (!record) return [];
|
|
13
|
+
|
|
14
|
+
return ASSISTANT_MESSAGE_MODEL_TOKEN_KEYS.map((key) => lower(record[key])).filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function hasAnyTokenContaining(tokens: string[], needles: string[]): boolean {
|
|
18
|
+
return tokens.some((token) => needles.some((needle) => token.includes(needle)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function modelOrAssistantMessageHas(message: unknown, model: PiModel | undefined, needles: string[]): boolean {
|
|
22
|
+
return hasAnyTokenContaining([...getModelIdNameTokenValues(model), ...getAssistantMessageModelTokenValues(message)], needles);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Adaptive generation model detection ────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check whether the model id uses Anthropic's adaptive generation (thinking)
|
|
29
|
+
* that requires `forceAdaptiveThinking: true` in compat.
|
|
30
|
+
*
|
|
31
|
+
* Adaptive-generation models (from pi-ai built-in catalog) include:
|
|
32
|
+
* claude-opus-4-6, claude-opus-4-7, claude-opus-4-8 (also dotted 4.6/4.7/4.8)
|
|
33
|
+
* claude-sonnet-4-6, claude-sonnet-5
|
|
34
|
+
* claude-fable-5
|
|
35
|
+
*
|
|
36
|
+
* We match broadly: opus >= 4-6, sonnet >= 4-6, fable >= 5.
|
|
37
|
+
* Ids may carry date-stamp or size suffixes like "[1M]".
|
|
38
|
+
*/
|
|
39
|
+
export const ADAPTIVE_OPUS_PATTERN = /(^|[\/\s:_-])(opus-4[.-][6-9]|opus-4-[1-9][0-9]|opus-([5-9]|[1-9][0-9]))($|[-_.:\/\s\[])/i;
|
|
40
|
+
|
|
41
|
+
export const ADAPTIVE_SONNET_PATTERN = /(^|[\/\s:_-])(sonnet-4[.-][6-9]|sonnet-4-[1-9][0-9]|sonnet-([5-9]|[1-9][0-9]))($|[-_.:\/\s\[])/i;
|
|
42
|
+
|
|
43
|
+
export const ADAPTIVE_FABLE_PATTERN = /(^|[\/\s:_-])fable-([5-9]|[1-9][0-9])($|[-_.:\/\s\[])/i;
|
|
44
|
+
|
|
45
|
+
export function isAdaptiveGenerationModel(model: PiModel | undefined): boolean {
|
|
46
|
+
if (!model) return false;
|
|
47
|
+
const tokens = getModelIdNameTokenValues(model);
|
|
48
|
+
return tokens.some((t) => ADAPTIVE_OPUS_PATTERN.test(t) || ADAPTIVE_SONNET_PATTERN.test(t) || ADAPTIVE_FABLE_PATTERN.test(t));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isKimiCodingAdaptiveModel(model: PiModel | undefined): boolean {
|
|
52
|
+
if (!model) return false;
|
|
53
|
+
const provider = lower(model.provider);
|
|
54
|
+
const baseUrl = lower(model.baseUrl);
|
|
55
|
+
const isKimiCodingChannel = provider.includes("kimi-coding") || baseUrl.includes("api.kimi.com/coding");
|
|
56
|
+
if (!isKimiCodingChannel) return false;
|
|
57
|
+
|
|
58
|
+
const tokens = getModelIdNameTokenValues(model);
|
|
59
|
+
return tokens.some((token) =>
|
|
60
|
+
token === "k3"
|
|
61
|
+
|| token.includes("kimi-k3")
|
|
62
|
+
|| token.includes("kimi k3")
|
|
63
|
+
|| token.includes("kimi-for-coding")
|
|
64
|
+
|| token.includes("kimi for coding")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { type ModelIdentity, type PiModel, type UnknownRecord, asRecord, isNonEmptyString, lower } from "./common.ts";
|
|
2
|
+
import { type CacheCompat, NESTED_COMPAT_KEYS, resolveEffectiveCompatFromConfig } from "./compat-config.ts";
|
|
3
|
+
import { parseJsonc } from "./jsonc.ts";
|
|
4
|
+
import { getAssistantMessageModelTokenValues, getModelIdNameTokenValues, hasAnyTokenContaining, isKimiCodingAdaptiveModel, modelOrAssistantMessageHas } from "./model-detect.ts";
|
|
5
|
+
import { MODELS_JSON_PATH } from "./paths.ts";
|
|
6
|
+
import { readFileSync, statSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
export const ROUTED_FALLBACK_MODEL_SYMBOL = Symbol("pi-cache-optimizer.routed-fallback-model");
|
|
9
|
+
|
|
10
|
+
// Model API that Pi 0.99+ assigns to native virtual models (VIRTUAL_MODEL_API).
|
|
11
|
+
export const PI_VIRTUAL_MODEL_API = "pi-virtual";
|
|
12
|
+
|
|
13
|
+
export const OPENAI_REASONING_MODEL_PATTERN = /(^|[/\s:_-])o[1345]($|[-_.:/\s])/;
|
|
14
|
+
|
|
15
|
+
export const XAI_MODEL_PATTERN = /(^|[/\s:_-])xai($|[-_.:/\s])/;
|
|
16
|
+
|
|
17
|
+
export const MIMO_MODEL_PATTERN = /(^|[/\s:_-])mi-?mo($|[-_.:/\s])/i;
|
|
18
|
+
|
|
19
|
+
export const PPLX_MODEL_PATTERN = /(^|[/\s:_-])pplx($|[-_.:/\s])/i;
|
|
20
|
+
|
|
21
|
+
export const NOVA_MODEL_PATTERN = /(^|[/\s:_-])nova($|[-_.:/\s])/i;
|
|
22
|
+
|
|
23
|
+
export const MPT_MODEL_PATTERN = /(^|[/\s:_-])mpt($|[-_.:/\s])/i;
|
|
24
|
+
|
|
25
|
+
export const ALEPH_MODEL_PATTERN = /(^|[/\s:_-])aleph($|[-_.:/\s])/i;
|
|
26
|
+
|
|
27
|
+
// Safe-boundary patterns for models with short or ambiguous tokens
|
|
28
|
+
export const ARCTIC_MODEL_PATTERN = /(^|[\/\s:_-])arctic($|[\-_.:\/\s])/i;
|
|
29
|
+
|
|
30
|
+
export const AYA_MODEL_PATTERN = /(^|[\/\s:_-])aya($|[\-_.:\/\s])/i;
|
|
31
|
+
|
|
32
|
+
export const ORION_MODEL_PATTERN = /(^|[\/\s:_-])orion($|[\-_.:\/\s])/i;
|
|
33
|
+
|
|
34
|
+
export function isRoutedFallbackModel(model: PiModel | undefined): boolean {
|
|
35
|
+
return !!model && (model as PiModel & Record<symbol, unknown>)[ROUTED_FALLBACK_MODEL_SYMBOL] === true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isNativeVirtualModel(model: { api?: unknown } | undefined): boolean {
|
|
39
|
+
return model?.api === PI_VIRTUAL_MODEL_API;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isOptionalString(value: unknown): boolean {
|
|
43
|
+
return value === undefined || (typeof value === "string" && value.length > 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isOptionalBoolean(value: unknown): boolean {
|
|
47
|
+
return value === undefined || typeof value === "boolean";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isFiniteNumber(value: unknown): value is number {
|
|
51
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isRecordOfStrings(value: unknown): boolean {
|
|
55
|
+
const record = asRecord(value);
|
|
56
|
+
return !!record && Object.values(record).every((entry) => typeof entry === "string");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isThinkingLevelMap(value: unknown): boolean {
|
|
60
|
+
const map = asRecord(value);
|
|
61
|
+
if (!map) return false;
|
|
62
|
+
return ["off", "minimal", "low", "medium", "high", "xhigh", "max"].every((key) =>
|
|
63
|
+
map[key] === undefined || map[key] === null || typeof map[key] === "string"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isValidModelCostTier(value: unknown): boolean {
|
|
68
|
+
const tier = asRecord(value);
|
|
69
|
+
return !!tier
|
|
70
|
+
&& isFiniteNumber(tier.inputTokensAbove)
|
|
71
|
+
&& isFiniteNumber(tier.input)
|
|
72
|
+
&& isFiniteNumber(tier.output)
|
|
73
|
+
&& isFiniteNumber(tier.cacheRead)
|
|
74
|
+
&& isFiniteNumber(tier.cacheWrite);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isValidModelCost(value: unknown, partial: boolean): boolean {
|
|
78
|
+
const cost = asRecord(value);
|
|
79
|
+
if (!cost) return false;
|
|
80
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
|
|
81
|
+
if ((!partial || cost[key] !== undefined) && !isFiniteNumber(cost[key])) return false;
|
|
82
|
+
}
|
|
83
|
+
return cost.tiers === undefined || (Array.isArray(cost.tiers) && cost.tiers.every(isValidModelCostTier));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isValidModelDefinition(value: unknown): boolean {
|
|
87
|
+
const model = asRecord(value);
|
|
88
|
+
if (!model || !isNonEmptyString(model.id)) return false;
|
|
89
|
+
if (!isOptionalString(model.name) || !isOptionalString(model.api) || !isOptionalString(model.baseUrl)) return false;
|
|
90
|
+
if (!isOptionalBoolean(model.reasoning) || !isValidCompatRecord(model.compat)) return false;
|
|
91
|
+
if (model.thinkingLevelMap !== undefined && !isThinkingLevelMap(model.thinkingLevelMap)) return false;
|
|
92
|
+
if (model.input !== undefined && (!Array.isArray(model.input) || !model.input.every((entry) => entry === "text" || entry === "image"))) return false;
|
|
93
|
+
if (model.cost !== undefined && !isValidModelCost(model.cost, false)) return false;
|
|
94
|
+
if (model.contextWindow !== undefined && !isFiniteNumber(model.contextWindow)) return false;
|
|
95
|
+
if (model.maxTokens !== undefined && !isFiniteNumber(model.maxTokens)) return false;
|
|
96
|
+
if (model.samplingParams !== undefined && !asRecord(model.samplingParams)) return false;
|
|
97
|
+
if (model.headers !== undefined && !isRecordOfStrings(model.headers)) return false;
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function isValidModelOverride(value: unknown): boolean {
|
|
102
|
+
const override = asRecord(value);
|
|
103
|
+
if (!override || !isValidCompatRecord(override.compat)) return false;
|
|
104
|
+
if (!isOptionalString(override.name) || !isOptionalBoolean(override.reasoning)) return false;
|
|
105
|
+
if (override.thinkingLevelMap !== undefined && !isThinkingLevelMap(override.thinkingLevelMap)) return false;
|
|
106
|
+
if (override.input !== undefined && (!Array.isArray(override.input) || !override.input.every((entry) => entry === "text" || entry === "image"))) return false;
|
|
107
|
+
if (override.cost !== undefined && !isValidModelCost(override.cost, true)) return false;
|
|
108
|
+
if (override.contextWindow !== undefined && !isFiniteNumber(override.contextWindow)) return false;
|
|
109
|
+
if (override.maxTokens !== undefined && !isFiniteNumber(override.maxTokens)) return false;
|
|
110
|
+
if (override.samplingParams !== undefined && !asRecord(override.samplingParams)) return false;
|
|
111
|
+
if (override.headers !== undefined && !isRecordOfStrings(override.headers)) return false;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isValidOpenAICompletionsCompat(compat: UnknownRecord): boolean {
|
|
116
|
+
const booleanKeys = [
|
|
117
|
+
"supportsStore", "supportsDeveloperRole", "supportsReasoningEffort",
|
|
118
|
+
"supportsUsageInStreaming", "requiresToolResultName", "requiresAssistantAfterToolResult",
|
|
119
|
+
"requiresThinkingAsText", "requiresReasoningContentOnAssistantMessages",
|
|
120
|
+
"supportsOpenAIGrammarTools", "supportsStrictMode", "sendSessionAffinityHeaders",
|
|
121
|
+
"supportsLongCacheRetention",
|
|
122
|
+
];
|
|
123
|
+
if (booleanKeys.some((key) => !isOptionalBoolean(compat[key]))) return false;
|
|
124
|
+
if (compat.maxTokensField !== undefined && compat.maxTokensField !== "max_completion_tokens" && compat.maxTokensField !== "max_tokens") return false;
|
|
125
|
+
if (compat.thinkingFormat !== undefined && ![
|
|
126
|
+
"openai", "openrouter", "together", "baseten", "deepseek", "zai", "qwen",
|
|
127
|
+
"chat-template", "qwen-chat-template", "string-thinking", "ant-ling",
|
|
128
|
+
].includes(String(compat.thinkingFormat))) return false;
|
|
129
|
+
if (compat.cacheControlFormat !== undefined && compat.cacheControlFormat !== "anthropic") return false;
|
|
130
|
+
if (compat.deferredToolsMode !== undefined && compat.deferredToolsMode !== "kimi") return false;
|
|
131
|
+
if (compat.sessionAffinityFormat !== undefined && !["openai", "openai-nosession", "openrouter"].includes(String(compat.sessionAffinityFormat))) return false;
|
|
132
|
+
for (const key of NESTED_COMPAT_KEYS) {
|
|
133
|
+
if (compat[key] !== undefined && !asRecord(compat[key])) return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function isValidOpenAIResponsesCompat(compat: UnknownRecord): boolean {
|
|
139
|
+
const booleanKeys = [
|
|
140
|
+
"supportsDeveloperRole", "supportsLongCacheRetention", "supportsStrictMode",
|
|
141
|
+
"supportsOpenAIGrammarTools", "supportsAdditionalTools", "supportsToolSearch",
|
|
142
|
+
];
|
|
143
|
+
if (booleanKeys.some((key) => !isOptionalBoolean(compat[key]))) return false;
|
|
144
|
+
return compat.sessionAffinityFormat === undefined
|
|
145
|
+
|| ["openai", "openai-nosession", "openrouter"].includes(String(compat.sessionAffinityFormat));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function isValidAnthropicMessagesCompat(compat: UnknownRecord): boolean {
|
|
149
|
+
return [
|
|
150
|
+
"supportsEagerToolInputStreaming", "supportsLongCacheRetention",
|
|
151
|
+
"sendSessionAffinityHeaders", "supportsCacheControlOnTools", "supportsTemperature",
|
|
152
|
+
"forceAdaptiveThinking", "allowEmptySignature", "supportsStrictTools", "supportsToolReferences",
|
|
153
|
+
].every((key) => isOptionalBoolean(compat[key]));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function isValidCompatRecord(value: unknown): boolean {
|
|
157
|
+
if (value === undefined) return true;
|
|
158
|
+
const compat = asRecord(value);
|
|
159
|
+
return !!compat
|
|
160
|
+
&& !Object.prototype.hasOwnProperty.call(compat, "supportsPromptCacheKey")
|
|
161
|
+
&& (
|
|
162
|
+
isValidOpenAICompletionsCompat(compat)
|
|
163
|
+
|| isValidOpenAIResponsesCompat(compat)
|
|
164
|
+
|| isValidAnthropicMessagesCompat(compat)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Credential-blind fail-closed subset of Pi's models.json validation. This
|
|
169
|
+
// intentionally covers only fields consumed by compat diagnostics, transport
|
|
170
|
+
// recovery, and exact route fallback; it is not a replacement for Pi's full
|
|
171
|
+
// schema and must stay conservative when the file is malformed.
|
|
172
|
+
export function isValidModelsConfigForEffectiveCompat(value: unknown): boolean {
|
|
173
|
+
const root = asRecord(value);
|
|
174
|
+
const providers = asRecord(root?.providers);
|
|
175
|
+
if (!root || !providers) return false;
|
|
176
|
+
|
|
177
|
+
for (const providerValue of Object.values(providers)) {
|
|
178
|
+
const provider = asRecord(providerValue);
|
|
179
|
+
if (!provider) return false;
|
|
180
|
+
if (!isOptionalString(provider.name) || !isOptionalString(provider.baseUrl) || !isOptionalString(provider.apiKey) || !isOptionalString(provider.api)) return false;
|
|
181
|
+
if (!isOptionalBoolean(provider.authHeader) || !isValidCompatRecord(provider.compat)) return false;
|
|
182
|
+
if (provider.oauth !== undefined && provider.oauth !== "radius") return false;
|
|
183
|
+
if (provider.headers !== undefined && !isRecordOfStrings(provider.headers)) return false;
|
|
184
|
+
if (provider.models !== undefined && (!Array.isArray(provider.models) || !provider.models.every(isValidModelDefinition))) return false;
|
|
185
|
+
const overrides = provider.modelOverrides === undefined ? undefined : asRecord(provider.modelOverrides);
|
|
186
|
+
if (provider.modelOverrides !== undefined && (!overrides || !Object.values(overrides).every(isValidModelOverride))) return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type ModelsConfigCache = {
|
|
192
|
+
signature: string;
|
|
193
|
+
value: unknown | undefined;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
let modelsConfigCache: ModelsConfigCache | undefined;
|
|
197
|
+
|
|
198
|
+
export function getModelsConfigSignature(): string {
|
|
199
|
+
try {
|
|
200
|
+
const info = statSync(MODELS_JSON_PATH);
|
|
201
|
+
return `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}:${info.ctimeMs}`;
|
|
202
|
+
} catch {
|
|
203
|
+
return "missing";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function readEffectiveCompatConfig(): unknown | undefined {
|
|
208
|
+
const signature = getModelsConfigSignature();
|
|
209
|
+
if (modelsConfigCache?.signature === signature) return modelsConfigCache.value;
|
|
210
|
+
|
|
211
|
+
let value: unknown | undefined;
|
|
212
|
+
if (signature !== "missing") {
|
|
213
|
+
try {
|
|
214
|
+
const parsed = parseJsonc(readFileSync(MODELS_JSON_PATH, "utf8"));
|
|
215
|
+
value = isValidModelsConfigForEffectiveCompat(parsed) ? parsed : undefined;
|
|
216
|
+
} catch {
|
|
217
|
+
value = undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
modelsConfigCache = { signature, value };
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function getCompat(model: PiModel | undefined): CacheCompat {
|
|
226
|
+
if (!model) return {} as CacheCompat;
|
|
227
|
+
return resolveEffectiveCompatFromConfig(model, readEffectiveCompatConfig());
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function isAssistantMessage(message: unknown): boolean {
|
|
231
|
+
return asRecord(message)?.role === "assistant";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function getAssistantRecord(message: unknown): UnknownRecord | undefined {
|
|
235
|
+
const record = asRecord(message);
|
|
236
|
+
return record?.role === "assistant" ? record : undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function isDeepSeekLikeModel(model: PiModel | undefined): boolean {
|
|
240
|
+
return hasAnyTokenContaining(getModelIdNameTokenValues(model), ["deepseek"]);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function isDeepSeekLikeAssistantMessage(message: unknown, model: PiModel | undefined): boolean {
|
|
244
|
+
return modelOrAssistantMessageHas(message, model, ["deepseek"]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function isOpenAICompatibleApi(api: unknown): boolean {
|
|
248
|
+
const value = lower(api);
|
|
249
|
+
return value === "openai-completions" || value === "openai-responses";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function isAnthropicMessagesApi(api: unknown): boolean {
|
|
253
|
+
return lower(api) === "anthropic-messages";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function isOpenAICompatibleProxyApi(api: unknown): boolean {
|
|
257
|
+
return lower(api) === "openai-completions";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function isPiBuiltInLlamaCppModel(model: PiModel | undefined): boolean {
|
|
261
|
+
if (lower(model?.provider) !== "llama.cpp" || !isOpenAICompatibleProxyApi(model?.api)) return false;
|
|
262
|
+
|
|
263
|
+
// Pi's built-in llama.cpp provider supplies this exact explicit compat
|
|
264
|
+
// fingerprint. Provider ids are extension-overridable and models.json can add
|
|
265
|
+
// cache/routing overrides, so provider id alone must never imply exemption.
|
|
266
|
+
// supportsUsageInStreaming is deliberately not part of the fingerprint: Pi
|
|
267
|
+
// 0.82.x sets it false and Pi 0.83+ sets it true after fixing streamed usage,
|
|
268
|
+
// and it carries no routing or cache configuration.
|
|
269
|
+
const compat = getCompat(model);
|
|
270
|
+
return compat.supportsStore === false
|
|
271
|
+
&& compat.supportsDeveloperRole === false
|
|
272
|
+
&& compat.supportsReasoningEffort === false
|
|
273
|
+
&& compat.supportsStrictMode === false
|
|
274
|
+
&& compat.maxTokensField === "max_tokens"
|
|
275
|
+
&& compat.sendSessionAffinityHeaders === undefined
|
|
276
|
+
&& compat.sessionAffinityFormat === undefined
|
|
277
|
+
&& compat.supportsLongCacheRetention === undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function isResponsesPromptRewriteBypassApi(api: unknown): boolean {
|
|
281
|
+
const value = lower(api);
|
|
282
|
+
return value === "openai-codex-responses" || value === "openai-responses" || value === "azure-openai-responses";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function isMistralConversationsApi(api: unknown): boolean {
|
|
286
|
+
return lower(api) === "mistral-conversations";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function isOpenAIFamilyToken(token: string): boolean {
|
|
290
|
+
return token.includes("gpt-") || token.includes("chatgpt") || OPENAI_REASONING_MODEL_PATTERN.test(token);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function isOpenAIFamilyModel(model: PiModel | undefined): boolean {
|
|
294
|
+
return getModelIdNameTokenValues(model).some(isOpenAIFamilyToken);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function isOpenAIFamilyAssistantMessage(message: unknown, model: PiModel | undefined): boolean {
|
|
298
|
+
return [...getModelIdNameTokenValues(model), ...getAssistantMessageModelTokenValues(message)].some(isOpenAIFamilyToken);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function isClaudeLikeModel(model: PiModel | undefined): boolean {
|
|
302
|
+
return hasAnyTokenContaining(getModelIdNameTokenValues(model), ["anthropic", "claude"]);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function isClaudeLikeAssistantMessage(message: unknown, model: PiModel | undefined): boolean {
|
|
306
|
+
return modelOrAssistantMessageHas(message, model, ["anthropic", "claude"]);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function isGeminiLikeModel(model: PiModel | undefined): boolean {
|
|
310
|
+
return hasAnyTokenContaining(getModelIdNameTokenValues(model), ["gemini", "vertex"]);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function isGeminiLikeAssistantMessage(message: unknown, model: PiModel | undefined): boolean {
|
|
314
|
+
return modelOrAssistantMessageHas(message, model, ["gemini", "vertex"]);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function isKimiCodingEmptySignatureModel(model: PiModel | undefined): boolean {
|
|
318
|
+
if (!isKimiCodingAdaptiveModel(model)) return false;
|
|
319
|
+
return getModelIdNameTokenValues(model).some((token) =>
|
|
320
|
+
token === "k3"
|
|
321
|
+
|| token === "kimi-k3"
|
|
322
|
+
|| token.startsWith("kimi-k3-")
|
|
323
|
+
|| token === "kimi k3"
|
|
324
|
+
|| token === "kimi-for-coding"
|
|
325
|
+
|| token === "kimi for coding"
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── Additional OpenAI-compatible model detection ──────────────────
|
|
330
|
+
|
|
331
|
+
export const YI_MODEL_PATTERN = /(^|[\/\s:_-])yi($|[\-_.:\/\s])/;
|
|
332
|
+
|
|
333
|
+
// ── More OpenAI-compatible model detection (batch 2) ───────────────
|
|
334
|
+
|
|
335
|
+
export const DOUBAO_SEED_PATTERN = /(^|[\/\s:_-])seed($|[\-_.:\/\s])/i;
|
|
336
|
+
|
|
337
|
+
export const PHI_MODEL_PATTERN = /(^|[\/\s:_-])phi($|[\-_.:\/\s])/i;
|
|
338
|
+
|
|
339
|
+
// ── New OpenAI-compatible model detection (batch 3, 12 families) ──────
|
|
340
|
+
|
|
341
|
+
// ── More OpenAI-compatible model detection (batch 4, 18 families) ──
|
|
342
|
+
|
|
343
|
+
// ── Model key ──────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
export function modelKey(model: ModelIdentity): string {
|
|
346
|
+
return `${model.provider}/${model.id}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function isOfficialOpenAIBaseUrl(model: PiModel): boolean {
|
|
350
|
+
const value = lower(model.baseUrl).trim();
|
|
351
|
+
if (!value) {
|
|
352
|
+
return lower(model.provider) === "openai";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
return new URL(value).hostname === "api.openai.com";
|
|
357
|
+
} catch {
|
|
358
|
+
return value === "api.openai.com" || value.startsWith("api.openai.com/");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function isKnownThirdPartyOpenAIEndpoint(model: PiModel): boolean {
|
|
363
|
+
return isNonEmptyString(model.baseUrl) && !isOfficialOpenAIBaseUrl(model);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function invalidateModelsConfigCache(): void {
|
|
367
|
+
modelsConfigCache = undefined;
|
|
368
|
+
}
|