openclacky 1.5.18 → 1.5.19

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/docs/session-observation-hooks.md +86 -0
  4. data/lib/clacky/agent/chunk_index.rb +4 -1
  5. data/lib/clacky/agent/history_navigation.rb +6 -1
  6. data/lib/clacky/agent/hook_manager.rb +11 -4
  7. data/lib/clacky/agent.rb +39 -3
  8. data/lib/clacky/billing/platform_billing.rb +9 -0
  9. data/lib/clacky/default_extensions/advisor/hooks/advisor.rb +2 -1
  10. data/lib/clacky/default_extensions/advisor/hooks/advisor_complete.rb +3 -2
  11. data/lib/clacky/default_skills/channel-manager/SKILL.md +70 -9
  12. data/lib/clacky/default_skills/channel-manager/qq_setup.rb +79 -0
  13. data/lib/clacky/default_skills/media-gen/SKILL.md +45 -14
  14. data/lib/clacky/locales/en.rb +1 -1
  15. data/lib/clacky/locales/zh.rb +1 -1
  16. data/lib/clacky/media/openai_compat.rb +89 -2
  17. data/lib/clacky/message_format/bedrock.rb +5 -2
  18. data/lib/clacky/providers.rb +46 -27
  19. data/lib/clacky/server/channel/adapters/qq/adapter.rb +662 -0
  20. data/lib/clacky/server/channel/adapters/qq/api_client.rb +245 -0
  21. data/lib/clacky/server/channel/adapters/qq/gateway_client.rb +340 -0
  22. data/lib/clacky/server/channel/adapters/qq/qr_binder.rb +140 -0
  23. data/lib/clacky/server/channel/channel_config.rb +9 -0
  24. data/lib/clacky/server/channel/channel_ui_controller.rb +6 -2
  25. data/lib/clacky/server/channel.rb +1 -0
  26. data/lib/clacky/server/http_server.rb +116 -11
  27. data/lib/clacky/server/model_prices.rb +11 -3
  28. data/lib/clacky/shell_hook_loader.rb +3 -1
  29. data/lib/clacky/utils/model_pricing.rb +160 -20
  30. data/lib/clacky/version.rb +1 -1
  31. data/lib/clacky/web/app.css +357 -180
  32. data/lib/clacky/web/components/chat-navigator.js +154 -171
  33. data/lib/clacky/web/components/composer.js +11 -2
  34. data/lib/clacky/web/components/model-picker.js +133 -23
  35. data/lib/clacky/web/core/aside.js +72 -0
  36. data/lib/clacky/web/features/billing/store.js +1 -1
  37. data/lib/clacky/web/features/billing/view.js +126 -34
  38. data/lib/clacky/web/features/channels/store.js +25 -0
  39. data/lib/clacky/web/features/channels/view.js +122 -5
  40. data/lib/clacky/web/features/skills/view.js +12 -0
  41. data/lib/clacky/web/features/workspace/view.js +88 -12
  42. data/lib/clacky/web/i18n.js +64 -22
  43. data/lib/clacky/web/index.html +15 -1
  44. data/lib/clacky/web/projects.js +131 -3
  45. data/lib/clacky/web/sessions.js +65 -9
  46. data/lib/clacky/web/settings.js +19 -0
  47. metadata +7 -1
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: media-gen
3
3
  name_zh: 媒体生成
4
- description_zh: 生成或编辑图片、视频,以及文字转语音,适用于封面、海报、插画、配音等。
5
- description: 'Generate or edit images, videos, or audio (text-to-speech) in the current task. Use whenever the user asks to create/generate/produce or edit/modify a picture / image / illustration / cover / poster / icon / artwork, a video / clip / animation, or speech / voiceover / narration / TTS — e.g. generate image, draw, design a cover, edit this image, change the background, text-to-video, generate speech; 画一张, 配图, 编辑图片, 改图, 换背景, 做个视频, 配音, 文字转语音. Also use when a document (slides, poster, README hero) needs an inline image.'
4
+ description_zh: 生成或编辑图片、视频和音频,适用于封面、海报、插画、配音、环境音等。
5
+ description: 'Generate or edit images, videos, or audio in the current task. Use whenever the user asks to create/generate/produce or edit/modify a picture / image / illustration / cover / poster / icon / artwork, a video / clip / animation, or speech / voiceover / narration / TTS / ambient audio / soundscapes — e.g. generate image, draw, design a cover, edit this image, change the background, text-to-video, generate speech; 画一张, 配图, 编辑图片, 改图, 换背景, 做个视频, 配音, 文字转语音, 环境音. Also use when a document (slides, poster, README hero) needs an inline image.'
6
6
  disable-model-invocation: false
7
7
  user-invocable: true
8
8
  always-show: true
@@ -71,6 +71,12 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/ima
71
71
  - If a call fails with `400 / INVALID_ARGUMENT`, drop the `aspect_ratio` field and retry once before reporting the error.
72
72
  - If a call fails with `unknown image model` (400), the configured model name isn't recognized by its backend — tell the user to fix the model name in the settings page; do NOT guess another name and retry.
73
73
 
74
+ ### Content-safety refusal (content_filter)
75
+
76
+ If a call fails with no image and mentions `content_filter` / `safety` / `blocked` (or HTTP `422`), the prompt was blocked. Don't resend as-is — **rephrase the prompt** (soften the sensitive part, keep the intent) and retry, up to 3 times, checking each returned image. Still blocked? Stop and ask the user to adjust.
77
+
78
+
79
+
74
80
  ### Request fields
75
81
 
76
82
  | Field | Required | Values | Notes |
@@ -82,6 +88,11 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/ima
82
88
  | `image` | no | file path / base64 / data URL | A single input image to **edit**. Triggers image-edit mode (see below). |
83
89
  | `images` | no | array of the above | Multiple input images for a multi-image edit. Takes precedence over `image`. |
84
90
 
91
+ ### Input / reference image format
92
+
93
+ Images passed as `image` / `images` must be **PNG, JPEG, or WebP**. Other formats (SVG, GIF, BMP, TIFF, …) are rejected upstream and fail the call — convert them to one of the supported formats first.
94
+
95
+
85
96
  ### Editing an existing image
86
97
 
87
98
  To edit instead of generate from scratch, pass the existing image as `image`
@@ -313,14 +324,16 @@ Rules & caveats:
313
324
  chaining is the practical option today; Veo's native server-side `extend`
314
325
  (148s) is not wired into this endpoint yet.
315
326
 
316
- ### Seedance (Volcengine Ark) — multimodal video
327
+ ### Seedance — multimodal video
317
328
 
318
329
  When the configured `type=video` model is a ByteDance **Doubao Seedance**
319
- model on Volcengine Ark (its Base URL is under `*.volces.com`, e.g.
320
- `https://ark.cn-beijing.volces.com/api/v3`), the **same**
321
- `POST /api/media/video` endpoint drives it. No separate endpoint — the server
322
- routes by Base URL automatically. Seedance adds richer inputs on top of the
323
- common fields; all are optional and only apply to Seedance:
330
+ model, the **same** `POST /api/media/video` endpoint drives it. No separate
331
+ endpoint — the server routes by Base URL automatically. Seedance adds richer
332
+ inputs on top of the common fields (all optional, Seedance-only), split below
333
+ into a **common** set that works on any Seedance gateway and an **Ark-only**
334
+ set that takes effect only on the native Volcengine Ark transport (Base URL
335
+ under `*.volces.com`). On any other OpenAI-compatible Seedance gateway, stick
336
+ to the common fields.
324
337
 
325
338
  > **Cost gate — ask before EVERY generation.** Resolution is the main driver
326
339
  > of Seedance's price (4k costs far more than 720p). So **once you've confirmed
@@ -337,22 +350,34 @@ common fields; all are optional and only apply to Seedance:
337
350
  > **When editing or continuing/extending an existing video, default to that
338
351
  > source video's resolution — never silently upgrade it (e.g. don't turn a
339
352
  > 720p source into a 4k render).** If the user gave no answer and you didn't
340
- > ask, the server pins `720p`. **These Seedance-only fields (`resolution`,
341
- > `generate_audio`, `watermark`, `seed`, `first_frame`, `last_frame`,
342
- > `reference_*`) have NO effect on Veo or Qwen/DashScope backends — never send
343
- > them unless the Base URL is `*.volces.com`.**
353
+ > ask, the server pins `720p`. **These Seedance-only fields have NO effect on
354
+ > Veo or Qwen/DashScope backends — never send them there.** (And within
355
+ > Seedance, the Ark-only table additionally requires a `*.volces.com` Base
356
+ > URL.)
357
+
358
+ **Common fields** — work on any Seedance gateway (native Ark or an
359
+ OpenAI-compatible host):
344
360
 
345
361
  | Field | Values | Notes |
346
362
  |--------------------|------------------------------------------|-------|
347
363
  | `aspect_ratio` | `landscape`/`portrait`/`square`, or a raw Ark ratio like `16:9`, `9:16`, `4:3`, `3:4`, `21:9`, `adaptive` | Raw ratios pass through unchanged. |
348
364
  | `duration_seconds` | integer, or `-1` | `-1` lets the model pick the length (Seedance 2.0 / 1.5 Pro). |
365
+ | `first_frame` | media ref (see below) | First frame → image-to-video. |
366
+ | `reference_images` | array of media refs (0–9) | Reference images. |
367
+
368
+ **Ark-only fields** — only take effect on the native Volcengine Ark transport
369
+ (`video.base_url` under `*.volces.com`). On any other OpenAI-compatible
370
+ Seedance gateway, `resolution` / `generate_audio` / `watermark` / `seed` are
371
+ silently ignored, and `last_frame` / `reference_videos` / `reference_audios`
372
+ are rejected outright — so don't send any of them there:
373
+
374
+ | Field | Values | Notes |
375
+ |--------------------|------------------------------------------|-------|
349
376
  | `resolution` | `480p` / `720p` / `1080p` / `4k` | **Defaults to `720p` when omitted** (cost control). Ask the user before every generation — never reuse a prior answer. See the cost gate above. Model-dependent; unsupported values are rejected upstream. |
350
377
  | `generate_audio` | `true` / `false` | Seedance 2.0 / 1.5 Pro can synthesize a synced audio track. |
351
378
  | `watermark` | `true` / `false` | |
352
379
  | `seed` | integer | Reproducibility. |
353
- | `first_frame` | media ref (see below) | First frame → image-to-video. |
354
380
  | `last_frame` | media ref | Together with `first_frame` → first+last-frame video. |
355
- | `reference_images` | array of media refs (0–9) | Reference images. |
356
381
  | `reference_videos` | array of media refs (0–3) | Reference videos. |
357
382
  | `reference_audios` | array of media refs (0–3) | Reference audio (background music / voice). |
358
383
 
@@ -482,6 +507,12 @@ done
482
507
  polling and tell the user the task is still rendering in the background,
483
508
  give them the `task_id`, and let them check again later — do NOT resubmit.
484
509
 
510
+ ## Generating ambient audio
511
+
512
+ Ambient audio includes rain, ocean waves, wind, and café background
513
+ sounds. Generate it programmatically, verify the audio, and provide
514
+ it for playback or download.
515
+
485
516
  ## Generating speech (Gemini TTS)
486
517
 
487
518
  The same `/api/media/` namespace serves text-to-speech. The user must
@@ -6,7 +6,7 @@ module Clacky
6
6
  "phase.skill_evolution" => "Reflecting on this task",
7
7
  "phase.memory_update" => "Updating long-term memory",
8
8
  "llm.error.insufficient_credit" => "Insufficient credit, please top up your account to continue",
9
- "llm.error.rate_limit_400" => "Rate limit or service issue, retrying...",
9
+ "llm.error.rate_limit_400" => "Too many requests or model temporarily unavailable. Please switch to another model and retry",
10
10
  "llm.error.invalid_api_key" => "API key is invalid or expired, please update it in Settings",
11
11
  "llm.error.403.model_not_allowed" => "Free trial only supports the or-gemini-3-8-flash model. Switch to that model, or top up to use other models",
12
12
  "llm.error.403.api_key_revoked" => "API key has been revoked, please generate a new one",
@@ -6,7 +6,7 @@ module Clacky
6
6
  "phase.skill_evolution" => "正在复盘本次任务",
7
7
  "phase.memory_update" => "正在更新长期记忆",
8
8
  "llm.error.insufficient_credit" => "API Key 账户余额不足,请前往控制台充值后继续使用",
9
- "llm.error.rate_limit_400" => "请求频率过高或服务暂时不可用,正在重试...",
9
+ "llm.error.rate_limit_400" => "请求频率过高或模型暂时不可用,请暂时切换其他模型重试",
10
10
  "llm.error.invalid_api_key" => "API Key 无效或已过期,请到设置中重新配置",
11
11
  "llm.error.403.model_not_allowed" => "免费试用仅支持 or-gemini-3-8-flash 模型,请切换到该模型,或充值后使用其他模型",
12
12
  "llm.error.403.api_key_revoked" => "API 密钥已被撤销,请前往控制台重新生成",
@@ -160,7 +160,10 @@ module Clacky
160
160
  )
161
161
  end
162
162
 
163
- def generate_video(prompt:, aspect_ratio: DEFAULT_ASPECT, duration_seconds: nil, output_dir: nil, image: nil, **_kwargs)
163
+ def generate_video(prompt:, aspect_ratio: DEFAULT_ASPECT, duration_seconds: nil, output_dir: nil,
164
+ image: nil, first_frame: nil, last_frame: nil,
165
+ reference_images: nil, reference_videos: nil, reference_audios: nil,
166
+ **_kwargs)
164
167
  provider_id = Clacky::Providers.find_by_base_url(@base_url) || "custom"
165
168
  aspect = VIDEO_ASPECTS.include?(aspect_ratio) ? aspect_ratio : DEFAULT_ASPECT
166
169
  duration = duration_seconds.to_i
@@ -179,8 +182,42 @@ module Clacky
179
182
  )
180
183
  end
181
184
 
185
+ # The OpenAI-compatible video endpoint has two independent image channels:
186
+ # • the first frame (exact anchor), carried as `image` — Veo-style
187
+ # image-to-video, and the only thing this facade honored before.
188
+ # • reference images (non-exact subject/identity/style guidance),
189
+ # carried as `input_references` — forwarded to Seedance 2.x's
190
+ # reference-to-video. `last_frame` and video/audio references are
191
+ # NOT representable on this facade (only the native Volcengine Ark
192
+ # path supports them); callers wanting those must use an Ark model.
193
+ # A bare `first_frame` (no explicit `image`) acts as the first frame.
194
+ first_frame_image = normalize_video_first_frame(image, first_frame)
195
+ begin
196
+ references = normalize_video_references(reference_images)
197
+ rescue ArgumentError => e
198
+ return video_error_response(
199
+ error: e.message,
200
+ error_type: "invalid_argument", provider: provider_id, prompt: prompt, aspect_ratio: aspect
201
+ )
202
+ end
203
+ unsupported = []
204
+ unsupported << "last_frame" unless to_list(last_frame).empty?
205
+ unsupported << "reference_videos" unless to_list(reference_videos).empty?
206
+ unsupported << "reference_audios" unless to_list(reference_audios).empty?
207
+ unless unsupported.empty?
208
+ return video_error_response(
209
+ error: "#{unsupported.join('/')} #{unsupported.length == 1 ? 'is' : 'are'} only " \
210
+ "supported by the native Volcengine (Seedance Ark) video backend, not by this " \
211
+ "OpenAI-compatible video model '#{@model}'. Use first_frame and/or " \
212
+ "reference_images here, or configure a *.volces.com video model for " \
213
+ "last_frame / reference_videos / reference_audios.",
214
+ error_type: "invalid_argument", provider: provider_id, prompt: prompt, aspect_ratio: aspect
215
+ )
216
+ end
217
+
182
218
  payload = { model: @model, prompt: prompt, aspect_ratio: aspect, duration_seconds: duration }
183
- payload[:image] = image if image.is_a?(Hash) && image["b64_json"]
219
+ payload[:image] = first_frame_image unless first_frame_image.nil?
220
+ payload[:input_references] = references unless references.empty?
184
221
 
185
222
  begin
186
223
  response = video_connection.post("videos/generations") do |req|
@@ -547,6 +584,56 @@ module Clacky
547
584
  end
548
585
  end
549
586
 
587
+ # Coerce one video media ref into the first-frame image hash the gateway's
588
+ # /videos endpoint expects: { "b64_json" => "<base64 or data URL>",
589
+ # "mime_type" => "image/..." }. An explicit `image` (already that hash)
590
+ # wins; otherwise a `first_frame` string (file path / data URL / bare
591
+ # base64) or hash is accepted. Returns nil when neither is given.
592
+ private def normalize_video_first_frame(image, first_frame)
593
+ if image.is_a?(Hash) && !image["b64_json"].to_s.strip.empty?
594
+ return image
595
+ end
596
+ source = first_frame
597
+ source = first_frame.first if first_frame.is_a?(Array) && !first_frame.empty?
598
+ return nil if source.nil?
599
+
600
+ if source.is_a?(Hash)
601
+ return source unless source["b64_json"].to_s.strip.empty?
602
+ return nil
603
+ end
604
+ s = source.to_s.strip
605
+ return nil if s.empty?
606
+ { "b64_json" => to_data_url(s) }
607
+ end
608
+
609
+ # Normalise reference images into the gateway `input_references` array:
610
+ # a list of data-URL / https strings. Accepts a single value or an array,
611
+ # where each entry is a file path, a data URL, a bare base64 string, an
612
+ # http(s) URL, or a { "b64_json", "mime_type" } hash. Raises ArgumentError
613
+ # on an undecodable local input (same contract as normalize_input_images).
614
+ private def normalize_video_references(reference_images)
615
+ to_list(reference_images).filter_map do |item|
616
+ if item.is_a?(Hash)
617
+ b64 = item["b64_json"].to_s.strip
618
+ next if b64.empty?
619
+ next b64 if b64.start_with?("data:")
620
+ mime = item["mime_type"].to_s.strip
621
+ mime = "image/png" if mime.empty?
622
+ next "data:#{mime};base64,#{b64}"
623
+ end
624
+ s = item.to_s.strip
625
+ next if s.empty?
626
+ next s if s.start_with?("http://", "https://")
627
+ to_data_url(s)
628
+ end
629
+ end
630
+
631
+ # Coerce a nil / scalar / array value into an array, dropping a nil.
632
+ private def to_list(value)
633
+ return [] if value.nil?
634
+ value.is_a?(Array) ? value : [value]
635
+ end
636
+
550
637
  private def to_data_url(input)
551
638
  return input if input.start_with?("data:")
552
639
 
@@ -88,15 +88,18 @@ module Clacky
88
88
  body[:toolConfig] = { tools: tools.map { |t| to_api_tool(t) } }
89
89
  end
90
90
 
91
- extra = additional_fields_for_effort(reasoning_effort)
91
+ extra = additional_fields_for_effort(reasoning_effort, model)
92
92
  body[:additionalModelRequestFields] = extra if extra
93
93
 
94
94
  body
95
95
  end
96
96
 
97
- private_class_method def self.additional_fields_for_effort(effort)
97
+ private_class_method def self.additional_fields_for_effort(effort, model = nil)
98
98
  return nil if effort.nil? || effort.to_s.empty?
99
99
  return nil unless %w[low medium high xhigh max].include?(effort.to_s)
100
+ # Converse accepts thinking/output_config only for the Anthropic family;
101
+ # other Converse models (Z.AI GLM, DeepSeek) reject unknown fields.
102
+ return nil unless model.to_s.downcase.include?("claude")
100
103
  {
101
104
  thinking: { type: "adaptive" },
102
105
  output_config: { effort: effort.to_s }
@@ -36,23 +36,28 @@ module Clacky
36
36
  "default_model" => "auto",
37
37
  "models" => [
38
38
  "auto",
39
- "abs-gpt-6-astra",
40
- "abs-gpt-6-sol",
41
- "abs-gpt-6-luna",
42
- "abs-gpt-5.6-sol",
43
- "abs-gpt-5.6-terra",
44
- "abs-gpt-5.6-luna",
39
+ "dsk-deepseek-flash",
40
+ "dsk-deepseek-v4-pro",
41
+ "oc-glm-5.3",
42
+ "oc-kimi-k3",
43
+ "oc-minimax-m2.7",
45
44
  "abs-claude-fable-5-1",
46
45
  "abs-claude-fable-5",
47
46
  "abs-claude-opus-5-5",
48
47
  "abs-claude-opus-5",
49
48
  "abs-claude-opus-4-8",
50
49
  "abs-claude-opus-4-7",
50
+ "abs-claude-sonnet-5-5",
51
51
  "abs-claude-sonnet-5",
52
52
  "abs-claude-sonnet-4-6",
53
53
  "abs-claude-haiku-4-5",
54
- "dsk-deepseek-flash",
55
- "dsk-deepseek-v4-pro",
54
+ "abs-gpt-6-astra",
55
+ "abs-gpt-6.1-sol",
56
+ "abs-gpt-6-sol",
57
+ "abs-gpt-6-luna",
58
+ "abs-gpt-5.6-sol",
59
+ "abs-gpt-5.6-terra",
60
+ "abs-gpt-5.6-luna",
56
61
  "or-gemini-3-1-pro",
57
62
  "or-gemini-3-8-flash",
58
63
  "or-gemini-3-7-flash"
@@ -141,9 +146,17 @@ module Clacky
141
146
  # images uploaded for text-only models must be downgraded to disk
142
147
  # refs. Gemini 3.1 Pro keeps the provider-default vision=true
143
148
  # (it accepts image/audio/video input natively via OpenRouter).
149
+ # oc-glm-5.3 is likewise text-only: its upstream rejects any content
150
+ # block other than `type: "text"`, so images must never be sent
151
+ # inline. oc-minimax-m2.7 is also text-only, but its upstream accepts
152
+ # the image block and silently ignores it — a worse failure mode,
153
+ # since the model then answers as if no image had been attached.
154
+ # oc-kimi-k3 is natively multimodal and keeps vision=true.
144
155
  "model_capabilities" => {
145
156
  "dsk-deepseek-flash" => { "vision" => true }.freeze,
146
- "dsk-deepseek-v4-pro" => { "vision" => false }.freeze
157
+ "dsk-deepseek-v4-pro" => { "vision" => false }.freeze,
158
+ "oc-glm-5.3" => { "vision" => false }.freeze,
159
+ "oc-minimax-m2.7" => { "vision" => false }.freeze
147
160
  }.freeze,
148
161
  # Bedrock GPT models (abs-gpt-*) are served through the OpenAI
149
162
  # Responses API — their Chat Completions endpoint rejects function
@@ -166,6 +179,7 @@ module Clacky
166
179
  "abs-claude-opus-5" => "abs-claude-haiku-4-5",
167
180
  "abs-claude-opus-4-8" => "abs-claude-haiku-4-5",
168
181
  "abs-claude-opus-4-7" => "abs-claude-haiku-4-5",
182
+ "abs-claude-sonnet-5-5" => "abs-claude-haiku-4-5",
169
183
  "abs-claude-sonnet-5" => "abs-claude-haiku-4-5",
170
184
  "abs-claude-sonnet-4-6" => "abs-claude-haiku-4-5",
171
185
  "abs-gpt-6-astra" => "abs-gpt-6-luna",
@@ -182,6 +196,7 @@ module Clacky
182
196
  "abs-claude-fable-5" => "abs-claude-opus-5",
183
197
  "abs-claude-opus-5-5" => "abs-claude-opus-5",
184
198
  "abs-claude-opus-5" => "abs-claude-opus-4-8",
199
+ "abs-claude-sonnet-5-5" => "abs-claude-sonnet-5",
185
200
  "abs-claude-sonnet-5" => "abs-claude-sonnet-4-6"
186
201
  },
187
202
  # Selectable endpoints exposed in the Base URL dropdown. The gateway
@@ -515,27 +530,29 @@ module Clacky
515
530
  "name" => "MiMo (Xiaomi)",
516
531
  "base_url" => "https://api.xiaomimimo.com/v1",
517
532
  "api" => "openai-completions",
518
- "default_model" => "mimo-v2.5-pro",
519
- # The MiMo-V2 family (mimo-v2-pro / mimo-v2-omni) was retired on
520
- # 2026-06-30 and the model ids are no longer accepted by the API. The
521
- # current lineup is the V2.5 series:
522
- # - mimo-v2.5-pro: text reasoning flagship, no vision
523
- # - mimo-v2.5: native omni-modal (image/video/audio/text), vision-capable
524
- # Source: https://platform.xiaomimimo.com/docs/zh-CN/model
525
- "models" => ["mimo-v2.5-pro", "mimo-v2.5"],
526
- "capabilities" => { "vision" => false }.freeze,
533
+ "default_model" => "mimo-v2.6-pro",
534
+ # The V2.6 chat models are both native omni-modal models. Keep the V2.5
535
+ # ids available during Xiaomi's migration window; V2.5-Pro remains the
536
+ # only text-only model in this preset.
537
+ # - mimo-v2.6-pro: flagship omni-modal reasoning model
538
+ # - mimo-v2.6-flash: efficient omni-modal reasoning model
539
+ # - mimo-v2.5-pro: legacy text reasoning flagship, no vision
540
+ # - mimo-v2.5: legacy native omni-modal model
541
+ # Source: https://mimo.mi.com/docs/zh-CN/updates/model
542
+ "models" => ["mimo-v2.6-pro", "mimo-v2.6-flash", "mimo-v2.5-pro", "mimo-v2.5"],
543
+ "capabilities" => { "vision" => true }.freeze,
527
544
  "model_capabilities" => {
528
- "mimo-v2.5" => { "vision" => true }.freeze
545
+ "mimo-v2.5-pro" => { "vision" => false }.freeze
529
546
  }.freeze,
530
- "default_ocr_model" => "mimo-v2.5",
531
- # Xiaomi serves the same V2.5 lineup from two billing endpoints: the
547
+ "default_ocr_model" => "mimo-v2.6-flash",
548
+ # Xiaomi serves the same chat lineup from two billing endpoints: the
532
549
  # pay-as-you-go API (api.xiaomimimo.com) and the Token Plan subscription
533
550
  # endpoint (token-plan-cn.xiaomimimo.com). Both accept identical model
534
551
  # ids and share one capability profile, so a single preset with
535
552
  # endpoint_variants recognises both. Without this, users on the Token
536
553
  # Plan endpoint fell through to "unknown provider" and the conservative
537
554
  # vision=true default applied to every model — sending images to the
538
- # text-only mimo-v2.5-pro, which rejects image input.
555
+ # text-only legacy mimo-v2.5-pro, which rejects image input.
539
556
  "endpoint_variants" => [
540
557
  { "label" => "Pay-as-you-go", "label_key" => "settings.models.baseurl.variant.mimo_payg", "base_url" => "https://api.xiaomimimo.com/v1", "region" => "cn" }.freeze,
541
558
  { "label" => "Token Plan", "label_key" => "settings.models.baseurl.variant.mimo_token_plan", "base_url" => "https://token-plan-cn.xiaomimimo.com/v1", "region" => "cn" }.freeze
@@ -791,17 +808,19 @@ module Clacky
791
808
  # Per-model maximum output token limits.
792
809
  #
793
810
  # The Agent global default (@max_tokens = 16_384) is tuned for the lowest
794
- # common denominator. Strong reasoning models (GLM-5.2, Kimi-K3, MiMo-V2.5)
795
- # support 64K–128K output but are artificially throttled to 16K, causing
796
- # truncation on long reasoning chains and code generation.
811
+ # common denominator. Strong reasoning models (GLM-5.2, Kimi-K3, MiniMax-M2.7,
812
+ # MiMo-V2.5/V2.6) support 64K–128K output but are artificially throttled to
813
+ # 16K, causing truncation on long reasoning chains and code generation.
797
814
  #
798
815
  # Entries are matched top-to-bottom; the first match wins. Models not
799
816
  # listed here fall back to the global default.
800
817
  MODEL_MAX_OUTPUT = [
801
818
  { pattern: /glm/i, limit: 65_536 }, # GLM-5.2: 128K output ceiling; 64K ample for reasoning+answer
802
819
  { pattern: /kimi-k3/i, limit: 65_536 }, # Kimi K3: max_completion_tokens=131072 (max 1M); 64K ample
803
- { pattern: /mimo-v2\.5-pro/i, limit: 65_536 }, # MiMo-V2.5-Pro: max_completion_tokens=131072; 64K ample
804
- { pattern: /mimo/i, limit: 32_768 }, # MiMo-V2.5: max_completion_tokens=32768; full default ceiling
820
+ { pattern: /minimax-m2\.7/i, limit: 65_536 }, # MiniMax M2.7: max_tokens ceiling 196608; 64K ample
821
+ { pattern: /\Amimo-v2\.6-(?:pro|flash)\z/i, limit: 65_536 }, # MiMo-V2.6: max_completion_tokens=131072; 64K ample
822
+ { pattern: /mimo-v2\.5-pro/i, limit: 65_536 }, # MiMo-V2.5-Pro: max_completion_tokens=131072; 64K ample
823
+ { pattern: /mimo/i, limit: 32_768 }, # MiMo-V2.5: max_completion_tokens=32768; full default ceiling
805
824
  { pattern: /gemini/i, limit: 65_536 } # Gemini 3.x: 64K max output ceiling
806
825
  ].freeze
807
826