@oh-my-pi/pi-catalog 16.2.9 → 16.2.11

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 CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.2.11] - 2026-07-01
6
+
7
+ ### Fixed
8
+
9
+ - Fixed a potential memory leak caused by dangling timeout timers during model discovery in OpenAI-compatible, vLLM, LiteLLM, and LM Studio catalogs.
10
+ - Widened stream watchdogs for local OpenAI-compatible backends (including llama.cpp, LM Studio, vLLM, and Ollama) to prevent premature timeouts during cold model loads.
11
+
12
+ ## [16.2.10] - 2026-06-30
13
+
14
+ ### Added
15
+
16
+ - Added Claude Sonnet 3.7, Claude Opus 3, and Claude Sonnet 3 model entries to the Anthropic catalog
17
+ - Added Anthropic Claude Sonnet 5 model entry to the Kilo provider catalog
18
+ - Added first-party catalog discovery support for the Anthropic provider
19
+ - Added Gemini 3.1 Flash Lite Image model entry to the Kilo provider catalog
20
+ - Added Anthropic Claude Sonnet 5 model variants with low, medium, high, xhigh, and max thinking efforts to the Devin provider catalog
21
+ - Added Claude Sonnet 5 model entry to the Anthropic curated catalog.
22
+
23
+ ### Changed
24
+
25
+ - Updated the base API URL for the Claude Sonnet 5 model in the Anthropic catalog
26
+ - Updated pricing metrics for DeepSeek R1 and DeepSeek V3 model entries to reflect new rates
27
+
5
28
  ## [16.2.9] - 2026-06-30
6
29
 
7
30
  ### Added
@@ -48,8 +48,10 @@ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
48
48
  apiKey?: string;
49
49
  /** Additional request headers. */
50
50
  headers?: Record<string, string>;
51
- /** Optional AbortSignal for request cancellation. */
51
+ /** Optional AbortSignal for request cancellation; caller owns its lifecycle. */
52
52
  signal?: AbortSignal;
53
+ /** Optional cancellable request timeout used when `signal` is omitted. */
54
+ timeoutMs?: number;
53
55
  /** Optional fetch implementation override for testing/custom runtimes. */
54
56
  fetch?: FetchImpl;
55
57
  /**
@@ -29,7 +29,11 @@ export declare const CATALOG_PROVIDERS: readonly [{
29
29
  }, {
30
30
  readonly id: "anthropic";
31
31
  readonly defaultModel: "claude-opus-4-8";
32
+ readonly envVars: readonly ["ANTHROPIC_API_KEY"];
32
33
  readonly createModelManagerOptions: (config: ModelManagerConfig) => import("..").ModelManagerOptions<"anthropic-messages", unknown>;
34
+ readonly catalogDiscovery: {
35
+ readonly label: "Anthropic";
36
+ };
33
37
  }, {
34
38
  readonly id: "azure";
35
39
  readonly defaultModel: "gpt-5.5";
@@ -29,8 +29,8 @@ export interface ModelsDevModel {
29
29
  * first-party `/v1/models` endpoint but that models.dev has not catalogued yet.
30
30
  * Seeded into model generation so the bundled catalog is never gated on
31
31
  * models.dev's update cadence; deduped behind upstream catalog / models.dev
32
- * entries once those appear. Token limits and pricing are pinned
33
- * authoritatively in `applyAnthropicCatalogPolicy`, and `thinking` is re-baked
32
+ * entries once those appear. Token limits and pricing are pinned either directly or
33
+ * in `applyAnthropicCatalogPolicy`, and `thinking` is re-baked
34
34
  * by the generator's policy pass (scripts/generated-policies.ts).
35
35
  */
36
36
  export declare const ANTHROPIC_CURATED_FALLBACK_MODELS: readonly ModelSpec<"anthropic-messages">[];
@@ -371,6 +371,7 @@ export interface FetchLiteLLMRichModelsOptions<TApi extends Api> {
371
371
  headers?: Record<string, string>;
372
372
  fetch?: FetchImpl;
373
373
  signal?: AbortSignal;
374
+ timeoutMs?: number;
374
375
  referenceResolver?: (modelId: string) => ModelSpec<TApi> | undefined;
375
376
  }
376
377
  export declare const OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW = 128000;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-catalog",
4
- "version": "16.2.9",
4
+ "version": "16.2.11",
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.0",
37
- "@oh-my-pi/pi-utils": "16.2.9",
37
+ "@oh-my-pi/pi-utils": "16.2.11",
38
38
  "arktype": "^2.2.0",
39
39
  "zod": "^4"
40
40
  },
41
41
  "devDependencies": {
42
- "@oh-my-pi/pi-ai": "16.2.9",
42
+ "@oh-my-pi/pi-ai": "16.2.11",
43
43
  "@types/bun": "^1.3.14"
44
44
  },
45
45
  "engines": {
@@ -57,6 +57,8 @@ function matchesKimiK27CodeFamily(spec: ModelSpec<"openai-completions">): boolea
57
57
  const XIAOMI_MIMO_STREAM_IDLE_TIMEOUT_MS = 300_000;
58
58
  /** Alibaba Coding Plan (coding-intl.dashscope) qwen models idle before the first event (issue #1770). */
59
59
  const ALIBABA_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;
60
+ /** Local OpenAI-compatible backends can spend minutes cold-loading a model before the first SSE event. */
61
+ const LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000;
60
62
  const MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;
61
63
  const DSML_HEALING_PROVIDERS = new Set([
62
64
  "ollama",
@@ -294,6 +296,9 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
294
296
  isMoonshotNative ||
295
297
  isOpenCodeHost;
296
298
  const isOpenCodeProvider = provider === "opencode-go" || provider === "opencode-zen";
299
+ const isLocalOpenAICompatBackend =
300
+ !PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&
301
+ (LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl));
297
302
 
298
303
  const useMaxTokens =
299
304
  isMistral ||
@@ -348,9 +353,10 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
348
353
  isCopilotHost ||
349
354
  isZenmuxHost);
350
355
 
351
- // Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, and direct
352
- // DeepSeek reasoning models can idle for minutes while reasoning; widen the
353
- // idle timeout so warm-ups stop aborting and retrying.
356
+ // Stream-watchdog floor: GLM coding-plan SKUs, Kimi K2.6, direct
357
+ // DeepSeek reasoning models, and local OpenAI-compatible backends can idle
358
+ // for minutes while reasoning or cold-loading weights; widen the idle
359
+ // timeout so warm-ups stop aborting and retrying.
354
360
  const streamIdleTimeoutMs =
355
361
  GLM_CODING_PLAN_MODEL_PATTERN.test(spec.id) && (isZai || isZhipu)
356
362
  ? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS
@@ -362,7 +368,9 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
362
368
  ? KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS
363
369
  : spec.reasoning && isDirectDeepseekApi
364
370
  ? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS
365
- : undefined;
371
+ : isLocalOpenAICompatBackend
372
+ ? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS
373
+ : undefined;
366
374
 
367
375
  // Fireworks "Fast" variants (`<id>-fast`) are served from the router
368
376
  // namespace (`accounts/fireworks/routers/<id>-fast`), like Fire Pass, rather
@@ -482,9 +490,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
482
490
  // when a thinking block actually exists on the turn
483
491
  // (`nonEmptyThinkingBlocks.length > 0`), so the flag is a no-op on
484
492
  // pure-text histories.
485
- replayReasoningContent:
486
- !PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&
487
- (LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl)),
493
+ replayReasoningContent: isLocalOpenAICompatBackend,
488
494
  // `preserve_thinking: true` makes the Qwen3.6+ chat template render
489
495
  // `<think>...</think>` for older assistant turns too, instead of
490
496
  // stripping it the moment a new user message moves them past
@@ -496,9 +502,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
496
502
  // with `replayReasoningContent` above). Non-Qwen templates ignore the
497
503
  // parameter, so the flag stays a no-op outside the Qwen path.
498
504
  qwenPreserveThinking:
499
- (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") &&
500
- !PROXY_OPENAI_COMPAT_PROVIDERS.has(provider) &&
501
- (LOCAL_OPENAI_COMPAT_PROVIDERS.has(provider) || hasLocalLoopbackBaseUrl(baseUrl)),
505
+ (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") && isLocalOpenAICompatBackend,
502
506
  requiresAssistantContentForToolCalls: isKimiModel || isDirectDeepseekReasoning,
503
507
  cacheControlFormat: isOpenRouter && spec.id.startsWith("anthropic/") ? "anthropic" : undefined,
504
508
  openRouterRouting: undefined,
@@ -584,6 +588,9 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
584
588
  const isAnthropicModel = id ? isClaudeModelId(id) || isAnthropicNamespacedModelId(id) : false;
585
589
  const isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;
586
590
  const reasoningCapable = Boolean(spec.reasoning);
591
+ const isLocalOpenAICompatBackend =
592
+ !PROXY_OPENAI_COMPAT_PROVIDERS.has(spec.provider) &&
593
+ (LOCAL_OPENAI_COMPAT_PROVIDERS.has(spec.provider) || hasLocalLoopbackBaseUrl(baseUrl));
587
594
 
588
595
  const compat: ResolvedOpenAIResponsesCompat = {
589
596
  supportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, "githubCopilot"),
@@ -645,7 +652,9 @@ export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): Resol
645
652
  emptyLengthFinishIsContextError: spec.provider === "ollama",
646
653
  usesOpenAIToolCallIdLimit: spec.provider === "openai",
647
654
  promptCacheSessionHeader: spec.provider === "xai-oauth" ? "x-grok-conv-id" : undefined,
648
- streamIdleTimeoutMs: spec.compat?.streamIdleTimeoutMs,
655
+ streamIdleTimeoutMs: isLocalOpenAICompatBackend
656
+ ? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS
657
+ : spec.compat?.streamIdleTimeoutMs,
649
658
  };
650
659
  applyCompatOverrides(compat, spec.compat);
651
660
  if (spec.compat?.reasoningDisableMode === undefined) {
@@ -3,6 +3,27 @@ import type { Api, FetchImpl, ModelSpec, Provider } from "../types";
3
3
 
4
4
  const MODELS_PATH = "/models";
5
5
 
6
+ /**
7
+ * Uses a cancellable timer rather than the native abort-timeout helper so
8
+ * successful fast discovery requests do not leave armed timeout signals for
9
+ * concurrent GC to trip over later.
10
+ */
11
+ async function withOpenAICompatibleDiscoveryTimeout<T>(
12
+ timeoutMs: number,
13
+ run: (signal: AbortSignal) => Promise<T>,
14
+ ): Promise<T> {
15
+ const controller = new AbortController();
16
+ const timer = setTimeout(
17
+ () => controller.abort(new DOMException("The operation timed out.", "TimeoutError")),
18
+ timeoutMs,
19
+ );
20
+ try {
21
+ return await run(controller.signal);
22
+ } finally {
23
+ clearTimeout(timer);
24
+ }
25
+ }
26
+
6
27
  /**
7
28
  * Minimal OpenAI-style model entry shape consumed by discovery.
8
29
  *
@@ -72,8 +93,10 @@ export interface FetchOpenAICompatibleModelsOptions<TApi extends Api> {
72
93
  apiKey?: string;
73
94
  /** Additional request headers. */
74
95
  headers?: Record<string, string>;
75
- /** Optional AbortSignal for request cancellation. */
96
+ /** Optional AbortSignal for request cancellation; caller owns its lifecycle. */
76
97
  signal?: AbortSignal;
98
+ /** Optional cancellable request timeout used when `signal` is omitted. */
99
+ timeoutMs?: number;
77
100
  /** Optional fetch implementation override for testing/custom runtimes. */
78
101
  fetch?: FetchImpl;
79
102
  /**
@@ -115,25 +138,35 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
115
138
  }
116
139
 
117
140
  const fetchImpl = options.fetch ?? globalThis.fetch;
118
- let response: Response;
119
- try {
120
- response = await fetchImpl(`${baseUrl}${MODELS_PATH}`, {
121
- method: "GET",
122
- headers: requestHeaders,
123
- signal: options.signal,
124
- });
125
- } catch {
126
- return null;
127
- }
141
+ const fetchPayload = async (signal?: AbortSignal): Promise<unknown | null> => {
142
+ let response: Response;
143
+ try {
144
+ response = await fetchImpl(`${baseUrl}${MODELS_PATH}`, {
145
+ method: "GET",
146
+ headers: requestHeaders,
147
+ signal,
148
+ });
149
+ } catch {
150
+ return null;
151
+ }
128
152
 
129
- if (!response.ok) {
130
- return null;
131
- }
153
+ if (!response.ok) {
154
+ return null;
155
+ }
132
156
 
133
- let payload: unknown;
134
- try {
135
- payload = await response.json();
136
- } catch {
157
+ try {
158
+ return await response.json();
159
+ } catch {
160
+ return null;
161
+ }
162
+ };
163
+ const payload =
164
+ options.signal !== undefined
165
+ ? await fetchPayload(options.signal)
166
+ : options.timeoutMs !== undefined
167
+ ? await withOpenAICompatibleDiscoveryTimeout(options.timeoutMs, fetchPayload)
168
+ : await fetchPayload();
169
+ if (payload === null) {
137
170
  return null;
138
171
  }
139
172