cecli-dev 1.2.2rc2__py3-none-any.whl → 1.2.3rc2__py3-none-any.whl

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.
cecli/_version.py CHANGED
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.2.2rc2'
22
- __version_tuple__ = version_tuple = (1, 2, 2, 'rc2')
21
+ __version__ = version = '1.2.3rc2'
22
+ __version_tuple__ = version_tuple = (1, 2, 3, 'rc2')
23
23
 
24
- __commit_id__ = commit_id = 'gfedaa5649'
24
+ __commit_id__ = commit_id = 'ga5ff62c22'
@@ -358,18 +358,6 @@ class ConversationManager:
358
358
  with safe_open(".cecli/logs/conversation.log", "w") as f:
359
359
  json.dump(messages_dict, f, indent=4, default=lambda o: "<not serializable>")
360
360
 
361
- # Add cache control headers when getting all messages (for LLM consumption)
362
- # Only add cache control if the coder has add_cache_headers = True
363
- if tag is None:
364
- if (
365
- coder
366
- and hasattr(coder, "add_cache_headers")
367
- and coder.add_cache_headers
368
- and hasattr(coder, "main_model")
369
- and not coder.main_model.caches_by_default
370
- and not getattr(coder.main_model, "uses_messages_api", False)
371
- ):
372
- messages_dict = self._add_cache_control(messages_dict)
373
361
  return messages_dict
374
362
 
375
363
  def clear_tag(self, tag: str, ratio: float = 0) -> None:
@@ -28,6 +28,14 @@ PROVIDER_DEFAULTS: Dict[str, Dict[str, Any]] = {
28
28
  "github_copilot": {"api_base": "https://api.githubcopilot.com", "api_key_env": None},
29
29
  "meta": {"api_base": "https://api.meta.ai/v1", "api_key_env": "META_API_KEY"},
30
30
  "chutes": {"api_base": "https://llm.chutes.ai/v1/", "api_key_env": "CHUTES_API_KEY"},
31
+ "opencode-go": {
32
+ "api_base": "https://opencode.ai/zen/go/v1",
33
+ "api_key_env": "OPENCODE_GO_API_KEY",
34
+ },
35
+ "opencode-zen": {
36
+ "api_base": "https://opencode.ai/zen/zen/v1",
37
+ "api_key_env": "OPENCODE_ZEN_API_KEY",
38
+ },
31
39
  }
32
40
 
33
41
 
@@ -45,9 +45,12 @@ def anthropic_payload(
45
45
  kwargs: Dict[str, Any],
46
46
  ) -> Dict[str, Any]:
47
47
  system = system_prompt(messages)
48
+ wire_messages = _coalesce_anthropic_messages(
49
+ [anthropic_message(m) for m in messages if m.get("role") != "system"]
50
+ )
48
51
  payload: Dict[str, Any] = {
49
52
  "model": resolved["route"],
50
- "messages": [anthropic_message(m) for m in messages if m.get("role") != "system"],
53
+ "messages": wire_messages,
51
54
  "max_tokens": (
52
55
  kwargs.get("max_tokens") or resolved.get("llm_block", {}).get("max_tokens") or 4096
53
56
  ),
@@ -274,6 +277,7 @@ async def anthropic_stream(
274
277
  "id": block.get("id", ""),
275
278
  "name": block.get("name", ""),
276
279
  "input": block.get("input") or {},
280
+ "_input_raw": "",
277
281
  }
278
282
 
279
283
  elif evt == "content_block_delta":
@@ -291,7 +295,21 @@ async def anthropic_stream(
291
295
  elif dtype == "signature_delta":
292
296
  entry["signature"] = delta.get("signature")
293
297
 
298
+ elif dtype == "input_json_delta":
299
+ entry["_input_raw"] += delta.get("partial_json") or ""
300
+
294
301
  elif evt == "content_block_stop":
302
+ entry = blocks.get(current) if current is not None else None
303
+
304
+ if entry is not None:
305
+ raw = entry.pop("_input_raw", None)
306
+
307
+ if raw is not None:
308
+ try:
309
+ entry["input"] = json.loads(raw)
310
+ except json.JSONDecodeError:
311
+ pass
312
+
295
313
  current = None
296
314
 
297
315
  chunk = parse_anthropic_chunk(json_obj)
@@ -743,6 +761,62 @@ def _is_tool_turn(msg: Dict[str, Any]) -> bool:
743
761
  )
744
762
 
745
763
 
764
+ def _coalesce_anthropic_messages(wire_messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
765
+ """Merge consecutive user turns into a single user message.
766
+
767
+ The conversation manager emits one user message per tool result and can
768
+ inject file-context text as its own user message directly after the
769
+ results, so the raw wire list can contain several ``user`` messages in a
770
+ row. The Messages API requires alternating roles and, per the tool-use
771
+ spec, ``tool_result`` blocks must come first in the user message that
772
+ follows an assistant ``tool_use`` turn. Text runs (across messages) are
773
+ concatenated with ``"\n---\n"`` separators.
774
+ """
775
+ result: List[Dict[str, Any]] = []
776
+ pending: List[Dict[str, Any]] = []
777
+
778
+ def flush() -> None:
779
+ if not pending:
780
+ return
781
+
782
+ tool_results = [b for b in pending if b.get("type") == "tool_result"]
783
+ others = [b for b in pending if b.get("type") != "tool_result"]
784
+ content: List[Dict[str, Any]] = list(tool_results)
785
+ text_parts: List[str] = []
786
+
787
+ for block in others:
788
+ if block.get("type") == "text":
789
+ text_parts.append(block.get("text") or "")
790
+
791
+ continue
792
+
793
+ if text_parts:
794
+ content.append({"type": "text", "text": "\n---\n".join(text_parts)})
795
+ text_parts = []
796
+
797
+ content.append(block)
798
+
799
+ if text_parts:
800
+ content.append({"type": "text", "text": "\n---\n".join(text_parts)})
801
+
802
+ result.append({"role": "user", "content": content})
803
+ pending.clear()
804
+
805
+ for msg in wire_messages:
806
+ if msg.get("role") != "user":
807
+ flush()
808
+ result.append(msg)
809
+ continue
810
+
811
+ content = msg.get("content")
812
+ blocks = content if isinstance(content, list) else [{"type": "text", "text": content}]
813
+ pending.extend(blocks)
814
+
815
+ flush()
816
+
817
+ return result
818
+
819
+
746
820
  __all__ = [
747
821
  "anthropic_payload",
748
822
  "anthropic_message",
cecli/models.py CHANGED
@@ -142,7 +142,6 @@ with importlib.resources.open_text("cecli.resources", "model-settings.yml") as f
142
142
 
143
143
 
144
144
  class ModelInfoManager:
145
- MODEL_INFO_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
146
145
  CACHE_TTL = 60 * 60 * 24
147
146
 
148
147
  def __init__(self):
@@ -177,28 +176,6 @@ class ModelInfoManager:
177
176
  pass
178
177
  self._cache_loaded = True
179
178
 
180
- def _update_cache(self):
181
- try:
182
- import requests
183
-
184
- response = requests.get(self.MODEL_INFO_URL, timeout=5, verify=self.verify_ssl)
185
- if response.status_code == 200:
186
- # Use json.dumps(response.json()) instead of response.text for
187
- # compatibility with mocked responses in tests
188
- parsed = response.json()
189
- self._raw_content = json.dumps(parsed)
190
- try:
191
- parsed = response.json()
192
- self.cache_file.write_text(json.dumps(parsed, indent=4))
193
- except OSError:
194
- pass
195
- except Exception as ex:
196
- print(str(ex))
197
- try:
198
- self.cache_file.write_text("{}")
199
- except OSError:
200
- pass
201
-
202
179
  def _get_entry_from_raw(self, key):
203
180
  """Parse a single model entry from raw JSON string without loading the entire dict."""
204
181
  return get_entry_from_raw(self._raw_content, key)
@@ -208,8 +185,6 @@ class ModelInfoManager:
208
185
  if data:
209
186
  return data
210
187
  self._load_cache()
211
- if not self._raw_content:
212
- self._update_cache()
213
188
  if not self._raw_content:
214
189
  return dict()
215
190
  info = self._get_entry_from_raw(model)