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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- 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
|