openclacky 1.4.1 → 1.5.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/lib/clacky/agent/llm_caller.rb +3 -3
  4. data/lib/clacky/agent/session_serializer.rb +39 -5
  5. data/lib/clacky/brand_config.rb +49 -7
  6. data/lib/clacky/client.rb +22 -16
  7. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +135 -84
  8. data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -4
  9. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +522 -0
  10. data/lib/clacky/extension/api_extension.rb +5 -6
  11. data/lib/clacky/extension/loader.rb +16 -2
  12. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  13. data/lib/clacky/mcp/http_transport.rb +1 -1
  14. data/lib/clacky/providers.rb +109 -1
  15. data/lib/clacky/server/http_server.rb +115 -19
  16. data/lib/clacky/server/session_registry.rb +9 -0
  17. data/lib/clacky/utils/model_pricing.rb +36 -0
  18. data/lib/clacky/version.rb +1 -1
  19. data/lib/clacky/web/app.css +321 -32
  20. data/lib/clacky/web/app.js +29 -6
  21. data/lib/clacky/web/components/onboard.js +7 -3
  22. data/lib/clacky/web/features/extensions/store.js +54 -2
  23. data/lib/clacky/web/features/extensions/view.js +40 -5
  24. data/lib/clacky/web/features/new-session/store.js +8 -4
  25. data/lib/clacky/web/features/version/view.js +5 -1
  26. data/lib/clacky/web/i18n.js +52 -18
  27. data/lib/clacky/web/index.html +51 -5
  28. data/lib/clacky/web/sessions.js +40 -11
  29. data/lib/clacky/web/settings.js +22 -3
  30. data/lib/clacky/web/theme.js +27 -58
  31. data/scripts/build/src/uninstall.sh.cc +1 -1
  32. metadata +2 -5
  33. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +0 -71
  34. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +0 -73
  35. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +0 -65
  36. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +0 -106
@@ -376,6 +376,70 @@ module Clacky
376
376
  "website_url" => "https://open.bigmodel.cn/usercenter/apikeys"
377
377
  }.freeze,
378
378
 
379
+ # Volcengine Ark (Doubao) — ByteDance's model platform, OpenAI-compatible.
380
+ # Exposes three functionally-equivalent endpoints (Pay-as-you-go / Coding
381
+ # Plan / Agent Plan) that share the same model lineup and capability
382
+ # profile, so a single preset with endpoint_variants is the right shape.
383
+ # Without a preset match, base_urls like /api/coding/v3 fell through to the
384
+ # conservative "assume vision=true" default and inlined images into
385
+ # text-only models (e.g. glm-5.2), which Ark rejects. The vision matrix
386
+ # below is the source of truth so text-only models route through the OCR
387
+ # sidecar instead.
388
+ "volcengine-ark" => {
389
+ "name" => "Volcengine Ark (Doubao)",
390
+ "name_key" => "provider.name.volcengine_ark",
391
+ "base_url" => "https://ark.cn-beijing.volces.com/api/v3",
392
+ "api" => "openai-completions",
393
+ "default_model" => "doubao-seed-2.0-pro",
394
+ "models" => [
395
+ "doubao-seed-evolving",
396
+ "doubao-seed-2.1-pro",
397
+ "doubao-seed-2.1-turbo",
398
+ "doubao-seed-2.0-code",
399
+ "doubao-seed-2.0-pro",
400
+ "doubao-seed-2.0-lite",
401
+ "minimax-m3",
402
+ "minimax-m2.7",
403
+ "kimi-k2.7-code",
404
+ "kimi-k2.6",
405
+ "glm-5.2",
406
+ "deepseek-v4-pro",
407
+ "deepseek-v4-flash"
408
+ ],
409
+ "endpoint_variants" => [
410
+ { "label" => "Pay-as-you-go", "label_key" => "settings.models.baseurl.variant.ark_payg", "base_url" => "https://ark.cn-beijing.volces.com/api/v3", "alias_group" => "payg" }.freeze,
411
+ { "label" => "Coding Plan", "label_key" => "settings.models.baseurl.variant.ark_coding", "base_url" => "https://ark.cn-beijing.volces.com/api/coding/v3" }.freeze,
412
+ { "label" => "Agent Plan", "label_key" => "settings.models.baseurl.variant.ark_agent", "base_url" => "https://ark.cn-beijing.volces.com/api/plan/v3" }.freeze
413
+ ].freeze,
414
+ # Pay-as-you-go billing endpoint requires versioned model ids, while the
415
+ # Coding/Agent Plan endpoints accept the short display names. Users pick
416
+ # the intuitive short name; on the payg endpoint we transparently swap
417
+ # it for the versioned id the billing API expects. Only models whose ids
418
+ # actually differ are listed here.
419
+ "api_model_aliases" => {
420
+ "payg" => {
421
+ "glm-5.2" => "glm-5-2-260617",
422
+ "deepseek-v4-pro" => "deepseek-v4-pro-260425",
423
+ "deepseek-v4-flash" => "deepseek-v4-flash-260425",
424
+ "doubao-seed-2.1-pro" => "doubao-seed-2-1-pro-260628",
425
+ "doubao-seed-2.1-turbo" => "doubao-seed-2-1-turbo-260628",
426
+ "doubao-seed-2.0-pro" => "doubao-seed-2-0-pro-260215",
427
+ "doubao-seed-2.0-lite" => "doubao-seed-2-0-lite-260428",
428
+ "doubao-seed-2.0-code" => "doubao-seed-2-0-code-preview-260215"
429
+ }.freeze
430
+ }.freeze,
431
+ # Most Doubao/multimodal models accept image input; GLM-5.2 and
432
+ # DeepSeek-V4 on Ark are text-only.
433
+ "capabilities" => { "vision" => true }.freeze,
434
+ "model_capabilities" => {
435
+ "glm-5.2" => { "vision" => false }.freeze,
436
+ "deepseek-v4-pro" => { "vision" => false }.freeze,
437
+ "deepseek-v4-flash" => { "vision" => false }.freeze
438
+ }.freeze,
439
+ "default_ocr_model" => "doubao-seed-2.0-lite",
440
+ "website_url" => "https://console.volcengine.com/ark/region:cn-beijing/overview"
441
+ }.freeze,
442
+
379
443
  "openai" => {
380
444
  "name" => "OpenAI (GPT)",
381
445
  "base_url" => "https://api.openai.com/v1",
@@ -788,7 +852,51 @@ module Clacky
788
852
  nil
789
853
  end
790
854
 
791
- # Resolve the capabilities hash for a given provider+model.
855
+ # Translate a display model name into the real model id the target
856
+ # endpoint expects. Some providers expose the same model under different
857
+ # ids per billing endpoint (e.g. Volcengine Ark's pay-as-you-go API needs
858
+ # versioned ids like "doubao-seed-2-0-pro-260215" while the Coding/Agent
859
+ # Plan endpoints accept the short "doubao-seed-2.0-pro"). Users always see
860
+ # and pick the short name; this swaps it just before the request goes out.
861
+ #
862
+ # Returns the original model unchanged when no alias applies (unknown
863
+ # provider, endpoint without an alias group, or model not in the map).
864
+ #
865
+ # @param base_url [String, nil] the configured base_url (identifies endpoint)
866
+ # @param api_key [String, nil] the configured api_key (provider fallback)
867
+ # @param model [String, nil] the display model name
868
+ # @return [String, nil] the real model id to send, or the input unchanged
869
+ def resolve_api_model(base_url:, api_key: nil, model:)
870
+ return model if model.nil?
871
+
872
+ provider_id = resolve_provider(base_url: base_url, api_key: api_key)
873
+ return model unless provider_id
874
+
875
+ preset = PRESETS[provider_id]
876
+ aliases = preset && preset["api_model_aliases"]
877
+ return model unless aliases.is_a?(Hash)
878
+
879
+ group = alias_group_for_base_url(preset, base_url)
880
+ return model unless group
881
+
882
+ aliases.dig(group, model.to_s) || model
883
+ end
884
+
885
+ # Find which alias group the given base_url belongs to by matching it
886
+ # against the preset's endpoint_variants. Uses exact base_url equality
887
+ # (after normalising trailing slash) so "/api/v3" never leaks into the
888
+ # "/api/coding/v3" match.
889
+ def alias_group_for_base_url(preset, base_url)
890
+ return nil if base_url.nil?
891
+ variants = preset["endpoint_variants"]
892
+ return nil unless variants.is_a?(Array)
893
+
894
+ normalized = base_url.to_s.chomp("/")
895
+ variant = variants.find do |v|
896
+ v.is_a?(Hash) && v["base_url"].to_s.chomp("/") == normalized
897
+ end
898
+ variant && variant["alias_group"]
899
+ end
792
900
  #
793
901
  # Resolution order (most specific wins):
794
902
  # 1. PRESETS[provider_id]["model_capabilities"][model_name] — per-model
@@ -546,6 +546,7 @@ module Clacky
546
546
  when ["POST", "/api/onboard/skip-soul"] then api_onboard_skip_soul(req, res)
547
547
  when ["GET", "/api/store/skills"] then api_store_skills(res)
548
548
  when ["GET", "/api/store/extensions"] then api_store_extensions(req, res)
549
+ when ["GET", "/api/store/extensions/brand"] then api_store_extensions_brand(res)
549
550
  when ["GET", "/api/store/extensions/installed"] then api_store_extensions_installed(res)
550
551
  when ["GET", "/api/store/extension"] then api_store_extension_detail(req, res)
551
552
  when ["POST", "/api/store/extension/install"] then api_store_extension_install(req, res)
@@ -2312,6 +2313,9 @@ module Clacky
2312
2313
  # Refresh skill_loader with the now-activated brand config so brand
2313
2314
  # skills are loadable from this point forward (e.g. after sync).
2314
2315
  @skill_loader = Clacky::SkillLoader.new(working_dir: nil, brand_config: brand)
2316
+ # Install all brand skills in the background on first activation so
2317
+ # they are available immediately without manual user action.
2318
+ brand.sync_brand_skills_async!(install_new: true)
2315
2319
  json_response(res, 200, {
2316
2320
  ok: true,
2317
2321
  product_name: result[:product_name] || brand.product_name,
@@ -2362,10 +2366,9 @@ module Clacky
2362
2366
 
2363
2367
  # GET /api/store/extensions?q=&sort=
2364
2368
  #
2365
- # Public extension marketplace catalog. Anonymous (no license needed).
2366
- # Proxies BrandConfig#search_extensions! which hits the platform's public
2367
- # /api/v1/extensions endpoint. Extension archives are NOT downloadable here
2368
- # — they ship inside license-gated brand packages (path B distribution).
2369
+ # Public extension marketplace catalog always returns the full public
2370
+ # catalog regardless of brand status. Both brand and non-brand users see
2371
+ # the same public marketplace here.
2369
2372
  def api_store_extensions(req, res)
2370
2373
  brand = Clacky::BrandConfig.load
2371
2374
  result = brand.search_extensions!(query: req.query["q"], sort: req.query["sort"])
@@ -2373,7 +2376,7 @@ module Clacky
2373
2376
  if result[:success]
2374
2377
  installed = installed_extension_containers
2375
2378
  extensions = Array(result[:extensions]).map do |ext|
2376
- slug = ext["name"] || ext[:name] || ext["slug"] || ext[:slug]
2379
+ slug = ext["name"] || ext[:name] || ext["slug"] || ext[:slug]
2377
2380
  container = installed[slug]
2378
2381
  ext.merge(
2379
2382
  "installed" => !container.nil?,
@@ -2390,6 +2393,49 @@ module Clacky
2390
2393
  end
2391
2394
  end
2392
2395
 
2396
+ # GET /api/store/extensions/brand
2397
+ #
2398
+ # Brand-private extension catalog — only available to users with an
2399
+ # activated brand license. Returns extensions belonging to this brand via
2400
+ # BrandConfig#fetch_brand_extensions!.
2401
+ # Returns 403 when the license is not activated.
2402
+ def api_store_extensions_brand(res)
2403
+ brand = Clacky::BrandConfig.load
2404
+
2405
+ unless brand.activated?
2406
+ json_response(res, 403, { ok: false, error: "Brand license not activated." })
2407
+ return
2408
+ end
2409
+
2410
+ result = brand.fetch_brand_extensions!
2411
+
2412
+ if result[:success]
2413
+ installed = installed_extension_containers
2414
+ extensions = Array(result[:extensions]).map do |ext|
2415
+ slug = ext["name"] || ext[:name] || ext["slug"] || ext[:slug]
2416
+ container = installed[slug]
2417
+ # Merge local container data (e.g. emoji) for self-authored extensions.
2418
+ local_overrides = {}
2419
+ if container && ext["emoji"].to_s.empty?
2420
+ local_emoji = container.dig(:raw, "emoji") || container[:emoji]
2421
+ local_overrides["emoji"] = local_emoji if local_emoji.to_s.strip != ""
2422
+ end
2423
+ ext.merge(
2424
+ "installed" => !ext["installed_version"].nil?,
2425
+ "installed_version" => ext["installed_version"],
2426
+ **local_overrides
2427
+ )
2428
+ end
2429
+ json_response(res, 200, { ok: true, extensions: extensions })
2430
+ else
2431
+ json_response(res, 200, {
2432
+ ok: true,
2433
+ extensions: [],
2434
+ warning: result[:error] || "Could not reach the extension store."
2435
+ })
2436
+ end
2437
+ end
2438
+
2393
2439
  # GET /api/store/extensions/installed
2394
2440
  #
2395
2441
  # Returns all locally installed extensions (all layers: builtin, installed,
@@ -2408,9 +2454,15 @@ module Clacky
2408
2454
 
2409
2455
  extensions = local_entries.map do |ext_id, container|
2410
2456
  market = market_by_slug[ext_id]
2457
+ # For self-authored (origin: self) extensions market is nil.
2458
+ # Fall back to local ext.yml data so name/description/author are populated.
2459
+ local_name = container[:name].to_s.then { |n| n.empty? ? ext_id : n }
2460
+ local_desc = container.dig(:raw, "description").to_s
2461
+ local_author = container[:author].to_s
2462
+ local_units = units_from_container(container)
2411
2463
  {
2412
2464
  "id" => ext_id,
2413
- "name" => market ? (market["name"] || ext_id) : ext_id,
2465
+ "name" => market ? (market["name"] || ext_id) : local_name,
2414
2466
  "display_name" => market&.dig("display_name"),
2415
2467
  "display_name_zh" => market&.dig("display_name_zh"),
2416
2468
  "name_zh" => market&.dig("name_zh"),
@@ -2418,15 +2470,19 @@ module Clacky
2418
2470
  "slug" => ext_id,
2419
2471
  "version" => market ? (market["version"] || container[:version]) : container[:version],
2420
2472
  "installed_version" => container[:version],
2421
- "description" => market&.dig("description"),
2422
- "author" => market&.dig("author"),
2473
+ "description" => market ? market["description"] : local_desc,
2474
+ "author" => market ? market["author"] : local_author,
2423
2475
  "icon_url" => market&.dig("icon_url"),
2424
- "units" => market&.dig("units"),
2425
- "homepage" => market ? (market["homepage"] || "") : "",
2476
+ "units" => market ? market["units"] : local_units,
2477
+ "homepage" => market ? (market["homepage"] || "") : container[:homepage].to_s,
2426
2478
  "origin" => market ? (market["origin"] || container[:origin]) : container[:origin],
2427
2479
  "hub_active" => market&.dig("hub_active"),
2428
2480
  "download_count" => market&.dig("download_count").to_i,
2429
- "unlisted" => market.nil?,
2481
+ # Only mark as unlisted when the extension was published to the
2482
+ # marketplace (origin != "self") but is no longer listed there.
2483
+ # Self-authored extensions (origin: self) have never been published
2484
+ # and should never show the "unlisted" badge.
2485
+ "unlisted" => market.nil? && container[:origin].to_s != "self",
2430
2486
  "layer" => container[:layer].to_s,
2431
2487
  "installed" => true,
2432
2488
  "removable" => true,
@@ -2470,6 +2526,21 @@ module Clacky
2470
2526
  {}
2471
2527
  end
2472
2528
 
2529
+ # Build a units summary hash from local ext.yml contributes data,
2530
+ # mirroring the format platform's unit_summary returns: { "api" => n, ... }
2531
+ private def units_from_container(container)
2532
+ contributes = container[:contributes] || {}
2533
+ units = {}
2534
+ units["api"] = 1 unless (contributes["api"] || contributes[:api]).to_s.empty?
2535
+ panels = Array(contributes["panels"] || contributes[:panels])
2536
+ units["panel"] = panels.size if panels.any?
2537
+ agents = Array(contributes["agents"] || contributes[:agents])
2538
+ units["agent"] = agents.size if agents.any?
2539
+ skills = Array(contributes["skills"] || contributes[:skills])
2540
+ units["skill"] = skills.size if skills.any?
2541
+ units
2542
+ end
2543
+
2473
2544
  # GET /api/store/extension?id=<slug-or-id>
2474
2545
  #
2475
2546
  # Public detail for a single marketplace extension (contributes + version
@@ -2482,7 +2553,14 @@ module Clacky
2482
2553
  end
2483
2554
 
2484
2555
  brand = Clacky::BrandConfig.load
2556
+ # Always try the public endpoint first — it covers all marketplace
2557
+ # extensions regardless of brand status. Only fall back to the
2558
+ # license-gated brand endpoint when the public lookup fails (e.g. for
2559
+ # origin=self brand-private extensions not exposed on the public API).
2485
2560
  result = brand.extension_detail!(id)
2561
+ if !result[:success] && brand.activated?
2562
+ result = brand.brand_extension_detail!(id)
2563
+ end
2486
2564
 
2487
2565
  if result[:success] && result[:extension]
2488
2566
  ext = result[:extension]
@@ -2499,22 +2577,29 @@ module Clacky
2499
2577
  container = extension_container(id)
2500
2578
  if container && container[:layer] == :installed
2501
2579
  market = fetch_batch_market_data([id])[id]
2580
+ # Fall back to local ext.yml for self-authored extensions.
2581
+ local_name = container[:name].to_s.then { |n| n.empty? ? id : n }
2582
+ local_desc = container.dig(:raw, "description").to_s
2583
+ local_author = container[:author].to_s
2584
+ local_units = units_from_container(container)
2502
2585
  ext = {
2503
2586
  "id" => id,
2504
- "name" => market ? (market["name"] || id) : id,
2587
+ "name" => market ? (market["name"] || id) : local_name,
2505
2588
  "name_zh" => market&.dig("name_zh"),
2506
2589
  "name_en" => market&.dig("name_en"),
2507
2590
  "slug" => id,
2508
2591
  "version" => market ? (market["version"] || container[:version]) : container[:version],
2509
2592
  "installed_version" => container[:version],
2510
- "description" => market&.dig("description"),
2511
- "author" => market&.dig("author"),
2593
+ "description" => market ? market["description"] : local_desc,
2594
+ "author" => market ? market["author"] : local_author,
2512
2595
  "icon_url" => market&.dig("icon_url"),
2513
- "units" => market&.dig("units"),
2514
- "homepage" => market ? (market["homepage"] || "") : "",
2596
+ "units" => market ? market["units"] : local_units,
2597
+ "homepage" => market ? (market["homepage"] || "") : container[:homepage].to_s,
2515
2598
  "origin" => market ? (market["origin"] || container[:origin]) : container[:origin],
2516
2599
  "hub_active" => market&.dig("hub_active"),
2517
- "unlisted" => market.nil?,
2600
+ # Only mark as unlisted when the extension was published to the
2601
+ # marketplace (origin != "self") but is no longer listed there.
2602
+ "unlisted" => market.nil? && container[:origin].to_s != "self",
2518
2603
  "installed" => true,
2519
2604
  "removable" => true,
2520
2605
  "disabled" => container[:disabled] == true,
@@ -2582,7 +2667,9 @@ module Clacky
2582
2667
  end
2583
2668
 
2584
2669
  def api_store_extension_uninstall(req, res)
2585
- id = parse_json_body(req)["id"].to_s
2670
+ body = parse_json_body(req)
2671
+ id = body["id"].to_s
2672
+ purge_data = body["purge_data"] == true
2586
2673
  container = extension_container(id)
2587
2674
  if container.nil?
2588
2675
  json_response(res, 404, { ok: false, error: "Not installed." })
@@ -2593,7 +2680,7 @@ module Clacky
2593
2680
  return
2594
2681
  end
2595
2682
 
2596
- if Clacky::ExtensionLoader.uninstall!(id)
2683
+ if Clacky::ExtensionLoader.uninstall!(id, purge_data: purge_data)
2597
2684
  json_response(res, 200, { ok: true, id: id })
2598
2685
  else
2599
2686
  json_response(res, 404, { ok: false, error: "Not installed." })
@@ -5725,6 +5812,9 @@ module Clacky
5725
5812
  {
5726
5813
  id: id,
5727
5814
  name: preset["name"],
5815
+ # Optional i18n key for the display name (localised per UI language);
5816
+ # frontend falls back to `name` when absent or untranslated.
5817
+ name_key: preset["name_key"],
5728
5818
  base_url: preset["base_url"],
5729
5819
  default_model: preset["default_model"],
5730
5820
  models: preset["models"] || [],
@@ -6424,6 +6514,12 @@ module Clacky
6424
6514
  private def interrupt_all_agents
6425
6515
  return unless @registry && @session_manager
6426
6516
 
6517
+ # Stop idle compression first: an in-flight compression must fully roll
6518
+ # back (or complete) before the worker is killed, otherwise a hot
6519
+ # restart's SIGKILL can leave a chunk file on disk whose chunk_path
6520
+ # never made it into session.json (history vanishes on replay).
6521
+ @registry.shutdown_all_idle_timers
6522
+
6427
6523
  @registry.each_live_agent do |id, agent, thread|
6428
6524
  next unless thread&.alive?
6429
6525
  begin
@@ -496,6 +496,15 @@ module Clacky
496
496
  snapshot.each { |id, agent, thread| yield id, agent, thread }
497
497
  end
498
498
 
499
+ # Shut down every session's idle-compression timer, waiting for any
500
+ # in-flight compression to roll back cleanly. Called on worker shutdown
501
+ # so a hot restart's SIGKILL cannot tear a compression apart between
502
+ # writing the chunk file and persisting its chunk_path into session.json.
503
+ def shutdown_all_idle_timers
504
+ timers = @mutex.synchronize { @sessions.values.filter_map { |s| s[:idle_timer] } }
505
+ timers.each { |t| t.shutdown rescue nil }
506
+ end
507
+
499
508
  private def persist_and_release(id, session)
500
509
  agent = session[:agent]
501
510
  @session_manager&.save(agent.to_session_data(status: :success, preserve_updated_at: true)) if agent
@@ -286,6 +286,42 @@ module Clacky
286
286
  }
287
287
  },
288
288
 
289
+ # Volcengine Ark (Doubao) — priced in CNY, converted at 1 USD = 6.7730 CNY
290
+ # (2026-07-23) and rounded up to the cent. Ark tiers by input length in K
291
+ # tokens; we map (32,128] → default and (128,256] → over_200k, matching
292
+ # the coarse two-tier structure used elsewhere. Single-tier [0,256] models
293
+ # use the same value for both. cache.write bills at the miss (input) rate.
294
+ "doubao-seed-evolving" => {
295
+ input: { default: 0.89, over_200k: 0.89 },
296
+ output: { default: 4.43, over_200k: 4.43 },
297
+ cache: { write: 0.89, read: 0.18 }
298
+ },
299
+ "doubao-seed-2.1-pro" => {
300
+ input: { default: 0.89, over_200k: 0.89 },
301
+ output: { default: 4.43, over_200k: 4.43 },
302
+ cache: { write: 0.89, read: 0.18 }
303
+ },
304
+ "doubao-seed-2.1-turbo" => {
305
+ input: { default: 0.45, over_200k: 0.45 },
306
+ output: { default: 2.22, over_200k: 2.22 },
307
+ cache: { write: 0.45, read: 0.09 }
308
+ },
309
+ "doubao-seed-2.0-pro" => {
310
+ input: { default: 0.71, over_200k: 1.42 },
311
+ output: { default: 3.55, over_200k: 7.09 },
312
+ cache: { write: 0.71, read: 0.15 }
313
+ },
314
+ "doubao-seed-2.0-code" => {
315
+ input: { default: 0.71, over_200k: 1.42 },
316
+ output: { default: 3.55, over_200k: 7.09 },
317
+ cache: { write: 0.71, read: 0.15 }
318
+ },
319
+ "doubao-seed-2.0-lite" => {
320
+ input: { default: 0.14, over_200k: 0.27 },
321
+ output: { default: 0.80, over_200k: 1.60 },
322
+ cache: { write: 0.14, read: 0.03 }
323
+ },
324
+
289
325
  # Google Gemini 3 series (via Vertex AI). Tiered at 200K input tokens
290
326
  # for Pro; Flash has flat pricing.
291
327
  "gemini-3.1-pro" => {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.4.1"
4
+ VERSION = "1.5.1"
5
5
  end