@samrito/pi-cliproxyapi-provider 0.16.0 → 0.17.1

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/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.1",
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);