algo-cli-runtime 0.14.0__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.
Files changed (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
@@ -0,0 +1,657 @@
1
+ """ChatGPT/OpenAI-compatible chat client adapter.
2
+
3
+ Uses ChatGPT/OpenAI OAuth tokens from chatgpt_auth and exposes an
4
+ ollama.Client.chat()-shaped interface so agent_loop can reuse one provider path.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import subprocess
11
+ import tempfile
12
+ import uuid
13
+ import urllib.error
14
+ import urllib.request
15
+ from pathlib import Path
16
+ from typing import Any, Callable, Iterator
17
+
18
+ from . import chatgpt_auth
19
+
20
+ try:
21
+ from ollama._utils import convert_function_to_tool
22
+ except Exception: # pragma: no cover
23
+ convert_function_to_tool = None # type: ignore[assignment]
24
+
25
+
26
+ class ChatGptOAuthAccessError(RuntimeError):
27
+ """Raised when ChatGPT OAuth access is unavailable."""
28
+
29
+
30
+ CODEX_SUBSCRIPTION_MODELS = {
31
+ "gpt-5.5",
32
+ "gpt-5.4",
33
+ "gpt-5.4-mini",
34
+ "gpt-5.3-codex-spark",
35
+ "gpt-5.1-codex",
36
+ }
37
+ _MODEL_REQUEST_SCOPE_MISSING = False
38
+ CODEX_RESPONSES_BASE_URL = os.environ.get("OPENAI_CODEX_RESPONSES_BASE", "https://chatgpt.com/backend-api").rstrip("/")
39
+ CODEX_RESPONSES_ORIGINATOR = os.environ.get("OPENAI_CODEX_ORIGINATOR", "pi").strip() or "pi"
40
+ _REASONING_EFFORT_ALIASES = {
41
+ "": "medium",
42
+ "default": "medium",
43
+ "normal": "medium",
44
+ "med": "medium",
45
+ "maximum": "xhigh",
46
+ "max": "xhigh",
47
+ "extra-high": "xhigh",
48
+ "extra_high": "xhigh",
49
+ "very-high": "xhigh",
50
+ "very_high": "xhigh",
51
+ }
52
+ _REASONING_EFFORT_LEVELS = {"minimal", "low", "medium", "high", "xhigh"}
53
+
54
+
55
+ def is_codex_subscription_model(model: str) -> bool:
56
+ return str(model or "").split(":", 1)[0] in CODEX_SUBSCRIPTION_MODELS
57
+
58
+
59
+ def _normalize_reasoning_effort(value: Any) -> str:
60
+ text = str(value or "").strip().lower()
61
+ normalized = _REASONING_EFFORT_ALIASES.get(text, text)
62
+ if normalized in _REASONING_EFFORT_LEVELS:
63
+ return normalized
64
+ return "medium"
65
+
66
+
67
+ def _messages_to_codex_prompt(messages: list[dict[str, Any]]) -> str:
68
+ lines = [
69
+ "You are being invoked by algo-cli through the OpenAI Codex CLI subscription runtime.",
70
+ "Answer the latest user request directly. Do not mention this transport unless asked.",
71
+ "",
72
+ "Conversation:",
73
+ ]
74
+ for msg in messages[-20:]:
75
+ role = str(msg.get("role") or "unknown")
76
+ content = msg.get("content", "")
77
+ if not content and msg.get("tool_calls"):
78
+ content = f"[tool calls: {json.dumps(msg.get('tool_calls'), ensure_ascii=False)}]"
79
+ lines.append(f"[{role}] {content}")
80
+ return "\n".join(lines).strip()
81
+
82
+
83
+ def _run_codex_exec(
84
+ model: str,
85
+ messages: list[dict[str, Any]],
86
+ *,
87
+ reasoning_effort: str = "medium",
88
+ timeout: float = 300.0,
89
+ runner: Any = subprocess.run,
90
+ ) -> str:
91
+ codex_bin = chatgpt_auth.resolve_codex_bin()
92
+ if not codex_bin:
93
+ raise ChatGptOAuthAccessError("Codex CLI is not installed or not discoverable. Run /chatgpt-login again after installing Codex.")
94
+ if not chatgpt_auth.get_valid_token():
95
+ raise ChatGptOAuthAccessError("Not authenticated with ChatGPT/Codex OAuth. Run /chatgpt-login first.")
96
+ prompt = _messages_to_codex_prompt(messages)
97
+ env = os.environ.copy()
98
+ env["CODEX_HOME"] = str(chatgpt_auth.CODEX_AUTH_HOME)
99
+ with tempfile.TemporaryDirectory(prefix="algo-codex-") as tmpdir:
100
+ output_path = Path(tmpdir) / "last-message.txt"
101
+ cmd = [
102
+ codex_bin,
103
+ "exec",
104
+ "-m",
105
+ model,
106
+ "-c",
107
+ f'model_reasoning_effort="{_normalize_reasoning_effort(reasoning_effort)}"',
108
+ "--skip-git-repo-check",
109
+ "--sandbox",
110
+ "read-only",
111
+ "--color",
112
+ "never",
113
+ "-o",
114
+ str(output_path),
115
+ "-",
116
+ ]
117
+ try:
118
+ result = runner(
119
+ cmd,
120
+ input=prompt,
121
+ text=True,
122
+ encoding="utf-8",
123
+ errors="replace",
124
+ capture_output=True,
125
+ timeout=timeout,
126
+ env=env,
127
+ check=False,
128
+ )
129
+ except subprocess.TimeoutExpired as exc:
130
+ raise ChatGptOAuthAccessError(f"Codex CLI timed out after {timeout:.0f}s for model {model}.") from exc
131
+ except FileNotFoundError as exc:
132
+ raise ChatGptOAuthAccessError("Codex CLI is not installed or not discoverable. Run /chatgpt-login again.") from exc
133
+ if getattr(result, "returncode", 0) != 0:
134
+ stderr = str(getattr(result, "stderr", "") or "").strip()
135
+ stdout = str(getattr(result, "stdout", "") or "").strip()
136
+ detail = (stderr or stdout or "no output")[-1500:]
137
+ raise ChatGptOAuthAccessError(f"Codex CLI request failed for {model}: {detail}")
138
+ if output_path.exists():
139
+ text = output_path.read_text(encoding="utf-8", errors="replace").strip()
140
+ if text:
141
+ return text
142
+ stdout = str(getattr(result, "stdout", "") or "").strip()
143
+ if stdout:
144
+ return stdout.splitlines()[-1].strip()
145
+ raise ChatGptOAuthAccessError(f"Codex CLI completed for {model} but produced no response.")
146
+
147
+
148
+ def _reasoning_effort_from_options(options: dict[str, Any] | None) -> str:
149
+ if not options:
150
+ return "medium"
151
+ effort = options.get("reasoning_effort") or options.get("chatgpt_reasoning_effort") or "medium"
152
+ return _normalize_reasoning_effort(effort)
153
+
154
+
155
+ def _codex_exec_chunk(model: str, messages: list[dict[str, Any]], options: dict[str, Any] | None) -> dict[str, Any]:
156
+ content = _run_codex_exec(
157
+ model,
158
+ messages,
159
+ reasoning_effort=_reasoning_effort_from_options(options),
160
+ timeout=300.0,
161
+ )
162
+ return {"message": {"content": content}}
163
+
164
+
165
+ def _is_missing_model_request_scope(exc: Exception) -> bool:
166
+ text = str(exc).lower()
167
+ return "missing scopes" in text and "model.request" in text
168
+
169
+
170
+ def _build_openai_tools(tools: list[Callable[..., Any]] | list[dict[str, Any]] | None) -> list[dict[str, Any]] | None:
171
+ if not tools:
172
+ return None
173
+ out: list[dict[str, Any]] = []
174
+ for item in tools:
175
+ if isinstance(item, dict):
176
+ out.append(item)
177
+ continue
178
+ if convert_function_to_tool is None:
179
+ continue
180
+ try:
181
+ out.append(convert_function_to_tool(item).model_dump(exclude_none=True))
182
+ except Exception:
183
+ continue
184
+ return out or None
185
+
186
+
187
+ def _build_responses_tools(tools: list[Callable[..., Any]] | list[dict[str, Any]] | None) -> list[dict[str, Any]] | None:
188
+ built = _build_openai_tools(tools)
189
+ if not built:
190
+ return None
191
+ out: list[dict[str, Any]] = []
192
+ for tool in built:
193
+ if tool.get("type") != "function":
194
+ continue
195
+ fn = tool.get("function")
196
+ if isinstance(fn, dict):
197
+ out.append(
198
+ {
199
+ "type": "function",
200
+ "name": fn.get("name", ""),
201
+ "description": fn.get("description", ""),
202
+ "parameters": fn.get("parameters") or {"type": "object", "properties": {}},
203
+ "strict": fn.get("strict"),
204
+ }
205
+ )
206
+ else:
207
+ out.append(tool)
208
+ return out or None
209
+
210
+
211
+ def _build_openai_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
212
+ out: list[dict[str, Any]] = []
213
+ pending_call_ids: list[str] = []
214
+ counter = 0
215
+ for msg in messages:
216
+ role = msg.get("role")
217
+ if role == "assistant" and msg.get("tool_calls"):
218
+ calls_out: list[dict[str, Any]] = []
219
+ for call in msg.get("tool_calls") or []:
220
+ fn = call.get("function", {}) if isinstance(call, dict) else getattr(call, "function", {})
221
+ call_id = call.get("id") if isinstance(call, dict) else getattr(call, "id", None)
222
+ if isinstance(fn, dict):
223
+ name = fn.get("name", "")
224
+ args = fn.get("arguments", "")
225
+ else:
226
+ name = getattr(fn, "name", "")
227
+ args = getattr(fn, "arguments", "")
228
+ if not isinstance(args, str):
229
+ args = json.dumps(args, ensure_ascii=False)
230
+ if not call_id:
231
+ counter += 1
232
+ call_id = f"call_{counter}"
233
+ call_id = str(call_id)
234
+ pending_call_ids.append(call_id)
235
+ calls_out.append({"id": call_id, "type": "function", "function": {"name": name, "arguments": args or "{}"}})
236
+ translated: dict[str, Any] = {"role": "assistant", "tool_calls": calls_out}
237
+ if msg.get("content"):
238
+ translated["content"] = msg["content"]
239
+ out.append(translated)
240
+ elif role == "tool":
241
+ explicit_call_id = msg.get("tool_call_id")
242
+ if explicit_call_id:
243
+ call_id = str(explicit_call_id)
244
+ if call_id not in pending_call_ids:
245
+ continue
246
+ pending_call_ids.remove(call_id)
247
+ elif pending_call_ids:
248
+ call_id = pending_call_ids.pop(0)
249
+ else:
250
+ continue
251
+ out.append(
252
+ {
253
+ "role": "tool",
254
+ "tool_call_id": call_id,
255
+ "content": str(msg.get("content", "")),
256
+ }
257
+ )
258
+ else:
259
+ keep = {k: v for k, v in msg.items() if k in {"role", "content"}}
260
+ if keep:
261
+ out.append(keep)
262
+ return out
263
+
264
+
265
+ def _build_responses_input(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
266
+ out: list[dict[str, Any]] = []
267
+ valid_call_ids: set[str] = set()
268
+ output_call_ids = {
269
+ str(msg.get("tool_call_id"))
270
+ for msg in messages
271
+ if msg.get("role") == "tool" and msg.get("tool_call_id")
272
+ }
273
+ counter = 0
274
+ for msg in messages:
275
+ role = msg.get("role")
276
+ content = msg.get("content", "")
277
+ if role == "assistant" and msg.get("tool_calls"):
278
+ if content:
279
+ out.append({"role": "assistant", "content": str(content)})
280
+ for call in msg.get("tool_calls") or []:
281
+ fn = call.get("function", {}) if isinstance(call, dict) else getattr(call, "function", {})
282
+ call_id = call.get("id") if isinstance(call, dict) else getattr(call, "id", None)
283
+ if isinstance(fn, dict):
284
+ name = str(fn.get("name", ""))
285
+ args = fn.get("arguments", "{}")
286
+ else:
287
+ name = str(getattr(fn, "name", ""))
288
+ args = getattr(fn, "arguments", "{}")
289
+ if not name.strip():
290
+ continue
291
+ if not isinstance(args, str):
292
+ args = json.dumps(args, ensure_ascii=False)
293
+ if not call_id:
294
+ counter += 1
295
+ call_id = f"call_{counter}"
296
+ call_id = str(call_id)
297
+ if call_id not in output_call_ids:
298
+ continue
299
+ valid_call_ids.add(call_id)
300
+ out.append({"type": "function_call", "call_id": call_id, "name": name, "arguments": args or "{}"})
301
+ elif role == "tool":
302
+ raw_call_id = msg.get("tool_call_id")
303
+ if raw_call_id:
304
+ call_id = str(raw_call_id)
305
+ if call_id not in valid_call_ids:
306
+ continue
307
+ else:
308
+ continue
309
+ out.append({"type": "function_call_output", "call_id": call_id, "output": str(content)})
310
+ elif role in {"system", "developer", "user", "assistant"}:
311
+ mapped_role = "developer" if role == "system" else role
312
+ out.append({"role": mapped_role, "content": str(content)})
313
+ return out
314
+
315
+
316
+ def _post_chat(payload: dict[str, Any], *, stream: bool, timeout: float = 120.0) -> Any:
317
+ token = chatgpt_auth.get_valid_token()
318
+ if not token:
319
+ raise ChatGptOAuthAccessError("Not authenticated with ChatGPT OAuth. Run /chatgpt-login first.")
320
+ body = json.dumps(payload).encode("utf-8")
321
+ req = urllib.request.Request(
322
+ f"{chatgpt_auth.CHATGPT_API_BASE}/chat/completions",
323
+ data=body,
324
+ headers={
325
+ "Authorization": f"Bearer {token}",
326
+ "Content-Type": "application/json",
327
+ "Accept": "text/event-stream" if stream else "application/json",
328
+ },
329
+ method="POST",
330
+ )
331
+ try:
332
+ return urllib.request.urlopen(req, timeout=timeout)
333
+ except urllib.error.HTTPError as exc:
334
+ detail = ""
335
+ try:
336
+ detail = exc.read().decode("utf-8", errors="replace")[:1500].strip()
337
+ except Exception:
338
+ pass
339
+ raise ChatGptOAuthAccessError(f"ChatGPT OAuth request failed ({exc.code}): {detail or '(no body)'}") from exc
340
+
341
+
342
+ def _codex_responses_url() -> str:
343
+ base = CODEX_RESPONSES_BASE_URL
344
+ if base.endswith("/codex/responses"):
345
+ return base
346
+ return f"{base}/codex/responses"
347
+
348
+
349
+ def _is_token_invalidated_error(detail: str) -> bool:
350
+ text = str(detail or "").lower()
351
+ return "token_invalidated" in text or "authentication token has been invalidated" in text
352
+
353
+
354
+ def _build_codex_responses_request(payload: dict[str, Any], *, token: str, account_id: str) -> urllib.request.Request:
355
+ body = json.dumps(payload).encode("utf-8")
356
+ request_id = str(uuid.uuid4())
357
+ return urllib.request.Request(
358
+ _codex_responses_url(),
359
+ data=body,
360
+ headers={
361
+ "Authorization": f"Bearer {token}",
362
+ "Content-Type": "application/json",
363
+ "Accept": "text/event-stream",
364
+ "chatgpt-account-id": account_id,
365
+ "originator": CODEX_RESPONSES_ORIGINATOR,
366
+ "User-Agent": "pi (Windows; x64)",
367
+ "OpenAI-Beta": "responses=experimental",
368
+ "x-client-request-id": request_id,
369
+ },
370
+ method="POST",
371
+ )
372
+
373
+
374
+ def _post_codex_responses(payload: dict[str, Any], *, timeout: float = 120.0, _retried: bool = False) -> Any:
375
+ token = chatgpt_auth.get_valid_token()
376
+ if not token:
377
+ raise ChatGptOAuthAccessError("Not authenticated with ChatGPT/Codex OAuth. Run /chatgpt-login first.")
378
+ account_id = chatgpt_auth.get_chatgpt_account_id()
379
+ if not account_id:
380
+ raise ChatGptOAuthAccessError(
381
+ "ChatGPT/Codex OAuth token does not include a ChatGPT account id. Run /chatgpt-login again."
382
+ )
383
+ req = _build_codex_responses_request(payload, token=token, account_id=account_id)
384
+ try:
385
+ return urllib.request.urlopen(req, timeout=timeout)
386
+ except urllib.error.HTTPError as exc:
387
+ detail = ""
388
+ try:
389
+ detail = exc.read().decode("utf-8", errors="replace")[:1500].strip()
390
+ except Exception:
391
+ pass
392
+ if exc.code == 401 and not _retried and _is_token_invalidated_error(detail):
393
+ refreshed = chatgpt_auth.force_refresh_token()
394
+ if refreshed:
395
+ return _post_codex_responses(payload, timeout=timeout, _retried=True)
396
+ raise ChatGptOAuthAccessError(f"ChatGPT Codex Responses request failed ({exc.code}): {detail or '(no body)'}") from exc
397
+
398
+
399
+ def _parse_sse_events(resp: Any) -> Iterator[dict[str, Any]]:
400
+ try:
401
+ for raw in resp:
402
+ line = raw.decode("utf-8", errors="replace").rstrip("\r\n")
403
+ if not line.startswith("data:"):
404
+ continue
405
+ data = line[5:].strip()
406
+ if not data:
407
+ continue
408
+ if data == "[DONE]":
409
+ return
410
+ try:
411
+ yield json.loads(data)
412
+ except json.JSONDecodeError:
413
+ continue
414
+ finally:
415
+ try:
416
+ resp.close()
417
+ except Exception:
418
+ pass
419
+
420
+
421
+ def _stream_iter(resp: Any) -> Iterator[dict[str, Any]]:
422
+ pending_calls: dict[int, dict[str, Any]] = {}
423
+ for event in _parse_sse_events(resp):
424
+ choices = event.get("choices") or []
425
+ if not choices:
426
+ continue
427
+ choice = choices[0]
428
+ delta = choice.get("delta") or {}
429
+ finish_reason = choice.get("finish_reason")
430
+ if delta.get("content"):
431
+ yield {"message": {"content": delta["content"]}}
432
+ for tc_delta in delta.get("tool_calls") or []:
433
+ idx = int(tc_delta.get("index", 0))
434
+ slot = pending_calls.setdefault(idx, {"function": {"name": "", "arguments": ""}})
435
+ if tc_delta.get("id"):
436
+ slot["id"] = tc_delta["id"]
437
+ if tc_delta.get("type"):
438
+ slot["type"] = tc_delta["type"]
439
+ fn_delta = tc_delta.get("function") or {}
440
+ if fn_delta.get("name"):
441
+ slot["function"]["name"] = fn_delta["name"]
442
+ if fn_delta.get("arguments"):
443
+ slot["function"]["arguments"] += fn_delta["arguments"]
444
+ if finish_reason in {"tool_calls", "stop"} and pending_calls:
445
+ completed = [pending_calls[i] for i in sorted(pending_calls)]
446
+ pending_calls.clear()
447
+ yield {"message": {"tool_calls": completed}}
448
+ if pending_calls:
449
+ yield {"message": {"tool_calls": [pending_calls[i] for i in sorted(pending_calls)]}}
450
+
451
+
452
+ def _extract_responses_event_text(event: dict[str, Any]) -> str:
453
+ if isinstance(event.get("delta"), str):
454
+ return event["delta"]
455
+ if isinstance(event.get("text"), str):
456
+ return event["text"]
457
+ if isinstance(event.get("content"), str):
458
+ return event["content"]
459
+ return ""
460
+
461
+
462
+ def _stream_codex_responses_iter(resp: Any) -> Iterator[dict[str, Any]]:
463
+ pending_calls: dict[str, dict[str, Any]] = {}
464
+ order: list[str] = []
465
+
466
+ def completed_calls() -> list[dict[str, Any]]:
467
+ return [
468
+ pending_calls[call_id]
469
+ for call_id in order
470
+ if pending_calls.get(call_id)
471
+ and str(pending_calls[call_id].get("function", {}).get("name") or "").strip()
472
+ ]
473
+
474
+ for event in _parse_sse_events(resp):
475
+ event_type = str(event.get("type") or "")
476
+ if event_type in {"response.output_text.delta", "response.refusal.delta"}:
477
+ text = _extract_responses_event_text(event)
478
+ if text:
479
+ yield {"message": {"content": text}}
480
+ continue
481
+ if event_type == "response.output_item.added":
482
+ item = event.get("item") or {}
483
+ if item.get("type") != "function_call":
484
+ continue
485
+ call_id = str(item.get("call_id") or item.get("id") or f"call_{len(order) + 1}")
486
+ if call_id not in pending_calls:
487
+ order.append(call_id)
488
+ pending_calls[call_id] = {
489
+ "id": call_id,
490
+ "type": "function",
491
+ "function": {"name": str(item.get("name", "")), "arguments": str(item.get("arguments") or "")},
492
+ }
493
+ continue
494
+ if event_type == "response.function_call_arguments.delta":
495
+ call_id = str(event.get("call_id") or event.get("item_id") or "")
496
+ if not call_id:
497
+ continue
498
+ if call_id not in pending_calls:
499
+ order.append(call_id)
500
+ pending_calls[call_id] = {"id": call_id, "type": "function", "function": {"name": "", "arguments": ""}}
501
+ pending_calls[call_id]["function"]["arguments"] += str(event.get("delta") or "")
502
+ continue
503
+ if event_type in {"response.function_call_arguments.done", "response.output_item.done"}:
504
+ item = event.get("item") or {}
505
+ call_id = str(event.get("call_id") or item.get("call_id") or item.get("id") or "")
506
+ if not call_id:
507
+ continue
508
+ if call_id not in pending_calls:
509
+ order.append(call_id)
510
+ pending_calls[call_id] = {"id": call_id, "type": "function", "function": {"name": "", "arguments": ""}}
511
+ if item.get("name"):
512
+ pending_calls[call_id]["function"]["name"] = str(item["name"])
513
+ if item.get("arguments") is not None:
514
+ pending_calls[call_id]["function"]["arguments"] = str(item.get("arguments") or "")
515
+ completed = completed_calls()
516
+ if completed:
517
+ yield {"message": {"tool_calls": completed}}
518
+
519
+
520
+ def _nonstream_to_chunk(body: dict[str, Any]) -> dict[str, Any]:
521
+ choice = (body.get("choices") or [{}])[0]
522
+ msg = choice.get("message") or {}
523
+ out_msg: dict[str, Any] = {}
524
+ if msg.get("content"):
525
+ out_msg["content"] = msg["content"]
526
+ if msg.get("tool_calls"):
527
+ out_msg["tool_calls"] = msg["tool_calls"]
528
+ chunk: dict[str, Any] = {"message": out_msg}
529
+ if body.get("usage"):
530
+ chunk["usage"] = body["usage"]
531
+ return chunk
532
+
533
+
534
+ def _codex_responses_to_chunk(resp: Any) -> dict[str, Any]:
535
+ content_parts: list[str] = []
536
+ tool_calls: list[dict[str, Any]] = []
537
+ for chunk in _stream_codex_responses_iter(resp):
538
+ message = chunk.get("message") or {}
539
+ if message.get("content"):
540
+ content_parts.append(str(message["content"]))
541
+ if message.get("tool_calls"):
542
+ tool_calls.extend(message["tool_calls"])
543
+ out_msg: dict[str, Any] = {}
544
+ if content_parts:
545
+ out_msg["content"] = "".join(content_parts)
546
+ if tool_calls:
547
+ out_msg["tool_calls"] = tool_calls
548
+ return {"message": out_msg}
549
+
550
+
551
+ class ChatGptClient:
552
+ """Ollama-shaped chat client routed to the OpenAI-compatible Chat Completions API."""
553
+
554
+ def chat(
555
+ self,
556
+ *,
557
+ model: str,
558
+ messages: list[dict[str, Any]],
559
+ tools: list[Callable[..., Any]] | list[dict[str, Any]] | None = None,
560
+ stream: bool = False,
561
+ options: dict[str, Any] | None = None,
562
+ **_ignored: Any,
563
+ ) -> Any:
564
+ global _MODEL_REQUEST_SCOPE_MISSING
565
+ if is_codex_subscription_model(model) and not tools:
566
+ chunk = _codex_exec_chunk(model, messages, options)
567
+ if stream:
568
+ return iter([chunk])
569
+ return chunk
570
+
571
+ if is_codex_subscription_model(model) and _MODEL_REQUEST_SCOPE_MISSING:
572
+ chunk = _codex_exec_chunk(model, messages, options)
573
+ note = (
574
+ "Note: ChatGPT OAuth is Codex-CLI-only in this session, so this response used "
575
+ "Codex CLI fallback without Algo CLI tool calls. For full file edits, shell, "
576
+ "approval gates, and tool ledger, switch to a local/Ollama/xAI tool-capable model "
577
+ "or authenticate with an OpenAI token that has model.request.\n\n"
578
+ )
579
+ chunk["message"]["content"] = note + str(chunk["message"].get("content", ""))
580
+ if stream:
581
+ return iter([chunk])
582
+ return chunk
583
+
584
+ if is_codex_subscription_model(model):
585
+ payload: dict[str, Any] = {
586
+ "model": model,
587
+ "store": False,
588
+ "stream": True,
589
+ "input": _build_responses_input(messages),
590
+ "include": ["reasoning.encrypted_content"],
591
+ "tool_choice": "auto",
592
+ "parallel_tool_calls": True,
593
+ }
594
+ built_tools = _build_responses_tools(tools)
595
+ if built_tools:
596
+ payload["tools"] = built_tools
597
+ if options:
598
+ effort = options.get("reasoning_effort") or options.get("chatgpt_reasoning_effort")
599
+ if effort:
600
+ payload["reasoning"] = {
601
+ "effort": _normalize_reasoning_effort(effort),
602
+ "summary": "auto",
603
+ }
604
+ if "num_predict" in options:
605
+ payload["max_output_tokens"] = options["num_predict"]
606
+ resp = _post_codex_responses(payload, timeout=120.0)
607
+ if stream:
608
+ return _stream_codex_responses_iter(resp)
609
+ return _codex_responses_to_chunk(resp)
610
+
611
+ payload: dict[str, Any] = {
612
+ "model": model,
613
+ "messages": _build_openai_messages(messages),
614
+ "stream": bool(stream),
615
+ }
616
+ built_tools = _build_openai_tools(tools)
617
+ if built_tools:
618
+ payload["tools"] = built_tools
619
+ if options:
620
+ if "temperature" in options:
621
+ payload["temperature"] = options["temperature"]
622
+ if "num_predict" in options:
623
+ payload["max_tokens"] = options["num_predict"]
624
+ try:
625
+ resp = _post_chat(payload, stream=stream, timeout=120.0)
626
+ except ChatGptOAuthAccessError as exc:
627
+ if not is_codex_subscription_model(model) or not _is_missing_model_request_scope(exc):
628
+ raise
629
+ _MODEL_REQUEST_SCOPE_MISSING = True
630
+ chunk = _codex_exec_chunk(model, messages, options)
631
+ note = (
632
+ "Note: ChatGPT OAuth lacks the model.request scope, so this response used "
633
+ "Codex CLI fallback without Algo CLI tool calls. For full file edits, shell, "
634
+ "approval gates, and tool ledger, switch to a local/Ollama/xAI tool-capable model "
635
+ "or authenticate with an OpenAI token that has model.request.\n\n"
636
+ )
637
+ chunk["message"]["content"] = note + str(chunk["message"].get("content", ""))
638
+ if stream:
639
+ return iter([chunk])
640
+ return chunk
641
+ if stream:
642
+ return _stream_iter(resp)
643
+ try:
644
+ body = json.loads(resp.read().decode("utf-8"))
645
+ finally:
646
+ try:
647
+ resp.close()
648
+ except Exception:
649
+ pass
650
+ return _nonstream_to_chunk(body)
651
+
652
+
653
+ _CLIENT = ChatGptClient()
654
+
655
+
656
+ def active_chatgpt_client() -> ChatGptClient:
657
+ return _CLIENT