samagotchi 0.2.0

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 (243) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +43 -0
  3. data/LICENSE +21 -0
  4. data/README.md +126 -0
  5. data/bin/chi +1140 -0
  6. data/docs/architecture.md +299 -0
  7. data/docs/cli.md +490 -0
  8. data/docs/configuration.md +494 -0
  9. data/docs/desktop.md +97 -0
  10. data/docs/guardrails.md +218 -0
  11. data/docs/hooks.md +309 -0
  12. data/docs/internals/background-tasks.md +26 -0
  13. data/docs/internals/context-telemetry.md +36 -0
  14. data/docs/internals/gemma4-contract.md +23 -0
  15. data/docs/internals/tool-guardrails.md +45 -0
  16. data/docs/memory.md +85 -0
  17. data/docs/plugins.md +819 -0
  18. data/docs/releasing.md +135 -0
  19. data/docs/sessions.md +155 -0
  20. data/lib/samagotchi/bridge/bounded_queue.rb +70 -0
  21. data/lib/samagotchi/bridge/card_store.rb +126 -0
  22. data/lib/samagotchi/bridge/event_id.rb +25 -0
  23. data/lib/samagotchi/bridge/ring_buffer.rb +63 -0
  24. data/lib/samagotchi/bridge/sse_writer.rb +248 -0
  25. data/lib/samagotchi/bridge/turn_accumulator.rb +189 -0
  26. data/lib/samagotchi/bridge.rb +993 -0
  27. data/lib/samagotchi/bridge_client/event_stream.rb +158 -0
  28. data/lib/samagotchi/bridge_client/sse_parser.rb +51 -0
  29. data/lib/samagotchi/bridge_client.rb +330 -0
  30. data/lib/samagotchi/bundle_needs.rb +97 -0
  31. data/lib/samagotchi/bundles/btw/manifest.yml +10 -0
  32. data/lib/samagotchi/bundles/btw/plugin.rb +100 -0
  33. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +82 -0
  34. data/lib/samagotchi/bundles/guardrails/guardrails.md +14 -0
  35. data/lib/samagotchi/bundles/guardrails/manifest.yml +8 -0
  36. data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +210 -0
  37. data/lib/samagotchi/bundles/known-names/known_names.md +3 -0
  38. data/lib/samagotchi/bundles/known-names/manifest.yml +14 -0
  39. data/lib/samagotchi/bundles/loop-guard/manifest.yml +10 -0
  40. data/lib/samagotchi/bundles/loop-guard/plugin.rb +158 -0
  41. data/lib/samagotchi/bundles/mcp/manifest.yml +11 -0
  42. data/lib/samagotchi/bundles/mcp/plugin.rb +631 -0
  43. data/lib/samagotchi/bundles/system/config_modification_protocol.md +149 -0
  44. data/lib/samagotchi/bundles/system/delegated.md +10 -0
  45. data/lib/samagotchi/bundles/system/identity.md +7 -0
  46. data/lib/samagotchi/bundles/system/manifest.yml +11 -0
  47. data/lib/samagotchi/bundles/system/memory_guide.md +107 -0
  48. data/lib/samagotchi/bundles/system/self_map.md +55 -0
  49. data/lib/samagotchi/cancellation_controller.rb +78 -0
  50. data/lib/samagotchi/client.rb +429 -0
  51. data/lib/samagotchi/commands/registry.rb +112 -0
  52. data/lib/samagotchi/config.rb +910 -0
  53. data/lib/samagotchi/context_note.rb +77 -0
  54. data/lib/samagotchi/context_quote.rb +21 -0
  55. data/lib/samagotchi/context_usage.rb +66 -0
  56. data/lib/samagotchi/context_window.rb +76 -0
  57. data/lib/samagotchi/debug_log.rb +110 -0
  58. data/lib/samagotchi/desktop/macos/App.swift +102 -0
  59. data/lib/samagotchi/desktop/macos/ChiRunner.swift +201 -0
  60. data/lib/samagotchi/desktop/macos/Hotkey.swift +42 -0
  61. data/lib/samagotchi/desktop/macos/Info.plist.erb +42 -0
  62. data/lib/samagotchi/desktop/macos/Panel.swift +383 -0
  63. data/lib/samagotchi/desktop/macos.rb +255 -0
  64. data/lib/samagotchi/desktop.rb +21 -0
  65. data/lib/samagotchi/desktop_command.rb +143 -0
  66. data/lib/samagotchi/engine.rb +2807 -0
  67. data/lib/samagotchi/guardrails/approval.rb +125 -0
  68. data/lib/samagotchi/guardrails/approvals.rb +177 -0
  69. data/lib/samagotchi/guardrails/context.rb +71 -0
  70. data/lib/samagotchi/guardrails/gate.rb +125 -0
  71. data/lib/samagotchi/guardrails/load_failures.rb +46 -0
  72. data/lib/samagotchi/guardrails/protected_paths.rb +77 -0
  73. data/lib/samagotchi/guardrails/rules.rb +199 -0
  74. data/lib/samagotchi/guardrails/targets.rb +119 -0
  75. data/lib/samagotchi/guardrails/verdict.rb +134 -0
  76. data/lib/samagotchi/guardrails.rb +18 -0
  77. data/lib/samagotchi/hooks/bundle_loader.rb +158 -0
  78. data/lib/samagotchi/hooks/loader.rb +162 -0
  79. data/lib/samagotchi/hooks/registry.rb +261 -0
  80. data/lib/samagotchi/hooks.rb +30 -0
  81. data/lib/samagotchi/host_registry.rb +315 -0
  82. data/lib/samagotchi/idle_client.rb +147 -0
  83. data/lib/samagotchi/idle_recap.rb +549 -0
  84. data/lib/samagotchi/idle_reminders.rb +101 -0
  85. data/lib/samagotchi/idle_scheduler.rb +76 -0
  86. data/lib/samagotchi/image_store.rb +393 -0
  87. data/lib/samagotchi/installed_gem.rb +38 -0
  88. data/lib/samagotchi/kernel_loop.rb +1017 -0
  89. data/lib/samagotchi/launch_mode.rb +34 -0
  90. data/lib/samagotchi/llm/backend.rb +28 -0
  91. data/lib/samagotchi/llm/chat_loop.rb +450 -0
  92. data/lib/samagotchi/llm/errors.rb +329 -0
  93. data/lib/samagotchi/llm/http.rb +412 -0
  94. data/lib/samagotchi/llm/model_result.rb +72 -0
  95. data/lib/samagotchi/llm/native_backend.rb +50 -0
  96. data/lib/samagotchi/llm/native_tool_normalizer.rb +277 -0
  97. data/lib/samagotchi/llm/openai_chat.rb +403 -0
  98. data/lib/samagotchi/llm/usage.rb +79 -0
  99. data/lib/samagotchi/log.rb +200 -0
  100. data/lib/samagotchi/log_line.rb +127 -0
  101. data/lib/samagotchi/log_path.rb +31 -0
  102. data/lib/samagotchi/log_subscriber.rb +163 -0
  103. data/lib/samagotchi/memory_bundle/builder.rb +364 -0
  104. data/lib/samagotchi/memory_bundle/index_updater.rb +123 -0
  105. data/lib/samagotchi/memory_bundle/installer.rb +528 -0
  106. data/lib/samagotchi/memory_bundle/listing.rb +72 -0
  107. data/lib/samagotchi/memory_bundle/manifest.rb +225 -0
  108. data/lib/samagotchi/memory_bundle/merger.rb +52 -0
  109. data/lib/samagotchi/memory_bundle/placeholder.rb +37 -0
  110. data/lib/samagotchi/memory_bundle/provenance.rb +257 -0
  111. data/lib/samagotchi/memory_bundle/source.rb +153 -0
  112. data/lib/samagotchi/memory_bundle/status.rb +107 -0
  113. data/lib/samagotchi/memory_bundle/system_bundle.rb +161 -0
  114. data/lib/samagotchi/memory_bundle/uninstaller.rb +128 -0
  115. data/lib/samagotchi/memory_bundle.rb +17 -0
  116. data/lib/samagotchi/memory_paths.rb +101 -0
  117. data/lib/samagotchi/model_overlay.rb +53 -0
  118. data/lib/samagotchi/model_profile.rb +309 -0
  119. data/lib/samagotchi/muted_memories.rb +66 -0
  120. data/lib/samagotchi/note_command.rb +163 -0
  121. data/lib/samagotchi/output_formatter.rb +100 -0
  122. data/lib/samagotchi/owner_lock.rb +110 -0
  123. data/lib/samagotchi/pending_input_queue.rb +48 -0
  124. data/lib/samagotchi/plugin/api.rb +362 -0
  125. data/lib/samagotchi/plugin/context.rb +193 -0
  126. data/lib/samagotchi/plugin/loader.rb +126 -0
  127. data/lib/samagotchi/plugin/service.rb +117 -0
  128. data/lib/samagotchi/plugin/sessions.rb +150 -0
  129. data/lib/samagotchi/plugin/side_question.rb +60 -0
  130. data/lib/samagotchi/plugin/tool_result.rb +24 -0
  131. data/lib/samagotchi/project_scope.rb +25 -0
  132. data/lib/samagotchi/prompt.rb +119 -0
  133. data/lib/samagotchi/prompt_literal_guard.rb +70 -0
  134. data/lib/samagotchi/recap_store.rb +92 -0
  135. data/lib/samagotchi/reminder_store.rb +165 -0
  136. data/lib/samagotchi/self_report.rb +195 -0
  137. data/lib/samagotchi/send_command.rb +170 -0
  138. data/lib/samagotchi/served_model.rb +32 -0
  139. data/lib/samagotchi/session.rb +508 -0
  140. data/lib/samagotchi/session_commands.rb +527 -0
  141. data/lib/samagotchi/session_delete_command.rb +105 -0
  142. data/lib/samagotchi/session_manager.rb +1049 -0
  143. data/lib/samagotchi/session_metrics.rb +466 -0
  144. data/lib/samagotchi/session_observer.rb +117 -0
  145. data/lib/samagotchi/terminal_ui/attach_launcher.rb +118 -0
  146. data/lib/samagotchi/terminal_ui/attached_loop.rb +1037 -0
  147. data/lib/samagotchi/terminal_ui/attached_view.rb +264 -0
  148. data/lib/samagotchi/terminal_ui/event_renderer.rb +192 -0
  149. data/lib/samagotchi/terminal_ui/formatting.rb +291 -0
  150. data/lib/samagotchi/terminal_ui/image_input.rb +36 -0
  151. data/lib/samagotchi/terminal_ui/input_support.rb +324 -0
  152. data/lib/samagotchi/terminal_ui/legacy_surface.rb +111 -0
  153. data/lib/samagotchi/terminal_ui/line_reader.rb +113 -0
  154. data/lib/samagotchi/terminal_ui/live_region.rb +36 -0
  155. data/lib/samagotchi/terminal_ui/plain_surface.rb +51 -0
  156. data/lib/samagotchi/terminal_ui/question_prompt.rb +153 -0
  157. data/lib/samagotchi/terminal_ui/question_slot.rb +131 -0
  158. data/lib/samagotchi/terminal_ui/reline_seam.rb +216 -0
  159. data/lib/samagotchi/terminal_ui/repl_input.rb +138 -0
  160. data/lib/samagotchi/terminal_ui/screen.rb +316 -0
  161. data/lib/samagotchi/terminal_ui/surface.rb +47 -0
  162. data/lib/samagotchi/terminal_ui/thinking_line.rb +101 -0
  163. data/lib/samagotchi/terminal_ui.rb +1992 -0
  164. data/lib/samagotchi/thinking_ticker.rb +110 -0
  165. data/lib/samagotchi/thought_stream_splitter.rb +149 -0
  166. data/lib/samagotchi/token_usage.rb +88 -0
  167. data/lib/samagotchi/tool_activity.rb +216 -0
  168. data/lib/samagotchi/tool_call_parser.rb +637 -0
  169. data/lib/samagotchi/tool_declarations.rb +561 -0
  170. data/lib/samagotchi/tool_runner.rb +211 -0
  171. data/lib/samagotchi/tools/args.rb +259 -0
  172. data/lib/samagotchi/tools/ask_user_question.rb +152 -0
  173. data/lib/samagotchi/tools/builtins.rb +122 -0
  174. data/lib/samagotchi/tools/cancel_reminder.rb +21 -0
  175. data/lib/samagotchi/tools/delegate.rb +167 -0
  176. data/lib/samagotchi/tools/delegate_result.rb +53 -0
  177. data/lib/samagotchi/tools/delegate_wait.rb +153 -0
  178. data/lib/samagotchi/tools/edit.rb +155 -0
  179. data/lib/samagotchi/tools/execute.rb +214 -0
  180. data/lib/samagotchi/tools/list_reminders.rb +20 -0
  181. data/lib/samagotchi/tools/list_sessions.rb +74 -0
  182. data/lib/samagotchi/tools/memory.rb +256 -0
  183. data/lib/samagotchi/tools/output_guardrails.rb +93 -0
  184. data/lib/samagotchi/tools/peers.rb +18 -0
  185. data/lib/samagotchi/tools/read.rb +182 -0
  186. data/lib/samagotchi/tools/register_reminder.rb +53 -0
  187. data/lib/samagotchi/tools/registry.rb +60 -0
  188. data/lib/samagotchi/tools/send_note.rb +49 -0
  189. data/lib/samagotchi/tools/task_create.rb +29 -0
  190. data/lib/samagotchi/tools/task_get.rb +39 -0
  191. data/lib/samagotchi/tools/task_list.rb +43 -0
  192. data/lib/samagotchi/tools/task_runtime.rb +311 -0
  193. data/lib/samagotchi/tools/task_stop.rb +29 -0
  194. data/lib/samagotchi/tools/task_wait.rb +104 -0
  195. data/lib/samagotchi/tools/tool_path.rb +18 -0
  196. data/lib/samagotchi/tools/web_fetch.rb +163 -0
  197. data/lib/samagotchi/tools/write.rb +26 -0
  198. data/lib/samagotchi/turn_flow.rb +242 -0
  199. data/lib/samagotchi/turn_note.rb +76 -0
  200. data/lib/samagotchi/turn_tally.rb +101 -0
  201. data/lib/samagotchi/version.rb +7 -0
  202. data/lib/samagotchi/vision_context.rb +132 -0
  203. data/lib/samagotchi/vision_support.rb +109 -0
  204. data/lib/samagotchi/web/app.rb +1349 -0
  205. data/lib/samagotchi/web/markdown_renderer.rb +107 -0
  206. data/lib/samagotchi/web/message_parts.rb +169 -0
  207. data/lib/samagotchi/web/public/activity.js +100 -0
  208. data/lib/samagotchi/web/public/annotations.js +67 -0
  209. data/lib/samagotchi/web/public/app.js +2382 -0
  210. data/lib/samagotchi/web/public/card.js +74 -0
  211. data/lib/samagotchi/web/public/chat_view.js +360 -0
  212. data/lib/samagotchi/web/public/chunk_router.js +25 -0
  213. data/lib/samagotchi/web/public/command_complete.js +39 -0
  214. data/lib/samagotchi/web/public/composer_size.js +19 -0
  215. data/lib/samagotchi/web/public/copy.js +142 -0
  216. data/lib/samagotchi/web/public/ctx.js +35 -0
  217. data/lib/samagotchi/web/public/data.js +256 -0
  218. data/lib/samagotchi/web/public/format.js +232 -0
  219. data/lib/samagotchi/web/public/hold.js +78 -0
  220. data/lib/samagotchi/web/public/images.js +77 -0
  221. data/lib/samagotchi/web/public/index.html +568 -0
  222. data/lib/samagotchi/web/public/init_row.js +60 -0
  223. data/lib/samagotchi/web/public/model_pick.js +23 -0
  224. data/lib/samagotchi/web/public/question_card.js +100 -0
  225. data/lib/samagotchi/web/public/route.js +17 -0
  226. data/lib/samagotchi/web/public/scope.js +36 -0
  227. data/lib/samagotchi/web/public/scroll.js +24 -0
  228. data/lib/samagotchi/web/public/sentences.js +88 -0
  229. data/lib/samagotchi/web/public/sessions_list.js +60 -0
  230. data/lib/samagotchi/web/public/strip.js +25 -0
  231. data/lib/samagotchi/web/public/tally.js +37 -0
  232. data/lib/samagotchi/web/public/thinking_ticker.js +79 -0
  233. data/lib/samagotchi/web/public/timing.js +185 -0
  234. data/lib/samagotchi/web/public/turn_events.js +209 -0
  235. data/lib/samagotchi/web/public/turn_model.js +204 -0
  236. data/lib/samagotchi/web/public/turn_view.js +587 -0
  237. data/lib/samagotchi/web/server.rb +183 -0
  238. data/lib/samagotchi/web/session_hub.rb +329 -0
  239. data/lib/samagotchi/web/session_summary.rb +85 -0
  240. data/lib/samagotchi/worker.rb +635 -0
  241. data/lib/samagotchi/worker_idle_exit.rb +87 -0
  242. data/lib/samagotchi.rb +12 -0
  243. metadata +374 -0
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "session"
4
+ require_relative "session_manager"
5
+ require_relative "context_quote"
6
+
7
+ module Samagotchi
8
+ # `chi send`: put text into sessions as the user's message, the same as
9
+ # typing it in the attached TUI or the web composer; the other half of
10
+ # `chi note`. Fire and forget: it returns once the message is queued, and
11
+ # the answer shows in whatever is attached. For a script:
12
+ # pbpaste | chi send -m "is this the same bug?" 3fa2
13
+ class SendCommand
14
+ CLIENT_ID = "cli:send"
15
+
16
+ USAGE = <<~TEXT
17
+ Usage: chi send [-m TEXT] (ID|PREFIX)...
18
+ Sends a message to each session, as if typed in it: a turn starts,
19
+ or a running one picks it up. A stopped session's worker starts.
20
+ -m TEXT the message; stdin, when piped too, goes above it as a
21
+ quote (context); without -m, stdin is the message
22
+ Only sessions on this machine. Answers show in the attached TUI
23
+ or web page, not here.
24
+ Find ids with: chi sessions list --live [--scope=all] [--format tsv]
25
+ TEXT
26
+
27
+ # @param argv [Array<String>] the arguments after "send"
28
+ def initialize(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, state_dir: nil)
29
+ @argv = argv.dup
30
+ @stdin = stdin
31
+ @stdout = stdout
32
+ @stderr = stderr
33
+ @state_dir = state_dir || Session.default_state_dir
34
+ end
35
+
36
+ # @return [Integer] exit status: 0 all sent, 1 any refused or failed,
37
+ # 2 usage
38
+ def run
39
+ options = parse or return 2
40
+ return 0 if options[:help]
41
+
42
+ prompt = compose(utf8(read_stdin), utf8(options[:message]))
43
+ unless prompt
44
+ usage_error("no message: pass -m TEXT or pipe it in")
45
+ return 2
46
+ end
47
+ begin
48
+ prompt = SessionManager.checked_text(prompt, noun: "message")
49
+ rescue SessionManager::NoteRejected => e
50
+ @stderr.puts("chi send: #{e.message}")
51
+ return 1
52
+ end
53
+
54
+ # One id at a time, so each line follows the order of the ids given.
55
+ seen = {}
56
+ results = options[:ids].uniq.map do |given|
57
+ id = resolve(given)
58
+ next false unless id
59
+ next true if seen[id]
60
+
61
+ seen[id] = true
62
+ deliver(id, prompt)
63
+ end
64
+ results.all? ? 0 : 1
65
+ end
66
+
67
+ private
68
+
69
+ def parse
70
+ options = { ids: [] }
71
+ until @argv.empty?
72
+ arg = @argv.shift
73
+ case arg
74
+ when "-h", "--help", "help"
75
+ @stdout.puts(USAGE)
76
+ return { help: true }
77
+ when "-m", "--message"
78
+ options[:message] = @argv.shift or return usage_error("#{arg} needs a value")
79
+ when /\A--message=(.*)\z/m then options[:message] = Regexp.last_match(1)
80
+ # Starting a turn in every live session at once is too easy to do
81
+ # by accident.
82
+ when "--all" then return usage_error("there is no --all: name the sessions")
83
+ when /\A-/ then return usage_error("unknown option #{arg}")
84
+ else options[:ids] << arg
85
+ end
86
+ end
87
+ return usage_error("give session ids") if options[:ids].empty?
88
+
89
+ options
90
+ end
91
+
92
+ # With both, stdin is the context quoted above the message; with one,
93
+ # it goes in as is. nil when both are blank.
94
+ def compose(context, message)
95
+ context = nil if context.to_s.strip.empty?
96
+ message = nil if message.to_s.strip.empty?
97
+ return message || context unless context && message
98
+
99
+ "#{ContextQuote.block(context)}#{message}"
100
+ end
101
+
102
+ # Only a pipe or a file is read. A terminal means nobody piped anything
103
+ # in, and a socket a launcher or an agent's shell passes down may never
104
+ # close: with -m, waiting on either would hang a script. (A pipe the
105
+ # caller never closes still hangs, as it would for cat.)
106
+ def read_stdin
107
+ return nil if @stdin.respond_to?(:tty?) && @stdin.tty?
108
+ if @stdin.respond_to?(:stat)
109
+ stat = @stdin.stat
110
+ return nil unless stat.pipe? || stat.file?
111
+ end
112
+
113
+ @stdin.read
114
+ end
115
+
116
+ def resolve(given)
117
+ id = Session.resolve_id(given, state_dir: @state_dir)
118
+ Session.load(id, state_dir: @state_dir)
119
+ id
120
+ rescue ArgumentError => e
121
+ message = e.is_a?(Session::AmbiguousId) ? e.message : "no session #{given}"
122
+ error_line("chi send: #{message}")
123
+ nil
124
+ end
125
+
126
+ # @return [Boolean] whether the message was queued
127
+ def deliver(id, prompt)
128
+ short = id[0, 8]
129
+ owner = SessionManager.session_owner(id, state_dir: @state_dir)
130
+ running = owner && Session.load(id, state_dir: @state_dir).status == Session::STATUS_RUNNING
131
+ result = SessionManager.deliver_turn(id, prompt: prompt, client_id: CLIENT_ID, state_dir: @state_dir)
132
+ unless result[:status] == :accepted
133
+ @stdout.puts("#{short} failed: #{result.dig(:ack, "detail") || "could not queue it"}")
134
+ return false
135
+ end
136
+
137
+ note = if owner.nil? then " (started its worker)"
138
+ elsif running then " (the running turn picks it up)"
139
+ end
140
+ @stdout.puts("#{short} sent#{note}")
141
+ true
142
+ rescue SessionManager::OwnedByTUI
143
+ @stdout.puts("#{short} refused: it is open in a chi REPL; messages need attached mode")
144
+ false
145
+ rescue StandardError => e
146
+ @stdout.puts("#{short} failed: #{e.message}")
147
+ false
148
+ end
149
+
150
+ # The text as UTF-8 whatever the locale says: with no LANG/LC_* (an app
151
+ # started from Finder, launchd) stdin reads as US-ASCII and ARGV as
152
+ # binary. Invalid bytes become U+FFFD rather than an error.
153
+ def utf8(text)
154
+ text&.dup&.force_encoding(Encoding::UTF_8)&.scrub
155
+ end
156
+
157
+ # stderr isn't buffered, stdout is when it's a pipe: flush the lines
158
+ # already printed, so the output keeps the order of the ids given.
159
+ def error_line(text)
160
+ @stdout.flush
161
+ @stderr.puts(text)
162
+ end
163
+
164
+ def usage_error(message)
165
+ error_line("chi send: #{message}")
166
+ @stderr.puts(USAGE)
167
+ nil
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samagotchi
4
+ # The model a server says it served, which can differ from the name asked
5
+ # for: a single-model llama.cpp answers any name with the model it loaded.
6
+ # Servers name it in /props (llama.cpp's model_alias, before any turn) and
7
+ # in each response's `model` field (see the loops' :generation_completed).
8
+ module ServedModel
9
+ module_function
10
+
11
+ # @param props [Client::ServerProps, nil] a /props probe
12
+ # @return [String, nil] llama.cpp's model_alias when the probe answered
13
+ def from_props(props)
14
+ return nil unless props&.answered? && props.body.is_a?(Hash)
15
+
16
+ name = props.body["model_alias"]
17
+ name.is_a?(String) && !name.strip.empty? ? name : nil
18
+ end
19
+
20
+ # Whether +served+ is another model than +asked+. Names that are equal
21
+ # (any case) or where one extends the other (a provider dropping
22
+ # `:free`, or adding a date) are the same model; unknown names never
23
+ # differ.
24
+ def differs?(asked, served)
25
+ asked = asked.to_s.strip.downcase
26
+ served = served.to_s.strip.downcase
27
+ return false if asked.empty? || served.empty?
28
+
29
+ !(asked.start_with?(served) || served.start_with?(asked))
30
+ end
31
+ end
32
+ end