@samrito/pi-cliproxyapi-provider 0.16.0 → 0.17.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/README.md +13 -0
- package/package.json +1 -1
- package/src/matching.ts +207 -3
- package/src/provider.ts +5 -2
package/README.md
CHANGED
|
@@ -224,6 +224,19 @@ Aliases affect metadata only. The package still sends the original CLIProxyAPI m
|
|
|
224
224
|
|
|
225
225
|
When `/v1/models` reports a canonical owner such as `openai`, the package uses that provider's metadata even if models.dev lists the model under several providers. Noncanonical owners can embed a provider hint, so `feedmob-opencode-go` resolves to `opencode-go` when that provider publishes the model. If ownership is still unresolved, the package uses OpenRouter metadata by default when there is exactly one matching OpenRouter entry. Set `metadataFallbackProvider` to another models.dev provider ID, or to `null`/`"none"` to disable this fallback. Legacy metadata caches without source-provider identity are ignored in favor of the bundled provider-qualified catalog until metadata is refreshed. Add an alias when CLIProxyAPI's reported owner or fallback does not match the provider whose limits and pricing apply to your setup.
|
|
226
226
|
|
|
227
|
+
When none of the above resolves a model, the package falls back to vendor
|
|
228
|
+
inference: it strips proxy variant suffixes such as `-high`, `-low`, `-medium`,
|
|
229
|
+
`-fast`, `-build`, and `-preview`, and infers the upstream vendor from the model
|
|
230
|
+
name (`claude-*` → `anthropic`, `gemini-*` → `google`, `deepseek-*` →
|
|
231
|
+
`deepseek`, `grok-*` → `xai`, and so on). This rescues proxy-renamed models
|
|
232
|
+
whose `owned_by` is missing, generic, or misleading — for example
|
|
233
|
+
`deepseek-flash` reported with owner `openai` resolves to
|
|
234
|
+
`deepseek/deepseek-flash`, and `gemini-3.6-flash-high` resolves to
|
|
235
|
+
`google/gemini-3.6-flash`. Models the primary matcher already resolves keep
|
|
236
|
+
their existing match source; inference runs only after the primary matcher
|
|
237
|
+
fails. Add a metadata alias to override an inferred match that does not match
|
|
238
|
+
your billing route.
|
|
239
|
+
|
|
227
240
|
Add global aliases to:
|
|
228
241
|
|
|
229
242
|
```text
|
package/package.json
CHANGED
package/src/matching.ts
CHANGED
|
@@ -16,7 +16,17 @@ const CANONICAL_OWNER_PREFIXES: Record<string, string> = {
|
|
|
16
16
|
cohere: "cohere",
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
export type MetadataMatchMethod =
|
|
19
|
+
export type MetadataMatchMethod =
|
|
20
|
+
| "alias"
|
|
21
|
+
| "exact"
|
|
22
|
+
| "owner-prefix"
|
|
23
|
+
| "owner-hint"
|
|
24
|
+
| "suffix"
|
|
25
|
+
| "normalized-suffix"
|
|
26
|
+
| "provider-fallback"
|
|
27
|
+
| "vendor-prefix"
|
|
28
|
+
| "vendor-first-party"
|
|
29
|
+
| "vendor-hint";
|
|
20
30
|
|
|
21
31
|
export interface MetadataMatch {
|
|
22
32
|
metadataId: string;
|
|
@@ -59,16 +69,33 @@ function ownerHintMatch(
|
|
|
59
69
|
owner: string | undefined,
|
|
60
70
|
candidates: string[],
|
|
61
71
|
catalog: ModelsDevCatalog,
|
|
72
|
+
includeIdPath = false,
|
|
62
73
|
): string | undefined {
|
|
63
74
|
if (!owner) return undefined;
|
|
64
75
|
const ownerTokens = identifierTokens(owner);
|
|
76
|
+
const normalizedOwner = normalizeModelName(owner);
|
|
65
77
|
const matches = candidates.flatMap((metadataId) => {
|
|
66
78
|
const metadata = catalog[metadataId];
|
|
67
79
|
if (!metadata) return [];
|
|
80
|
+
const found = [];
|
|
68
81
|
const provider = sourceProvider(metadataId, metadata);
|
|
69
82
|
const providerTokens = identifierTokens(provider);
|
|
70
|
-
if (
|
|
71
|
-
|
|
83
|
+
if (containsContiguousTokens(ownerTokens, providerTokens)) {
|
|
84
|
+
found.push({ metadataId, tokenCount: providerTokens.length, characterCount: provider.length });
|
|
85
|
+
}
|
|
86
|
+
if (includeIdPath && normalizedOwner) {
|
|
87
|
+
// Reseller entries often keep the upstream provider in the metadata id
|
|
88
|
+
// path (e.g. `poe/xai/grok-3-mini` has id `xai/grok-3-mini`). Match the
|
|
89
|
+
// owner against those path segments as a last-resort vendor hint. The
|
|
90
|
+
// large token count ranks this above sourceProvider token matches.
|
|
91
|
+
const idPathSegments = metadata.id.split("/").slice(0, -1);
|
|
92
|
+
for (const segment of idPathSegments) {
|
|
93
|
+
if (normalizeModelName(segment) === normalizedOwner) {
|
|
94
|
+
found.push({ metadataId, tokenCount: 1000 + segment.length, characterCount: segment.length });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return found;
|
|
72
99
|
});
|
|
73
100
|
if (matches.length === 0) return undefined;
|
|
74
101
|
|
|
@@ -148,3 +175,180 @@ export function findMetadataMatch(
|
|
|
148
175
|
|
|
149
176
|
return undefined;
|
|
150
177
|
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Proxy-side variant suffixes that CLIProxyAPI appends to model IDs while
|
|
181
|
+
* models.dev publishes the base model. Stripping them lets `grok-4.7-build-fast`
|
|
182
|
+
* resolve to `xai/grok-4.7` and `gemini-3.6-flash-high` to
|
|
183
|
+
* `google/gemini-3.6-flash`. Ordered longest-first so compound suffixes strip
|
|
184
|
+
* one segment at a time.
|
|
185
|
+
*/
|
|
186
|
+
const VARIANT_SUFFIXES = [
|
|
187
|
+
"-non-reasoning",
|
|
188
|
+
"-reasoning",
|
|
189
|
+
"-thinking",
|
|
190
|
+
"-preview",
|
|
191
|
+
"-medium",
|
|
192
|
+
"-build",
|
|
193
|
+
"-high",
|
|
194
|
+
"-fast",
|
|
195
|
+
"-low",
|
|
196
|
+
] as const;
|
|
197
|
+
|
|
198
|
+
function stripVariantSuffix(name: string): string | undefined {
|
|
199
|
+
for (const suffix of VARIANT_SUFFIXES) {
|
|
200
|
+
if (name.endsWith(suffix) && name.length > suffix.length) return name.slice(0, -suffix.length);
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Candidate names to try when the primary matcher fails. For each stripped
|
|
207
|
+
* variant it also tries a `-preview` variant, because some proxies drop the
|
|
208
|
+
* `-preview` suffix that models.dev keeps (`gemini-3-flash` vs
|
|
209
|
+
* `google/gemini-3-flash-preview`).
|
|
210
|
+
*/
|
|
211
|
+
function modelNameCandidates(id: string): string[] {
|
|
212
|
+
const modelName = id.split("/").at(-1) ?? id;
|
|
213
|
+
const candidates: string[] = [];
|
|
214
|
+
const push = (name: string) => {
|
|
215
|
+
if (!candidates.includes(name)) candidates.push(name);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
let current = modelName;
|
|
219
|
+
while (true) {
|
|
220
|
+
push(current);
|
|
221
|
+
if (!current.endsWith("-preview")) push(`${current}-preview`);
|
|
222
|
+
const stripped = stripVariantSuffix(current);
|
|
223
|
+
if (!stripped) break;
|
|
224
|
+
current = stripped;
|
|
225
|
+
}
|
|
226
|
+
return candidates;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const MODEL_NAME_PROVIDERS: ReadonlyArray<{ test: RegExp; provider: string }> = [
|
|
230
|
+
{ test: /^claude/, provider: "anthropic" },
|
|
231
|
+
{ test: /^(gemini|gemma|veo|imagen|nano-banana|lyria)/, provider: "google" },
|
|
232
|
+
{ test: /^(gpt-|o1|o3|o4|chatgpt|dall-e|sora|text-embedding|whisper|tts)/, provider: "openai" },
|
|
233
|
+
{ test: /^deepseek/, provider: "deepseek" },
|
|
234
|
+
{ test: /^grok/, provider: "xai" },
|
|
235
|
+
{ test: /^(mistral|codestral|mixtral|ministral|pixtral)/, provider: "mistral" },
|
|
236
|
+
{ test: /^glm/, provider: "zhipuai" },
|
|
237
|
+
{ test: /^qwen/, provider: "alibaba" },
|
|
238
|
+
{ test: /^(kimi|moonshot)/, provider: "moonshotai" },
|
|
239
|
+
{ test: /^(minimax|abab)/, provider: "minimax" },
|
|
240
|
+
{ test: /^nemotron/, provider: "nvidia" },
|
|
241
|
+
{ test: /^command-r/, provider: "cohere" },
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Infer the canonical provider from a model name. This is used only when the
|
|
246
|
+
* primary matcher fails, so it cannot steal matches from the stricter,
|
|
247
|
+
* owner-driven resolution; it exists to rescue proxy-renamed models whose
|
|
248
|
+
* `owned_by` is missing, generic, or misleading (`deepseek-flash` reported as
|
|
249
|
+
* `openai`, for example).
|
|
250
|
+
*/
|
|
251
|
+
function canonicalProviderForModelName(modelName: string): string | undefined {
|
|
252
|
+
const lower = modelName.toLowerCase();
|
|
253
|
+
return MODEL_NAME_PROVIDERS.find(({ test }) => test.test(lower))?.provider;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function matchCandidateWithInference(
|
|
257
|
+
cpaModel: Pick<CpaModel, "id" | "owned_by">,
|
|
258
|
+
candidate: string,
|
|
259
|
+
catalog: ModelsDevCatalog,
|
|
260
|
+
fallbackProvider?: string | null,
|
|
261
|
+
): MetadataMatch | undefined {
|
|
262
|
+
if (catalog[candidate]) {
|
|
263
|
+
return { metadataId: candidate, metadata: catalog[candidate], method: "exact" };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const keys = Object.keys(catalog);
|
|
267
|
+
const exactMetadataCandidates = keys.filter((key) => catalog[key]?.id === candidate);
|
|
268
|
+
const exactMetadataKey = oneMatch(exactMetadataCandidates);
|
|
269
|
+
if (exactMetadataKey) {
|
|
270
|
+
return { metadataId: exactMetadataKey, metadata: catalog[exactMetadataKey], method: "exact" };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const suffixCandidates = keys.filter(
|
|
274
|
+
(key) => metadataModelName(key, catalog[key]) === candidate,
|
|
275
|
+
);
|
|
276
|
+
const normalizedSuffixCandidates = keys.filter(
|
|
277
|
+
(key) => normalizeModelName(metadataModelName(key, catalog[key])) === normalizeModelName(candidate),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const vendor = canonicalProviderForModelName(candidate);
|
|
281
|
+
if (vendor) {
|
|
282
|
+
const vendorKey = `${vendor}/${candidate}`;
|
|
283
|
+
if (catalog[vendorKey]) {
|
|
284
|
+
return { metadataId: vendorKey, metadata: catalog[vendorKey], method: "vendor-prefix" };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (vendor) {
|
|
289
|
+
const firstParty = suffixCandidates.filter((key) => sourceProvider(key, catalog[key]) === vendor);
|
|
290
|
+
const firstPartyKey = oneMatch(firstParty);
|
|
291
|
+
if (firstPartyKey) {
|
|
292
|
+
return { metadataId: firstPartyKey, metadata: catalog[firstPartyKey], method: "vendor-first-party" };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const owner = cpaModel.owned_by?.trim().toLowerCase();
|
|
297
|
+
const hintedKey = ownerHintMatch(owner, normalizedSuffixCandidates, catalog, true);
|
|
298
|
+
if (hintedKey) {
|
|
299
|
+
return { metadataId: hintedKey, metadata: catalog[hintedKey], method: "vendor-hint" };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const suffixKey = oneMatch(suffixCandidates);
|
|
303
|
+
if (suffixKey) {
|
|
304
|
+
return { metadataId: suffixKey, metadata: catalog[suffixKey], method: "suffix" };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const normalizedSuffixKey = oneMatch(normalizedSuffixCandidates);
|
|
308
|
+
if (normalizedSuffixKey) {
|
|
309
|
+
return { metadataId: normalizedSuffixKey, metadata: catalog[normalizedSuffixKey], method: "normalized-suffix" };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (fallbackProvider) {
|
|
313
|
+
const normalizedFallbackProvider = fallbackProvider.trim().toLowerCase();
|
|
314
|
+
const fallbackKey = oneMatch(normalizedSuffixCandidates.filter((metadataId) => {
|
|
315
|
+
const metadata = catalog[metadataId];
|
|
316
|
+
return metadata && sourceProvider(metadataId, metadata).toLowerCase() === normalizedFallbackProvider;
|
|
317
|
+
}));
|
|
318
|
+
if (fallbackKey) {
|
|
319
|
+
return { metadataId: fallbackKey, metadata: catalog[fallbackKey], method: "provider-fallback" };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Resolve metadata when the primary matcher fails, by stripping proxy variant
|
|
328
|
+
* suffixes and inferring the vendor from the model name. This is deliberately a
|
|
329
|
+
* fallback: models the primary matcher resolves keep their existing, more
|
|
330
|
+
* conservative match source.
|
|
331
|
+
*/
|
|
332
|
+
export function inferMetadataMatch(
|
|
333
|
+
cpaModel: Pick<CpaModel, "id" | "owned_by">,
|
|
334
|
+
catalog: ModelsDevCatalog,
|
|
335
|
+
fallbackProvider?: string | null,
|
|
336
|
+
): MetadataMatch | undefined {
|
|
337
|
+
for (const candidate of modelNameCandidates(cpaModel.id)) {
|
|
338
|
+
const match = matchCandidateWithInference(cpaModel, candidate, catalog, fallbackProvider);
|
|
339
|
+
if (match) return match;
|
|
340
|
+
}
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function resolveMetadataMatch(
|
|
345
|
+
cpaModel: Pick<CpaModel, "id" | "owned_by">,
|
|
346
|
+
catalog: ModelsDevCatalog,
|
|
347
|
+
aliases: Record<string, string>,
|
|
348
|
+
fallbackProvider?: string | null,
|
|
349
|
+
): MetadataMatch | undefined {
|
|
350
|
+
return (
|
|
351
|
+
findMetadataMatch(cpaModel, catalog, aliases, fallbackProvider)
|
|
352
|
+
?? inferMetadataMatch(cpaModel, catalog, fallbackProvider)
|
|
353
|
+
);
|
|
354
|
+
}
|
package/src/provider.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CpaModel } from "./cpa.ts";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveMetadataMatch, type MetadataMatchMethod } from "./matching.ts";
|
|
3
3
|
import { getModelApiOverride, isGpt56Model, type ModelApiContext } from "./model-api.ts";
|
|
4
4
|
import { getModelCapabilityOverrides } from "./model-capabilities.ts";
|
|
5
5
|
import { thinkingLevelMapFromReasoningOptions } from "./reasoning-levels.ts";
|
|
@@ -139,6 +139,9 @@ function emptyMatchMethods(): Record<MetadataMatchMethod, number> {
|
|
|
139
139
|
suffix: 0,
|
|
140
140
|
"normalized-suffix": 0,
|
|
141
141
|
"provider-fallback": 0,
|
|
142
|
+
"vendor-prefix": 0,
|
|
143
|
+
"vendor-first-party": 0,
|
|
144
|
+
"vendor-hint": 0,
|
|
142
145
|
};
|
|
143
146
|
}
|
|
144
147
|
|
|
@@ -173,7 +176,7 @@ export function buildProviderModels(
|
|
|
173
176
|
let enriched = 0;
|
|
174
177
|
|
|
175
178
|
const models = cpaModels.map((cpaModel) => {
|
|
176
|
-
const match =
|
|
179
|
+
const match = resolveMetadataMatch(cpaModel, catalog, aliases, metadataFallbackProvider);
|
|
177
180
|
if (!match) {
|
|
178
181
|
unmatchedModelIds.push(cpaModel.id);
|
|
179
182
|
return applyModelOverride(defaultModel(cpaModel, gpt56ContextWindow), overrides);
|