pi-freeflow 1.32.2 → 1.33.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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.33.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Removed one free model from the catalog that could never be used in a coding session. It answers a plain chat request fine, but rejects any request that carries tool definitions — and coding agents always send tools, so picking it failed on every turn with a "no endpoints found that support tool use" error. The catalog is now 30 models.
8
+
9
+ ## 1.33.0
10
+
11
+ ### Minor Changes
12
+
13
+ - One new free model, picked the same way as the rest (`/model` → `freeflow` → pick):
14
+
15
+ - **Ling 3.1 Flash** on KiloCode (`inclusionai/ling-3.1-flash`, short name `ling-3.1-flash:kilo`) — 262K context, text answers.
16
+
17
+ ### Patch Changes
18
+
19
+ - Remove four free models that stopped serving upstream, so they no longer appear in the model list:
20
+
21
+ - **MiMo V2.5** on OpenCode (`mimo-v2.5-free`) — upstream answers `401 Model is not supported`.
22
+ - **Exo** on OpenCode (`exo-free`) — upstream answers `503 Endpoint is unavailable`.
23
+ - **Space Bunny Alpha** on KiloCode (`stealth/space-bunny-alpha`) — upstream answers `404 does not exist`.
24
+ - **Qwen 3.8 27B** on KiloCode (`qwen/qwen3.8-27b:free`) — upstream answers `404 currently unavailable`.
25
+
26
+ All four are excluded permanently, so a stale on-disk catalog or a later catalog refresh cannot bring them back.
27
+
28
+ - Requests that ask for a thinking level a model does not offer no longer fail: the level is clamped to the highest level that model supports (previously only Muse Spark was covered — now every model is), and a chat-style `response_format` left on a responses request is dropped instead of rejected upstream.
29
+
3
30
  ## 1.32.2
4
31
 
5
32
  ### Patch Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-freeflow
2
2
 
3
- > 33 free models with up to 1M context. No API keys to manage. Add your own relays to spread requests across more IPs.
3
+ > 30 free models with up to 1M context. No API keys to manage. Add your own relays to spread requests across more IPs.
4
4
 
5
5
  Thin by design: a model list, a relay proxy, and a log. The host (`pi-ai`) handles thinking, normalization, and provider behavior.
6
6
 
@@ -16,7 +16,7 @@ Thin by design: a model list, a relay proxy, and a log. The host (`pi-ai`) handl
16
16
 
17
17
  | Feature | What it does | Cost |
18
18
  | :--- | :--- | :--- |
19
- | **33 free models** | 11 from OpenCode Zen, 17 from KiloCode Gateway, 5 from Cline, context windows up to 1M. Full list below. | **$0** |
19
+ | **30 free models** | 10 from OpenCode Zen, 15 from KiloCode Gateway, 5 from Cline, context windows up to 1M. Full list below. | **$0** |
20
20
  | **Relay pool** | Route requests through your own Cloudflare Workers and Vercel Edge relays. Traffic spreads across the pool, or stays on one sticky relay. A relay that rate-limits, times out, or drops the connection cools down while healthy ones take its traffic. | **$0** beyond your platforms' free tiers |
21
21
  | **Automatic fallback** | When every relay is cooling down, requests go direct to upstream instead of failing. | **$0** |
22
22
  | **Short model names** | Every model has a slash-free, colon-free alias, plus an optional `:effort` suffix for thinking depth. You type `freeflow/<name>`. | **$0** |
@@ -185,13 +185,13 @@ The same command set works identically in OMP and Pi:
185
185
 
186
186
  ---
187
187
 
188
- ### 33 models, one command
188
+ ### 30 models, one command
189
189
 
190
190
  ```bash
191
191
  /model → freeflow → pick
192
192
  ```
193
193
 
194
- #### OpenCode Zen (11 models), Responses and Chat API
194
+ #### OpenCode Zen (10 models), Responses and Chat API
195
195
 
196
196
  Good defaults for long coding sessions and agentic work.
197
197
 
@@ -199,7 +199,6 @@ Good defaults for long coding sessions and agentic work.
199
199
  | :--- | :--- | :--- | :--- | :--- | :--- |
200
200
  | `muse-spark-1.2-contributor-free` | Meta Superintelligence Labs | **1M** (1.048.576) | **131K** (131.072) | `minimal … xhigh` | ✅ |
201
201
  | `muse-spark-1.3-contributor-free` | Meta Superintelligence Labs | **1M** (1.048.576) | **131K** (131.072) | `minimal … xhigh` | ✅ |
202
- | `mimo-v2.5-free` | Xiaomi MiMo | **1M** (1.048.576) | **131K** (131.072) | `minimal … xhigh`\* | ✅ |
203
202
  | `mimo-v2.6-flash-free` | Xiaomi MiMo | **1M** (1.048.576) | **131K** (131.072) | `minimal … xhigh`\* | ✅ |
204
203
  | `nemotron-3.5-lightning-free` | NVIDIA | **1M** (1.000.000) | **262K** (262.144) | `minimal … xhigh` | ❌ |
205
204
  | `nemotron-3-ultra-free` | NVIDIA | **1M** (1.000.000) | **128K** (128.000) | `minimal … xhigh` | ❌ |
@@ -209,7 +208,7 @@ Good defaults for long coding sessions and agentic work.
209
208
  | `fledge-alpha-free` | Stealth preview (lab undisclosed) | **1M** (1.048.576) | **131K** (131.072) | `low / high / max` | ✅ |
210
209
  | `ling-3.1-flash-free` | Inclusion AI | **262K** (262.144) | **32K** (32.768) | `minimal … max` | ❌ |
211
210
 
212
- #### KiloCode Gateway (17 models), OpenRouter compatible
211
+ #### KiloCode Gateway (16 models), OpenRouter compatible
213
212
 
214
213
  Keyless access. Short aliases work for every row (the full ID is in parentheses).
215
214
 
@@ -226,12 +225,10 @@ Keyless access. Short aliases work for every row (the full ID is in parentheses)
226
225
  | `lfm-2.5` (`liquid/lfm-2.5-2.6b:free`) | Liquid AI | **65K** (65.536) | **32K** (32.768) | `minimal…xhigh`\* | ❌ |
227
226
  | `kilo-auto` (`kilo-auto/free`) | Kilo Gateway Auto | **256K** (256.000) | **10K** (10.000) | `minimal…xhigh`\* | ❌ |
228
227
  | `openrouter` (`openrouter/free`) | OpenRouter Free | **200K** (200.000) | **65K** (65.536) | `minimal…xhigh`\* | ✅ |
229
- | `content-safety` (`nvidia/...:free`) | NVIDIA | **128K** (128.000) | **8K** (8.192) | ❌ *(non-thinking)* | ✅ |
230
228
  | `ling-3.0-flash-sante` (`inclusionai/ling-3.0-flash-sante:free`) | Inclusion AI | **262K** (262.144) | **32K** (32.768) | `minimal…xhigh`\* | ❌ |
231
229
  | `step-3.7-flash` (`stepfun/...:free`) | StepFun | **262K** (262.144) | **262K** (262.144) | `minimal…xhigh`\* | ✅ |
232
- | `space-bunny-alpha` (`stealth/...`) | Stealth preview (lab undisclosed) | **1M** (1.000.000) | **512K** (524.288) | `minimal…xhigh`\* | ✅ |
233
230
  | `inkling-small` (`thinkingmachines/inkling-small:free`) | Thinking Machines | **1M** (1.048.576) | **262K** (262.144) | `minimal…xhigh`\* | ✅ |
234
- | `qwen3.8-27b` (`qwen/...:free`) | Alibaba Qwen | **262K** (262.144) | **235K** (235.929) | `minimal…xhigh`\* | ✅ |
231
+ | `ling-3.1-flash-kilo` (`inclusionai/ling-3.1-flash`) | Inclusion AI | **262K** (262.144) | **32K** (32.768) | `minimal…xhigh`\* | ❌ |
235
232
 
236
233
  \* Levels are forwarded as-is through the OpenRouter-style nested `reasoning` parameter; effort mapping is decided by each model. MiMo collapses `minimal→low` and `xhigh→high` upstream, so its selector shows 5 labels but only 3 distinct effort values.
237
234
 
@@ -344,7 +341,7 @@ pnpm smoke # verifies extensions/index.ts loads without crashing
344
341
  ```
345
342
  src/
346
343
  ├── index.ts # extension entry, lifecycle hooks
347
- ├── models.ts # 33-model catalog definitions
344
+ ├── models.ts # 30-model catalog definitions
348
345
  ├── catalog.ts # model catalog cache (24h disk)
349
346
  ├── proxy.ts # local proxy server (127.0.0.1:28180)
350
347
  ├── relay.ts # relay fetch, rolling on retryable failures
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-freeflow",
3
3
  "type": "module",
4
- "version": "1.32.2",
4
+ "version": "1.33.1",
5
5
  "description": "Thin provider for OMP/Pi — model list + dumb relay proxy + log; host pi-ai owns thinking/normalization",
6
6
  "main": "extensions/index.ts",
7
7
  "types": "src/index.ts",
package/src/catalog.ts CHANGED
@@ -42,6 +42,18 @@ import type {
42
42
  * reproduced across separate runs on different days while every other Zen model answers.
43
43
  * - inclusionai/ling-3.0-flash-fin:free: gone from Kilo (2026-10-01) — keyless chat 404
44
44
  * "The requested model ... does not exist".
45
+ * - mimo-v2.5-free: decommissioned upstream (2026-10-07) — HTTP 401 "Model mimo-v2.5-free is not supported".
46
+ * - exo-free: unserved upstream (2026-10-07) — HTTP 503 "Error from provider (Console): Upstream request failed: Endpoint is unavailable.".
47
+ * - stealth/space-bunny-alpha: gone from Kilo (2026-10-07) — keyless chat 404 "The requested model ... does not exist".
48
+ * - qwen/qwen3.8-27b:free: gone from Kilo (2026-10-07) — keyless chat 404 "The requested model is currently unavailable.".
49
+ * - nvidia/nemotron-3.5-content-safety:free: tool-less guardrail model (2026-10-08),
50
+ * NOT decommissioned — it answers 200 keyless chat with no tools, but every
51
+ * tool-bearing request 404s "No endpoints found that support tool use" (Kilo
52
+ * `supported_parameters` lists reasoning only, no tools). It is a 4B
53
+ * moderation classifier fine-tuned from Gemma-3-4B that "moderates inputs to
54
+ * and responses from LLMs", not a chat model. Same exclusion class as
55
+ * jev-1.13-free: live, wrong shape for an agentic harness, so it must never
56
+ * re-enter the picker via disk cache or upstream merge.
45
57
  */
46
58
  export const DEAD_MODEL_IDS = new Set<string>([
47
59
  "jev-1.13-free",
@@ -61,13 +73,18 @@ export const DEAD_MODEL_IDS = new Set<string>([
61
73
  "inclusionai/ling-3.0-flash-vl:free",
62
74
  "ling-3.0-flash-fin-free",
63
75
  "inclusionai/ling-3.0-flash-fin:free",
76
+ "mimo-v2.5-free",
77
+ "exo-free",
78
+ "stealth/space-bunny-alpha",
79
+ "qwen/qwen3.8-27b:free",
80
+ "nvidia/nemotron-3.5-content-safety:free",
64
81
  ]);
65
82
  /**
66
83
  * Free-tier allowlist for anything entering the picker via network or stale disk.
67
84
  * Upstream lists paid models alongside free ones (e.g. claude-fable-5-1,
68
85
  * claude-opus-4-*, gemini-3-*) so a bare upstream merge leaks paid entries that
69
86
  * fail with 401 Missing API key. Known static IDs without a free suffix
70
- * (e.g. big-pickle, stealth/space-bunny-alpha) stay allowed via MODEL_MAP.
87
+ * (e.g. big-pickle, inclusionai/ling-3.1-flash) stay allowed via MODEL_MAP.
71
88
  */
72
89
  export function isFreeCatalogId(id: string): boolean {
73
90
  if (typeof id !== "string" || id.length === 0) return false;
@@ -100,7 +117,7 @@ export function sanitizeCatalogModels(models: RegisteredModel[]): RegisteredMode
100
117
  }
101
118
  /**
102
119
  * In-memory cache of currently active/available free models.
103
- * Initialized with all 33 verified models for 0ms instant availability.
120
+ * Initialized with all 30 verified models for 0ms instant availability.
104
121
  */
105
122
  let aliveCatalog: RegisteredModel[] = ALL_MODELS.map((m) => ({
106
123
  ...m,
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * pi-freeflow — Modular, high-resiliency LLM extension for Pi & Oh My Pi (OMP)
3
3
  *
4
- * Provides access to 33 free models (11 OpenCode Zen + 17 KiloCode Gateway + 5 Cline) with:
4
+ * Provides access to 30 free models (10 OpenCode Zen + 15 KiloCode Gateway + 5 Cline) with:
5
5
  * - Single-port daemon reuse on 28180 across concurrent subagents
6
6
  * - Multi-cloud rolling egress relays (Vercel Edge, Cloudflare, Deno)
7
7
  * - 0ms instant startup with verified static catalog and background live health checks
package/src/models.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Static model definitions and upstream routing catalogs for pi-freeflow
3
3
  *
4
- * Defines the 33 verified free models (live-verified 2026-09-28):
4
+ * Defines the 30 verified free models (live-verified 2026-10-08):
5
5
  * - 10 OpenCode Zen models (2 Responses API + 8 Chat Completions)
6
- * - 18 KiloCode Keyless Gateway models (OpenRouter format)
6
+ * - 15 KiloCode Keyless Gateway models (OpenRouter format)
7
7
  * - 5 Cline direct-only models (per-user pool)
8
8
  */
9
9
 
@@ -50,23 +50,6 @@ export const OPENCODE_MODELS: ModelDef[] = [
50
50
  max: null,
51
51
  },
52
52
  },
53
- {
54
- id: "mimo-v2.5-free",
55
- name: "MiMo V2.5 [OpenCode]",
56
- reasoning: true,
57
- contextWindow: 1_048_576,
58
- maxTokens: 131_072,
59
- input: ["text", "image"],
60
- thinkingLevelMap: {
61
- off: null,
62
- minimal: "low",
63
- low: "low",
64
- medium: "medium",
65
- high: "high",
66
- xhigh: "high",
67
- max: null,
68
- },
69
- },
70
53
  {
71
54
  id: "mimo-v2.6-flash-free",
72
55
  name: "MiMo V2.6 Flash [OpenCode]",
@@ -336,14 +319,6 @@ export const KILO_MODELS: ModelDef[] = [
336
319
  thinkingFormat: "openrouter",
337
320
  thinkingLevelMap: KILO_REASONING_MAP,
338
321
  },
339
- {
340
- id: "nvidia/nemotron-3.5-content-safety:free",
341
- name: "Nemotron Content Safety [Kilo]",
342
- reasoning: false,
343
- contextWindow: 128_000,
344
- maxTokens: 8_192,
345
- input: ["text", "image"],
346
- },
347
322
  {
348
323
  // Resurrected 2026-09-28: back on the live Kilo free list
349
324
  // (isFree:true, 0/0 pricing, no expiry) + keyless chat 200.
@@ -356,19 +331,6 @@ export const KILO_MODELS: ModelDef[] = [
356
331
  thinkingFormat: "openrouter",
357
332
  thinkingLevelMap: KILO_REASONING_MAP,
358
333
  },
359
- {
360
- // Added 2026-09-28: live on the Kilo free list (isFree:true, 0/0
361
- // pricing, 1M context, text+image in) + keyless chat 200. Passes
362
- // isFreeCatalogId via MODEL_MAP (no :free suffix by design).
363
- id: "stealth/space-bunny-alpha",
364
- name: "Space Bunny Alpha [Kilo]",
365
- reasoning: true,
366
- contextWindow: 1_000_000,
367
- maxTokens: 524_288,
368
- input: ["text", "image"],
369
- thinkingFormat: "openrouter",
370
- thinkingLevelMap: KILO_REASONING_MAP,
371
- },
372
334
  {
373
335
  id: "thinkingmachines/inkling-small:free",
374
336
  name: "Inkling Small [Kilo]",
@@ -390,12 +352,13 @@ export const KILO_MODELS: ModelDef[] = [
390
352
  thinkingLevelMap: KILO_REASONING_MAP,
391
353
  },
392
354
  {
393
- id: "qwen/qwen3.8-27b:free",
394
- name: "Qwen 3.8 27B [Kilo]",
355
+ // Added 2026-10-07: live on Kilo free list (isFree:true, 0/0 pricing, 262K ctx)
356
+ id: "inclusionai/ling-3.1-flash",
357
+ name: "Ling 3.1 Flash [Kilo]",
395
358
  reasoning: true,
396
359
  contextWindow: 262_144,
397
- maxTokens: 235_929,
398
- input: ["text", "image"],
360
+ maxTokens: 32_768,
361
+ input: ["text"],
399
362
  thinkingFormat: "openrouter",
400
363
  thinkingLevelMap: KILO_REASONING_MAP,
401
364
  },
@@ -514,10 +477,8 @@ export const MODEL_ALIASES: Record<string, string> = {
514
477
  "nemotron-3-super": "nvidia/nemotron-3-super-120b-a12b:free",
515
478
  "north-mini-code": "cohere/north-mini-code:free",
516
479
  "lfm-2.5": "liquid/lfm-2.5-2.6b:free",
517
- "content-safety": "nvidia/nemotron-3.5-content-safety:free",
518
480
  "ling-3.0-flash-sante": "inclusionai/ling-3.0-flash-sante:free",
519
481
  "step-3.7-flash": "stepfun/step-3.7-flash:free",
520
- "space-bunny-alpha": "stealth/space-bunny-alpha",
521
482
  "inkling-small": "thinkingmachines/inkling-small:free",
522
483
  "mimo-v2.6-flash": "mimo-v2.6-flash-free",
523
484
  "space-bunny": "space-bunny-free",
@@ -525,7 +486,7 @@ export const MODEL_ALIASES: Record<string, string> = {
525
486
  "longcat": "longcat-2.5-preview-free",
526
487
  "fledge-alpha": "fledge-alpha-free",
527
488
  "ling-3.1-flash": "ling-3.1-flash-free",
528
- "qwen3.8-27b": "qwen/qwen3.8-27b:free",
489
+ "ling-3.1-flash:kilo": "inclusionai/ling-3.1-flash",
529
490
  // provider-prefixed short aliases (slash-normalized)
530
491
  "laguna-s-2.1:free": "poolside/laguna-s-2.1:free",
531
492
  "laguna-xs-2.1:free": "poolside/laguna-xs-2.1:free",
@@ -559,9 +520,8 @@ export const CLINE_MODEL_IDS = new Set<string>([
559
520
  .filter(([_, target]) => CLINE_MODELS.some((cm) => cm.id === target))
560
521
  .map(([alias]) => alias),
561
522
  ]);
562
-
563
523
  /**
564
- * Combined list of all 33 static free models (canonical)
524
+ * Combined list of all 30 static free models (canonical)
565
525
  */
566
526
  export const ALL_MODELS: ModelDef[] = [...OPENCODE_MODELS, ...KILO_MODELS, ...CLINE_MODELS];
567
527
 
@@ -38,6 +38,35 @@ import {
38
38
  type FindGlobRestore,
39
39
  type FingerprintToolName,
40
40
  } from "./tool-translation.ts";
41
+ import { getModelDef } from "./models.ts";
42
+ import type { ThinkingLevel } from "./types.ts";
43
+
44
+ /** Effort levels in descending priority (off excluded — clamping to off is never correct). */
45
+ const EFFORT_DESCENDING: ThinkingLevel[] = ["max", "xhigh", "high", "medium", "low", "minimal"];
46
+
47
+ /**
48
+ * Find the highest supported effort for a model whose thinkingLevelMap
49
+ * declares the requested effort as null. Returns null when no clamp is
50
+ * needed (effort is supported or model has no map).
51
+ */
52
+ function clampedEffortFor(model: string, effort: string): string | null {
53
+ const def = getModelDef(model);
54
+ if (!def?.thinkingLevelMap) return null;
55
+ const map = def.thinkingLevelMap;
56
+ // "off" means disable reasoning entirely — never clamp it even when map says null
57
+ if (effort.toLowerCase() === "off") return null;
58
+ // "ultra" is an unofficial alias for beyond-max; treat as "max"
59
+ const key = (effort.toLowerCase() === "ultra" ? "max" : effort.toLowerCase()) as ThinkingLevel;
60
+ // Effort is supported (non-null) → no clamp
61
+ if (key in map && map[key] !== null && map[key] !== undefined) return null;
62
+ // Effort key is unknown and not "ultra" → let upstream decide
63
+ if (!(key in map) && effort.toLowerCase() !== "ultra") return null;
64
+ // Find highest supported effort
65
+ for (const level of EFFORT_DESCENDING) {
66
+ if (map[level] !== null && map[level] !== undefined) return level;
67
+ }
68
+ return null;
69
+ }
41
70
 
42
71
  /**
43
72
  * Safely extract the tool name whether formatted in Chat Completions style
@@ -262,14 +291,15 @@ export function ensureMessagesFingerprintTools(body: Record<string, unknown>, om
262
291
  * - `max_tokens` / `max_completion_tokens` become `max_output_tokens` (copied
263
292
  * when absent, dropped either way).
264
293
  * - `temperature` / `top_p` are dropped (Responses rejects them).
294
+ * - `response_format` is dropped (Chat Completions field, Responses rejects it).
265
295
  * - `parallel_tool_calls: false` is dropped; absent-or-true rides verbatim.
266
296
  * - `tool_choice` is auto-or-absent: `"none"` (string or `{ type: "none" }`)
267
297
  * is dropped, everything else rides (named choices still retarget find->glob).
268
298
  * - `store` defaults to false (never persisted server-side).
269
- * - spark reasoning effort clamps `max` / `ultra` to `xhigh`: the spark
270
- * thinkingLevelMap declares `max: null` (unoffered) and upstream serves at
271
- * most `xhigh`, so a stale or hand-built `max`/`ultra` effort would 400.
272
- * Only spark model ids are clamped; `off: null` stays untouched everywhere.
299
+ * - Reasoning effort is clamped per model: when the model's thinkingLevelMap
300
+ * declares the requested effort as null (unsupported), the effort is clamped
301
+ * to the highest supported level. Covers all OpenCode models generically
302
+ * (muse-spark, mimo, nemotron, etc.), not just a single model family.
273
303
  * Zen-responses only: chat/messages paths keep their own fields, and Kilo/Cline
274
304
  * bodies never reach here (the proxy fingerprints Zen bodies exclusively).
275
305
  */
@@ -287,23 +317,28 @@ export function normalizeResponsesBody(body: Record<string, unknown>): void {
287
317
  delete body.max_completion_tokens;
288
318
  delete body.temperature;
289
319
  delete body.top_p;
320
+ delete body.response_format;
290
321
  if (body.parallel_tool_calls === false) delete body.parallel_tool_calls;
291
322
  const choice = body.tool_choice;
292
323
  if (typeof choice === "string" ? choice.toLowerCase() === "none" : (
293
324
  choice !== null && typeof choice === "object" && !Array.isArray(choice)
294
325
  && (choice as Record<string, unknown>).type === "none"
295
326
  )) delete body.tool_choice;
296
- if (typeof body.model === "string" && /muse-spark/i.test(body.model)) {
327
+ if (typeof body.model === "string") {
297
328
  const reasoning = body.reasoning;
298
329
  if (reasoning !== null && typeof reasoning === "object" && !Array.isArray(reasoning)) {
299
330
  const effort = (reasoning as Record<string, unknown>).effort;
300
- if (typeof effort === "string" && (effort.toLowerCase() === "max" || effort.toLowerCase() === "ultra")) {
301
- (reasoning as Record<string, unknown>).effort = "xhigh";
331
+ if (typeof effort === "string") {
332
+ const clamped = clampedEffortFor(body.model as string, effort);
333
+ if (clamped !== null) (reasoning as Record<string, unknown>).effort = clamped;
302
334
  }
303
335
  }
304
336
  for (const key of ["reasoning_effort", "reasoningEffort"] as const) {
305
337
  const flat = body[key];
306
- if (typeof flat === "string" && (flat.toLowerCase() === "max" || flat.toLowerCase() === "ultra")) body[key] = "xhigh";
338
+ if (typeof flat === "string") {
339
+ const clamped = clampedEffortFor(body.model as string, flat);
340
+ if (clamped !== null) body[key] = clamped;
341
+ }
307
342
  }
308
343
  }
309
344
  if (body.store === undefined) body.store = false;