@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samrito/pi-cliproxyapi-provider",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Pi provider package for CLIProxyAPI with automatic model discovery and models.dev enrichment.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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 = "alias" | "exact" | "owner-prefix" | "owner-hint" | "suffix" | "normalized-suffix" | "provider-fallback";
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 (!containsContiguousTokens(ownerTokens, providerTokens)) return [];
71
- return [{ metadataId, tokenCount: providerTokens.length, characterCount: provider.length }];
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 { findMetadataMatch, type MetadataMatchMethod } from "./matching.ts";
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 = findMetadataMatch(cpaModel, catalog, aliases, metadataFallbackProvider);
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);