graphite-code 0.3.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.
- graphite/__init__.py +41 -0
- graphite/__main__.py +7 -0
- graphite/_cleanup_worker.py +525 -0
- graphite/activation.py +164 -0
- graphite/agent_hooks.py +577 -0
- graphite/agent_settings.py +226 -0
- graphite/analyze.py +146 -0
- graphite/answer_contract.py +420 -0
- graphite/bootstrap.py +210 -0
- graphite/buildlock.py +99 -0
- graphite/cache.py +131 -0
- graphite/channel.py +1325 -0
- graphite/cli.py +3053 -0
- graphite/cluster.py +111 -0
- graphite/config.py +209 -0
- graphite/context.py +355 -0
- graphite/daemon.py +745 -0
- graphite/daemon_health.py +733 -0
- graphite/debt.py +118 -0
- graphite/dependency_install.py +1597 -0
- graphite/detach.py +33 -0
- graphite/doctor.py +678 -0
- graphite/doctor_probes.py +2100 -0
- graphite/engine_identity.py +238 -0
- graphite/export/__init__.py +6 -0
- graphite/export/html.py +244 -0
- graphite/export/json.py +39 -0
- graphite/export/md.py +68 -0
- graphite/extract/__init__.py +4 -0
- graphite/extract/ast.py +1964 -0
- graphite/freshness.py +127 -0
- graphite/git.py +406 -0
- graphite/graph.py +117 -0
- graphite/graph_io.py +188 -0
- graphite/health.py +147 -0
- graphite/hook_entry.py +68 -0
- graphite/hookinstall.py +224 -0
- graphite/hookshim.py +86 -0
- graphite/incident_ledger.py +247 -0
- graphite/ingest.py +279 -0
- graphite/init.py +791 -0
- graphite/io.py +32 -0
- graphite/listing.py +51 -0
- graphite/llm.py +518 -0
- graphite/llm_probe.py +157 -0
- graphite/mcp.py +7 -0
- graphite/mcp_server.py +450 -0
- graphite/natural_query.py +252 -0
- graphite/overlays.py +713 -0
- graphite/probe_process.py +879 -0
- graphite/probe_workspace.py +728 -0
- graphite/process_contracts.py +22 -0
- graphite/provider_observer.py +397 -0
- graphite/query.py +646 -0
- graphite/query_plan.py +97 -0
- graphite/replacement_audit.py +291 -0
- graphite/resolve.py +660 -0
- graphite/review.py +782 -0
- graphite/routing/__init__.py +5 -0
- graphite/routing/approval.py +362 -0
- graphite/routing/classifier.py +169 -0
- graphite/routing/claude_executor.py +419 -0
- graphite/routing/claude_probe.py +102 -0
- graphite/routing/cli_identity.py +84 -0
- graphite/routing/codex_executor.py +383 -0
- graphite/routing/codex_probe.py +93 -0
- graphite/routing/context_builder.py +327 -0
- graphite/routing/contracts.py +802 -0
- graphite/routing/diff_policy.py +468 -0
- graphite/routing/edit_apply.py +166 -0
- graphite/routing/effort.py +43 -0
- graphite/routing/lifecycle.py +771 -0
- graphite/routing/lifecycle_operator.py +227 -0
- graphite/routing/lifecycle_service.py +555 -0
- graphite/routing/lifecycle_storage.py +977 -0
- graphite/routing/ollama_executor.py +341 -0
- graphite/routing/ollama_probe.py +72 -0
- graphite/routing/openrouter_executor.py +338 -0
- graphite/routing/openrouter_probe.py +188 -0
- graphite/routing/policy.py +815 -0
- graphite/routing/probe_runner.py +543 -0
- graphite/routing/process_runner.py +523 -0
- graphite/routing/profiles.py +554 -0
- graphite/routing/prompt.py +58 -0
- graphite/routing/registry.py +444 -0
- graphite/routing/route_pool.py +629 -0
- graphite/routing/route_pool_execution.py +275 -0
- graphite/routing/schema_validation.py +169 -0
- graphite/routing/service.py +1263 -0
- graphite/routing/settings.py +99 -0
- graphite/routing/shadow.py +201 -0
- graphite/routing/storage.py +4001 -0
- graphite/routing/telemetry.py +346 -0
- graphite/routing/worktree.py +259 -0
- graphite/routing/zai_edit.py +113 -0
- graphite/routing/zai_executor.py +191 -0
- graphite/routing/zai_probe.py +126 -0
- graphite/savings.py +84 -0
- graphite/ts_bridge.py +142 -0
- graphite/ts_resolver.mjs +314 -0
- graphite/typescript_activation.py +1586 -0
- graphite/usage_ledger.py +156 -0
- graphite/validation.py +148 -0
- graphite/watch.py +167 -0
- graphite/windows_job.py +368 -0
- graphite/windows_startup.py +144 -0
- graphite/windows_task.py +212 -0
- graphite_code-0.3.0.dist-info/METADATA +743 -0
- graphite_code-0.3.0.dist-info/RECORD +112 -0
- graphite_code-0.3.0.dist-info/WHEEL +4 -0
- graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
- graphite_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Bridge a z.ai plain-text whole-file edit response into the shared apply payload.
|
|
2
|
+
|
|
3
|
+
z.ai's native executor returns a bare plain-text message (no JSON envelope), so
|
|
4
|
+
the model emits each edited file verbatim between path-qualified markers and a
|
|
5
|
+
completion sentinel. This parser converts that into the exact payload
|
|
6
|
+
``apply_whole_file_edit`` consumes. It performs structural parsing plus a
|
|
7
|
+
defense-in-depth scope-set check only; ``apply_whole_file_edit`` remains the
|
|
8
|
+
sole authority on path safety and byte caps. Every non-conformance raises
|
|
9
|
+
``AdapterError("response_contract_invalid")``.
|
|
10
|
+
|
|
11
|
+
Preamble and interstitial prose (e.g. "Here are the files:") are tolerated by
|
|
12
|
+
design — non-marker lines outside blocks are skipped and the live smoke relies
|
|
13
|
+
on this. Two accepted, documented consequences: (1) a content line byte-identical
|
|
14
|
+
to its own path-qualified end marker truncates at that line (line boundaries
|
|
15
|
+
follow ``str.splitlines`` — any Unicode line terminator, not only ``\\n`` —
|
|
16
|
+
though content bytes are always sliced verbatim from the raw message, so
|
|
17
|
+
extraction stays byte-exact); (2) prose containing a begin-marker-shaped line
|
|
18
|
+
yields a phantom block, which surfaces as a scope-set mismatch (rejected, not
|
|
19
|
+
applied). Path-qualified markers plus first-matching-end parsing make both
|
|
20
|
+
improbable.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
|
|
26
|
+
from .claude_executor import AdapterError
|
|
27
|
+
from .edit_apply import EDIT_RESULT_MARKER
|
|
28
|
+
|
|
29
|
+
EDIT_BEGIN_TEMPLATE = "===GRAPHITE BEGIN FILE {path}==="
|
|
30
|
+
EDIT_END_TEMPLATE = "===GRAPHITE END FILE {path}==="
|
|
31
|
+
|
|
32
|
+
# Trailing spaces/tabs and a CR are tolerated on marker lines (models append
|
|
33
|
+
# them constantly); a content line must still start with the exact marker
|
|
34
|
+
# prefix to match, so this stays false-positive-safe.
|
|
35
|
+
_BEGIN_RE = re.compile(r"^===GRAPHITE BEGIN FILE (?P<path>.+?)===[ \t]*\r?$")
|
|
36
|
+
_END_RE = re.compile(r"^===GRAPHITE END FILE (?P<path>.+?)===[ \t]*\r?$")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _fail() -> AdapterError:
|
|
40
|
+
return AdapterError("response_contract_invalid")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def parse_whole_file_edit_text(message: str, *, edit_scope: tuple[str, ...]) -> dict:
|
|
44
|
+
"""Parse a plain-text multi-file edit response into an apply payload.
|
|
45
|
+
|
|
46
|
+
Returns ``{"files": [{"path","content"}, …], "result": EDIT_RESULT_MARKER}``
|
|
47
|
+
with files ordered to match ``edit_scope``. Raises
|
|
48
|
+
``AdapterError("response_contract_invalid")`` on any non-conformance.
|
|
49
|
+
"""
|
|
50
|
+
if not isinstance(message, str) or not message:
|
|
51
|
+
raise _fail()
|
|
52
|
+
if (
|
|
53
|
+
not isinstance(edit_scope, tuple)
|
|
54
|
+
or not edit_scope
|
|
55
|
+
or not all(isinstance(path, str) for path in edit_scope)
|
|
56
|
+
or len(set(edit_scope)) != len(edit_scope)
|
|
57
|
+
):
|
|
58
|
+
raise _fail()
|
|
59
|
+
|
|
60
|
+
lines = message.splitlines(keepends=True)
|
|
61
|
+
starts: list[int] = []
|
|
62
|
+
offset = 0
|
|
63
|
+
for line in lines:
|
|
64
|
+
starts.append(offset)
|
|
65
|
+
offset += len(line)
|
|
66
|
+
total = offset
|
|
67
|
+
|
|
68
|
+
files: list[dict[str, str]] = []
|
|
69
|
+
seen: set[str] = set()
|
|
70
|
+
covered: list[tuple[int, int]] = []
|
|
71
|
+
idx = 0
|
|
72
|
+
n = len(lines)
|
|
73
|
+
while idx < n:
|
|
74
|
+
begin = _BEGIN_RE.match(lines[idx].rstrip("\n"))
|
|
75
|
+
if begin is None:
|
|
76
|
+
idx += 1
|
|
77
|
+
continue
|
|
78
|
+
path = begin.group("path")
|
|
79
|
+
content_start = starts[idx] + len(lines[idx])
|
|
80
|
+
jdx = idx + 1
|
|
81
|
+
end_idx: int | None = None
|
|
82
|
+
while jdx < n:
|
|
83
|
+
stripped = lines[jdx].rstrip("\n")
|
|
84
|
+
end = _END_RE.match(stripped)
|
|
85
|
+
if end is not None and end.group("path") == path:
|
|
86
|
+
end_idx = jdx
|
|
87
|
+
break
|
|
88
|
+
if _BEGIN_RE.match(stripped) is not None:
|
|
89
|
+
break # a new begin before our end -> malformed block
|
|
90
|
+
jdx += 1
|
|
91
|
+
if end_idx is None:
|
|
92
|
+
raise _fail()
|
|
93
|
+
if path in seen:
|
|
94
|
+
raise _fail()
|
|
95
|
+
seen.add(path)
|
|
96
|
+
files.append({"path": path, "content": message[content_start:starts[end_idx]]})
|
|
97
|
+
covered.append((starts[idx], starts[end_idx] + len(lines[end_idx])))
|
|
98
|
+
idx = end_idx + 1
|
|
99
|
+
|
|
100
|
+
if not files or seen != set(edit_scope):
|
|
101
|
+
raise _fail()
|
|
102
|
+
|
|
103
|
+
residual: list[str] = []
|
|
104
|
+
cursor = 0
|
|
105
|
+
for block_start, block_end in covered:
|
|
106
|
+
residual.append(message[cursor:block_start])
|
|
107
|
+
cursor = block_end
|
|
108
|
+
residual.append(message[cursor:total])
|
|
109
|
+
if EDIT_RESULT_MARKER not in "".join(residual):
|
|
110
|
+
raise _fail()
|
|
111
|
+
|
|
112
|
+
ordered = sorted(files, key=lambda item: edit_scope.index(item["path"]))
|
|
113
|
+
return {"files": ordered, "result": EDIT_RESULT_MARKER}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Isolated hardened adapter for one bounded z.ai chat completion (plain text)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
from .claude_executor import AdapterError
|
|
11
|
+
from .lifecycle import LifecycleProviderId
|
|
12
|
+
from .probe_runner import (
|
|
13
|
+
MAX_INFERENCE_REQUEST_BYTES,
|
|
14
|
+
MAX_INFERENCE_RESPONSE_BYTES,
|
|
15
|
+
MAX_INFERENCE_TIMEOUT_SECONDS,
|
|
16
|
+
HttpProbeEndpoint,
|
|
17
|
+
HttpProbeResult,
|
|
18
|
+
ProbeEndpointPurpose,
|
|
19
|
+
ProviderProbeError,
|
|
20
|
+
run_http_probe,
|
|
21
|
+
)
|
|
22
|
+
from .zai_probe import ZAI_HOST, ZaiPricing, zai_cost_microunits
|
|
23
|
+
|
|
24
|
+
MAX_TOKEN_COUNT = 10_000_000
|
|
25
|
+
MAX_COST_MICROUNITS = 1_000_000_000
|
|
26
|
+
|
|
27
|
+
HttpProbe = Callable[..., HttpProbeResult]
|
|
28
|
+
|
|
29
|
+
_EXECUTION_TRANSPORT_CODES = {
|
|
30
|
+
"probe_response_limit": "response_limit",
|
|
31
|
+
"probe_timeout": "timeout",
|
|
32
|
+
"probe_http_status": "http_status",
|
|
33
|
+
"probe_redirect_rejected": "http_status",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class ZaiExecutionResult:
|
|
39
|
+
"""Sanitized outcome of one bounded z.ai plain-text execution."""
|
|
40
|
+
|
|
41
|
+
effective_model: str
|
|
42
|
+
message: str = field(repr=False)
|
|
43
|
+
input_tokens: int
|
|
44
|
+
output_tokens: int
|
|
45
|
+
cost_microunits: int
|
|
46
|
+
duration_seconds: float
|
|
47
|
+
request_sha256: str
|
|
48
|
+
response_sha256: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _model_name(value: object) -> str:
|
|
52
|
+
if (
|
|
53
|
+
not isinstance(value, str)
|
|
54
|
+
or not value
|
|
55
|
+
or len(value) > 128
|
|
56
|
+
or any(character.isspace() for character in value)
|
|
57
|
+
):
|
|
58
|
+
raise AdapterError("request_invalid")
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _token(value: object) -> int:
|
|
63
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= MAX_TOKEN_COUNT:
|
|
64
|
+
raise AdapterError("protocol")
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def execute_zai(
|
|
69
|
+
*,
|
|
70
|
+
api_key: str,
|
|
71
|
+
prompt: bytes,
|
|
72
|
+
requested_model: str,
|
|
73
|
+
expected_effective_model: str,
|
|
74
|
+
pricing: ZaiPricing,
|
|
75
|
+
max_output_tokens: int,
|
|
76
|
+
max_cost_microunits: int,
|
|
77
|
+
timeout_seconds: float,
|
|
78
|
+
transport: HttpProbe = run_http_probe,
|
|
79
|
+
) -> ZaiExecutionResult:
|
|
80
|
+
"""Perform exactly one bounded z.ai chat completion returning plain text; no retries."""
|
|
81
|
+
if not isinstance(api_key, str) or not api_key:
|
|
82
|
+
raise AdapterError("auth_required")
|
|
83
|
+
if len(api_key) > 4096 or any(character in api_key for character in "\r\n\x00"):
|
|
84
|
+
raise AdapterError("request_invalid")
|
|
85
|
+
if (
|
|
86
|
+
not isinstance(prompt, bytes)
|
|
87
|
+
or not prompt
|
|
88
|
+
or len(prompt) > MAX_INFERENCE_REQUEST_BYTES
|
|
89
|
+
):
|
|
90
|
+
raise AdapterError("request_invalid")
|
|
91
|
+
try:
|
|
92
|
+
prompt_text = prompt.decode("utf-8")
|
|
93
|
+
except UnicodeDecodeError:
|
|
94
|
+
raise AdapterError("request_invalid") from None
|
|
95
|
+
requested = _model_name(requested_model)
|
|
96
|
+
expected = _model_name(expected_effective_model)
|
|
97
|
+
if not isinstance(pricing, ZaiPricing):
|
|
98
|
+
raise AdapterError("request_invalid")
|
|
99
|
+
for value, maximum in (
|
|
100
|
+
(max_output_tokens, MAX_TOKEN_COUNT),
|
|
101
|
+
(max_cost_microunits, MAX_COST_MICROUNITS),
|
|
102
|
+
):
|
|
103
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= maximum:
|
|
104
|
+
raise AdapterError("request_invalid")
|
|
105
|
+
if (
|
|
106
|
+
isinstance(timeout_seconds, bool)
|
|
107
|
+
or not isinstance(timeout_seconds, (int, float))
|
|
108
|
+
or not math.isfinite(timeout_seconds)
|
|
109
|
+
or not 0.1 <= timeout_seconds <= MAX_INFERENCE_TIMEOUT_SECONDS
|
|
110
|
+
):
|
|
111
|
+
raise AdapterError("request_invalid")
|
|
112
|
+
payload = {
|
|
113
|
+
"max_tokens": max_output_tokens,
|
|
114
|
+
"messages": [{"content": prompt_text, "role": "user"}],
|
|
115
|
+
"model": requested,
|
|
116
|
+
"stream": False,
|
|
117
|
+
"temperature": 0,
|
|
118
|
+
# glm-5.2 is a reasoning model; with thinking on it consumes the entire
|
|
119
|
+
# output budget on reasoning_tokens and returns empty content
|
|
120
|
+
# (finish_reason=length), so the bounded plain-text answer is never
|
|
121
|
+
# emitted. Disable thinking so the exact response is produced
|
|
122
|
+
# deterministically within the token budget (z.ai honors this).
|
|
123
|
+
"thinking": {"type": "disabled"},
|
|
124
|
+
}
|
|
125
|
+
body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
|
126
|
+
if len(body) > MAX_INFERENCE_REQUEST_BYTES:
|
|
127
|
+
raise AdapterError("request_invalid")
|
|
128
|
+
endpoint = HttpProbeEndpoint(
|
|
129
|
+
LifecycleProviderId.ZAI,
|
|
130
|
+
"https",
|
|
131
|
+
ZAI_HOST,
|
|
132
|
+
443,
|
|
133
|
+
ProbeEndpointPurpose.ZAI_CHAT_COMPLETIONS,
|
|
134
|
+
)
|
|
135
|
+
try:
|
|
136
|
+
result = transport(
|
|
137
|
+
endpoint=endpoint,
|
|
138
|
+
timeout_seconds=timeout_seconds,
|
|
139
|
+
request_body=body,
|
|
140
|
+
authorization=f"Bearer {api_key}",
|
|
141
|
+
max_response_bytes=MAX_INFERENCE_RESPONSE_BYTES,
|
|
142
|
+
)
|
|
143
|
+
except ProviderProbeError as error:
|
|
144
|
+
raise AdapterError(
|
|
145
|
+
_EXECUTION_TRANSPORT_CODES.get(error.code, "unavailable")
|
|
146
|
+
) from None
|
|
147
|
+
except Exception:
|
|
148
|
+
raise AdapterError("unavailable") from None
|
|
149
|
+
if not isinstance(result, HttpProbeResult):
|
|
150
|
+
raise AdapterError("unavailable")
|
|
151
|
+
try:
|
|
152
|
+
envelope = json.loads(result.body.decode("utf-8"))
|
|
153
|
+
except (UnicodeDecodeError, json.JSONDecodeError, RecursionError):
|
|
154
|
+
raise AdapterError("unavailable") from None
|
|
155
|
+
if not isinstance(envelope, dict):
|
|
156
|
+
raise AdapterError("protocol")
|
|
157
|
+
reported_model = envelope.get("model")
|
|
158
|
+
if not isinstance(reported_model, str) or not reported_model:
|
|
159
|
+
raise AdapterError("model_identity_unverified")
|
|
160
|
+
if reported_model != expected:
|
|
161
|
+
raise AdapterError("model_mismatch")
|
|
162
|
+
choices = envelope.get("choices")
|
|
163
|
+
if not isinstance(choices, list) or len(choices) != 1 or not isinstance(choices[0], dict):
|
|
164
|
+
raise AdapterError("protocol")
|
|
165
|
+
message = choices[0].get("message")
|
|
166
|
+
if not isinstance(message, dict):
|
|
167
|
+
raise AdapterError("protocol")
|
|
168
|
+
content = message.get("content")
|
|
169
|
+
if not isinstance(content, str):
|
|
170
|
+
raise AdapterError("protocol")
|
|
171
|
+
usage = envelope.get("usage")
|
|
172
|
+
if not isinstance(usage, dict):
|
|
173
|
+
raise AdapterError("protocol")
|
|
174
|
+
input_tokens = _token(usage.get("prompt_tokens"))
|
|
175
|
+
output_tokens = _token(usage.get("completion_tokens"))
|
|
176
|
+
try:
|
|
177
|
+
cost = zai_cost_microunits(pricing, input_tokens=input_tokens, output_tokens=output_tokens)
|
|
178
|
+
except ProviderProbeError:
|
|
179
|
+
raise AdapterError("protocol") from None
|
|
180
|
+
if cost > max_cost_microunits:
|
|
181
|
+
raise AdapterError("cost_ceiling_exceeded")
|
|
182
|
+
return ZaiExecutionResult(
|
|
183
|
+
expected,
|
|
184
|
+
content,
|
|
185
|
+
input_tokens,
|
|
186
|
+
output_tokens,
|
|
187
|
+
cost,
|
|
188
|
+
result.duration_seconds,
|
|
189
|
+
hashlib.sha256(body).hexdigest(),
|
|
190
|
+
hashlib.sha256(result.body).hexdigest(),
|
|
191
|
+
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Local (no-network) z.ai identity + operator-pinned pricing for governed verification."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from decimal import Decimal, InvalidOperation
|
|
9
|
+
|
|
10
|
+
from .contracts import CliIdentity, ProviderId
|
|
11
|
+
from .lifecycle import (
|
|
12
|
+
LifecycleProviderId,
|
|
13
|
+
ProviderCompatibilityPolicy,
|
|
14
|
+
ProviderRuntimeIdentity,
|
|
15
|
+
RuntimeKind,
|
|
16
|
+
)
|
|
17
|
+
from .probe_runner import ProviderProbeError
|
|
18
|
+
|
|
19
|
+
ZAI_HOST = "api.z.ai"
|
|
20
|
+
ZAI_CANONICAL_ENDPOINT = "https://api.z.ai/api/paas/v4"
|
|
21
|
+
ZAI_MODEL = "glm-5.2"
|
|
22
|
+
ZAI_API_CONTRACT_VERSION = "1.0.0"
|
|
23
|
+
ZAI_ADAPTER_PROTOCOL_VERSION = "1.0.0"
|
|
24
|
+
ZAI_CAPABILITIES = ("remote_inference",)
|
|
25
|
+
# z.ai model ids are single-segment (no vendor/model slash, unlike OpenRouter).
|
|
26
|
+
_ZAI_MODEL_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
|
27
|
+
_PRICE = re.compile(r"^(0|[0-9]{1,10}(\.[0-9]{1,18})?|\.[0-9]{1,18})$")
|
|
28
|
+
|
|
29
|
+
# Compatibility policy for lifecycle observe(): promotes a DISCOVERED z.ai
|
|
30
|
+
# runtime identity to VERIFICATION_REQUIRED once it matches this shape.
|
|
31
|
+
# No `_OPENROUTER_POLICY` (or any other per-provider policy constant)
|
|
32
|
+
# exists elsewhere in this codebase to clone; policies are otherwise built
|
|
33
|
+
# ad hoc at each call site. This constant is the zai-owned analog of where
|
|
34
|
+
# such a policy would live for OpenRouter (openrouter_probe.py).
|
|
35
|
+
_ZAI_POLICY = ProviderCompatibilityPolicy(
|
|
36
|
+
provider=LifecycleProviderId.ZAI,
|
|
37
|
+
runtime_kind=RuntimeKind.REMOTE_HTTPS,
|
|
38
|
+
policy_version="1.0.0",
|
|
39
|
+
minimum_version="1.0.0",
|
|
40
|
+
maximum_version_exclusive="2.0.0",
|
|
41
|
+
required_capabilities=ZAI_CAPABILITIES,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _digest(value: object) -> str:
|
|
46
|
+
return hashlib.sha256(
|
|
47
|
+
json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
|
|
48
|
+
).hexdigest()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class ZaiPricing:
|
|
53
|
+
"""Operator-pinned per-token USD prices as exact decimal strings."""
|
|
54
|
+
|
|
55
|
+
prompt: str
|
|
56
|
+
completion: str
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
for value in (self.prompt, self.completion):
|
|
60
|
+
if not isinstance(value, str) or len(value) > 64 or _PRICE.fullmatch(value) is None:
|
|
61
|
+
raise ProviderProbeError("probe_protocol_invalid")
|
|
62
|
+
try:
|
|
63
|
+
parsed = Decimal(value)
|
|
64
|
+
except InvalidOperation:
|
|
65
|
+
raise ProviderProbeError("probe_protocol_invalid") from None
|
|
66
|
+
if not Decimal(0) <= parsed <= Decimal(1):
|
|
67
|
+
raise ProviderProbeError("probe_protocol_invalid")
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def digest(self) -> str:
|
|
71
|
+
return _digest({"completion": self.completion, "prompt": self.prompt})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def zai_cost_microunits(pricing: ZaiPricing, *, input_tokens: int, output_tokens: int) -> int:
|
|
75
|
+
"""Ceiling of the exact-decimal USD cost in microunits."""
|
|
76
|
+
if not isinstance(pricing, ZaiPricing):
|
|
77
|
+
raise ProviderProbeError("probe_request_invalid")
|
|
78
|
+
for value in (input_tokens, output_tokens):
|
|
79
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= 100_000_000:
|
|
80
|
+
raise ProviderProbeError("probe_request_invalid")
|
|
81
|
+
cost = (
|
|
82
|
+
Decimal(input_tokens) * Decimal(pricing.prompt)
|
|
83
|
+
+ Decimal(output_tokens) * Decimal(pricing.completion)
|
|
84
|
+
) * Decimal(1_000_000)
|
|
85
|
+
whole = int(cost)
|
|
86
|
+
return whole if cost == whole else whole + 1
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True, slots=True)
|
|
90
|
+
class ZaiPreflight:
|
|
91
|
+
identity: CliIdentity
|
|
92
|
+
runtime: ProviderRuntimeIdentity
|
|
93
|
+
pricing: ZaiPricing
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def preflight_zai(*, model_id: str, observed_at: int, policy_version: str) -> ZaiPreflight:
|
|
97
|
+
"""Construct the z.ai runtime + CLI identity and pinned pricing locally (no network)."""
|
|
98
|
+
if not isinstance(model_id, str) or _ZAI_MODEL_ID.fullmatch(model_id) is None:
|
|
99
|
+
raise ProviderProbeError("probe_request_invalid")
|
|
100
|
+
pricing = ZaiPricing(prompt="0.0000014", completion="0.0000044")
|
|
101
|
+
endpoint_digest = hashlib.sha256(ZAI_CANONICAL_ENDPOINT.encode("ascii")).hexdigest()
|
|
102
|
+
model_digest = _digest(model_id)
|
|
103
|
+
try:
|
|
104
|
+
runtime = ProviderRuntimeIdentity(
|
|
105
|
+
LifecycleProviderId.ZAI,
|
|
106
|
+
RuntimeKind.REMOTE_HTTPS,
|
|
107
|
+
ZAI_API_CONTRACT_VERSION,
|
|
108
|
+
endpoint_digest,
|
|
109
|
+
model_digest,
|
|
110
|
+
None, # routing_policy_digest forbidden for zai
|
|
111
|
+
ZAI_CAPABILITIES,
|
|
112
|
+
policy_version,
|
|
113
|
+
observed_at,
|
|
114
|
+
)
|
|
115
|
+
except ValueError:
|
|
116
|
+
raise ProviderProbeError("probe_request_invalid") from None
|
|
117
|
+
composite = _digest(
|
|
118
|
+
{"endpoint": endpoint_digest, "model": model_digest, "pricing": pricing.digest}
|
|
119
|
+
)
|
|
120
|
+
identity = CliIdentity(
|
|
121
|
+
ProviderId.ZAI,
|
|
122
|
+
composite,
|
|
123
|
+
ZAI_API_CONTRACT_VERSION,
|
|
124
|
+
ZAI_ADAPTER_PROTOCOL_VERSION,
|
|
125
|
+
)
|
|
126
|
+
return ZaiPreflight(identity, runtime, pricing)
|
graphite/savings.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Result-scaled counterfactual estimator for the savings display.
|
|
2
|
+
|
|
3
|
+
The numbers are ESTIMATES of what equivalent manual exploration would have
|
|
4
|
+
cost, scaled by what each graphite answer actually contained; they are never
|
|
5
|
+
measurements. ``methodology()`` prints the formula and constants next to any
|
|
6
|
+
report so the claim stays auditable.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Iterable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class SavingsModel:
|
|
16
|
+
grep_tokens: int = 1000
|
|
17
|
+
grep_seconds: int = 20
|
|
18
|
+
read_seconds: int = 10
|
|
19
|
+
file_token_cap: int = 2000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
MODEL = SavingsModel()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _num(value: Any, cast: type) -> Any:
|
|
26
|
+
"""Best-effort numeric coercion; schema-corrupt values count as zero."""
|
|
27
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
28
|
+
return cast(0)
|
|
29
|
+
return cast(value)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def estimate_entry(entry: dict[str, Any], model: SavingsModel = MODEL) -> dict[str, Any]:
|
|
33
|
+
files = entry.get("files") or []
|
|
34
|
+
count = len(files)
|
|
35
|
+
grep_rounds = 1 + count // 10
|
|
36
|
+
manual_tokens = grep_rounds * model.grep_tokens + sum(
|
|
37
|
+
min(_num(f.get("bytes", 0), int) // 4, model.file_token_cap) for f in files if isinstance(f, dict)
|
|
38
|
+
)
|
|
39
|
+
manual_seconds = float(grep_rounds * model.grep_seconds + count * model.read_seconds)
|
|
40
|
+
cost_tokens = _num(entry.get("output_bytes", 0), int) // 4
|
|
41
|
+
cost_seconds = _num(entry.get("wall_ms", 0), float) / 1000.0
|
|
42
|
+
return {
|
|
43
|
+
"tokens_saved": max(0, manual_tokens - cost_tokens),
|
|
44
|
+
"seconds_saved": max(0.0, manual_seconds - cost_seconds),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def summarize(entries: Iterable[dict[str, Any]], model: SavingsModel = MODEL) -> dict[str, Any]:
|
|
49
|
+
total_tokens = 0
|
|
50
|
+
total_seconds = 0.0
|
|
51
|
+
count = 0
|
|
52
|
+
by_cmd: dict[str, dict[str, Any]] = {}
|
|
53
|
+
for entry in entries:
|
|
54
|
+
est = estimate_entry(entry, model)
|
|
55
|
+
cmd = str(entry.get("cmd", "unknown"))
|
|
56
|
+
bucket = by_cmd.setdefault(cmd, {"count": 0, "tokens_saved": 0, "seconds_saved": 0.0})
|
|
57
|
+
bucket["count"] += 1
|
|
58
|
+
bucket["tokens_saved"] += est["tokens_saved"]
|
|
59
|
+
bucket["seconds_saved"] += est["seconds_saved"]
|
|
60
|
+
total_tokens += est["tokens_saved"]
|
|
61
|
+
total_seconds += est["seconds_saved"]
|
|
62
|
+
count += 1
|
|
63
|
+
return {
|
|
64
|
+
"count": count,
|
|
65
|
+
"tokens_saved": total_tokens,
|
|
66
|
+
"seconds_saved": total_seconds,
|
|
67
|
+
"by_cmd": by_cmd,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def methodology(model: SavingsModel = MODEL) -> str:
|
|
72
|
+
return (
|
|
73
|
+
"All figures are estimates of avoided manual exploration, never measurements. "
|
|
74
|
+
f"Model: an answer covering K files ~ (1 + K//10) grep rounds x {model.grep_tokens} tokens "
|
|
75
|
+
f"/ {model.grep_seconds}s each, plus per-file read of min(bytes/4, {model.file_token_cap}) tokens "
|
|
76
|
+
f"/ {model.read_seconds}s; minus graphite's actual cost (output bytes/4 tokens, measured wall time); "
|
|
77
|
+
"floored at zero."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def format_compact(tokens: int, seconds: float) -> str:
|
|
82
|
+
token_text = f"~{tokens / 1000:.1f}k tokens" if tokens >= 1000 else f"~{tokens} tokens"
|
|
83
|
+
time_text = f"~{seconds / 60:.0f} min" if seconds >= 90 else f"~{seconds:.0f}s"
|
|
84
|
+
return f"{token_text} / {time_text}"
|
graphite/ts_bridge.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Optional TypeScript compiler bridge for accurate import/export resolution."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
8
|
+
from typing import Iterable, Any
|
|
9
|
+
|
|
10
|
+
from .config import Config
|
|
11
|
+
|
|
12
|
+
_TS_LANGUAGES = {"javascript", "typescript", "tsx", "jsx"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class TypeScriptCompilerEdge:
|
|
17
|
+
source: str
|
|
18
|
+
target: str
|
|
19
|
+
specifier: str
|
|
20
|
+
syntax: str
|
|
21
|
+
relation: str
|
|
22
|
+
confidence: str
|
|
23
|
+
line: int | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class TypeScriptCompilerIndex:
|
|
28
|
+
available: bool
|
|
29
|
+
reason: str | None = None
|
|
30
|
+
typescript_version: str | None = None
|
|
31
|
+
config_path: str | None = None
|
|
32
|
+
import_map: dict[tuple[str, str], TypeScriptCompilerEdge] = field(default_factory=dict)
|
|
33
|
+
edges_by_file: dict[str, tuple[TypeScriptCompilerEdge, ...]] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
def resolve_import(self, rel_path: str, specifier: str) -> TypeScriptCompilerEdge | None:
|
|
36
|
+
return self.import_map.get((PurePosixPath(rel_path).as_posix(), specifier))
|
|
37
|
+
|
|
38
|
+
def supplemental_edges(self, rel_path: str) -> tuple[TypeScriptCompilerEdge, ...]:
|
|
39
|
+
return self.edges_by_file.get(PurePosixPath(rel_path).as_posix(), ())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_DISABLED = {"0", "false", "off", "none", "disabled", "heuristic"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_typescript_index(root: Path, entries: Iterable[object], cfg: Config) -> TypeScriptCompilerIndex:
|
|
46
|
+
"""Build a compiler-backed TS module-resolution index, falling back silently on failure."""
|
|
47
|
+
mode = cfg.typescript_resolver.strip().lower()
|
|
48
|
+
if mode in _DISABLED:
|
|
49
|
+
return TypeScriptCompilerIndex(available=False, reason="disabled")
|
|
50
|
+
|
|
51
|
+
rel_files = sorted(
|
|
52
|
+
PurePosixPath(entry.rel_path).as_posix()
|
|
53
|
+
for entry in entries
|
|
54
|
+
if getattr(entry, "language", None) in _TS_LANGUAGES
|
|
55
|
+
)
|
|
56
|
+
if not rel_files:
|
|
57
|
+
return TypeScriptCompilerIndex(available=False, reason="no_typescript_files")
|
|
58
|
+
|
|
59
|
+
script = Path(__file__).with_name("ts_resolver.mjs")
|
|
60
|
+
payload = json.dumps({"root": str(root), "files": rel_files, "symbolReferences": cfg.typescript_symbol_references}, ensure_ascii=False)
|
|
61
|
+
try:
|
|
62
|
+
completed = subprocess.run(
|
|
63
|
+
["node", str(script)],
|
|
64
|
+
input=payload,
|
|
65
|
+
text=True,
|
|
66
|
+
# Node speaks UTF-8 in both directions, and the payload above is
|
|
67
|
+
# built with `ensure_ascii=False`, so it carries whatever non-ASCII
|
|
68
|
+
# appears in a repo's paths or identifiers. Without this the locale
|
|
69
|
+
# codec is used: on Windows that is cp1252, which cannot ENCODE the
|
|
70
|
+
# request and cannot DECODE node's reply. A decode failure there is
|
|
71
|
+
# silent -- it kills subprocess's reader thread and yields None.
|
|
72
|
+
encoding="utf-8",
|
|
73
|
+
errors="replace",
|
|
74
|
+
capture_output=True,
|
|
75
|
+
cwd=str(Path.cwd()),
|
|
76
|
+
timeout=cfg.typescript_resolver_timeout_seconds,
|
|
77
|
+
check=False,
|
|
78
|
+
)
|
|
79
|
+
except FileNotFoundError:
|
|
80
|
+
return TypeScriptCompilerIndex(available=False, reason="node_not_available")
|
|
81
|
+
except subprocess.TimeoutExpired:
|
|
82
|
+
return TypeScriptCompilerIndex(available=False, reason="timeout")
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
return TypeScriptCompilerIndex(available=False, reason=f"bridge_error: {exc}")
|
|
85
|
+
|
|
86
|
+
if completed.returncode != 0:
|
|
87
|
+
detail = (completed.stderr or completed.stdout or "").strip()[:500]
|
|
88
|
+
return TypeScriptCompilerIndex(available=False, reason=f"node_exit_{completed.returncode}: {detail}")
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
data = json.loads(completed.stdout)
|
|
92
|
+
except json.JSONDecodeError as exc:
|
|
93
|
+
return TypeScriptCompilerIndex(available=False, reason=f"invalid_json: {exc}")
|
|
94
|
+
|
|
95
|
+
if not data.get("ok"):
|
|
96
|
+
return TypeScriptCompilerIndex(available=False, reason=str(data.get("reason") or "unavailable"))
|
|
97
|
+
|
|
98
|
+
import_map: dict[tuple[str, str], TypeScriptCompilerEdge] = {}
|
|
99
|
+
edges_by_file: dict[str, list[TypeScriptCompilerEdge]] = {}
|
|
100
|
+
for raw in data.get("edges", []):
|
|
101
|
+
edge = _edge_from_raw(raw)
|
|
102
|
+
if edge is None:
|
|
103
|
+
continue
|
|
104
|
+
if edge.syntax == "import":
|
|
105
|
+
import_map[(edge.source, edge.specifier)] = edge
|
|
106
|
+
else:
|
|
107
|
+
edges_by_file.setdefault(edge.source, []).append(edge)
|
|
108
|
+
|
|
109
|
+
return TypeScriptCompilerIndex(
|
|
110
|
+
available=True,
|
|
111
|
+
typescript_version=data.get("typescriptVersion"),
|
|
112
|
+
config_path=data.get("configPath"),
|
|
113
|
+
import_map=import_map,
|
|
114
|
+
edges_by_file={k: tuple(v) for k, v in edges_by_file.items()},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _edge_from_raw(raw: dict[str, Any]) -> TypeScriptCompilerEdge | None:
|
|
119
|
+
try:
|
|
120
|
+
source = PurePosixPath(str(raw["source"])).as_posix()
|
|
121
|
+
target = PurePosixPath(str(raw["target"])).as_posix()
|
|
122
|
+
specifier = str(raw["specifier"])
|
|
123
|
+
syntax = str(raw["syntax"])
|
|
124
|
+
relation = str(raw["relation"])
|
|
125
|
+
confidence = str(raw["confidence"])
|
|
126
|
+
line_raw = raw.get("line")
|
|
127
|
+
line = int(line_raw) if line_raw is not None else None
|
|
128
|
+
except (KeyError, TypeError, ValueError):
|
|
129
|
+
return None
|
|
130
|
+
if not source or not target or source == target:
|
|
131
|
+
return None
|
|
132
|
+
return TypeScriptCompilerEdge(
|
|
133
|
+
source=source,
|
|
134
|
+
target=target,
|
|
135
|
+
specifier=specifier,
|
|
136
|
+
syntax=syntax,
|
|
137
|
+
relation=relation,
|
|
138
|
+
confidence=confidence,
|
|
139
|
+
line=line,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|