feat(openai_compat_transport): port to FULL Go — dichotomy wave
CI Pipeline / rust-surface (push) Failing after 17s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 19s
CI Pipeline / best-practices (push) Canceled after 1s
Auto-version and Release / release (push) Canceled after 27s
CI Pipeline / canary (push) Canceled after 21s
deploy / deploy (push) Canceled after 25s
CI Pipeline / lint (push) Canceled after 25s
CI Pipeline / unit-tests (push) Canceled after 25s
CI Pipeline / rust-surface (push) Failing after 17s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 19s
CI Pipeline / best-practices (push) Canceled after 1s
Auto-version and Release / release (push) Canceled after 27s
CI Pipeline / canary (push) Canceled after 21s
deploy / deploy (push) Canceled after 25s
CI Pipeline / lint (push) Canceled after 25s
CI Pipeline / unit-tests (push) Canceled after 25s
This commit is contained in:
1 parent
ee411f8e6e
commit
2e3b7e76ec
10 files changed
+4627
No files matched your search
@@ -0,0 +1,526 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze truth for the openai_compat_transport Go port (dichotomy wave #2479).
|
||||
|
||||
Exercises EVERY public function of src/rune/openai_compat_transport.py against
|
||||
the LIVE oracle: __init__ (timeout clamp), _post (wire + non-object guard),
|
||||
_to_openai_messages (all branches), _from_openai_response (all branches),
|
||||
chat (model split, tools, options, usage), supports_tools, supports_thinking,
|
||||
context_length, is_cloud_pinned, cloud_transport_for (config/env/auth seams).
|
||||
|
||||
Wire IO served by a local stub HTTP server that captures each request
|
||||
(path, authorization, content-type, body bytes) and returns scripted bodies;
|
||||
cloud_transport_for's IO seams (os.path.expanduser, os.environ) are patched
|
||||
to temp files so config/auth resolution is deterministic.
|
||||
|
||||
Excluded branches (interpreter-specific error text — not portable truth):
|
||||
- _post on a non-2xx HTTP status (Python urllib HTTPError string vs Go
|
||||
net/http error string) and on connection failures.
|
||||
- _post on a body that is invalid JSON (Python json.JSONDecodeError text).
|
||||
- cloud_transport_for with auth.json containing invalid JSON (same reason).
|
||||
|
||||
Numbers: fixtures are written by json.dump so every numeric literal is in
|
||||
Python's canonical repr; the Go port preserves literals on parse. Floats in
|
||||
fixtures stay in the shortest-decimal band (no exponents) so literal
|
||||
preservation equals Python re-rendering.
|
||||
|
||||
Nondeterministic: usage_total_duration_ns (time.monotonic) is excluded from
|
||||
every row; the Go battery asserts it is > 0 after a chat instead.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
import rune.openai_compat_transport as oct_mod # noqa: E402
|
||||
|
||||
|
||||
# -- stub wire ---------------------------------------------------------------
|
||||
|
||||
class StubState:
|
||||
def __init__(self):
|
||||
self.script = [] # list of response body strings
|
||||
self.requests = [] # captured requests, one dict per POST
|
||||
|
||||
def reset(self, bodies):
|
||||
self.script = list(bodies)
|
||||
self.requests = []
|
||||
|
||||
|
||||
STUB = StubState()
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
STUB.requests.append({
|
||||
"path": self.path,
|
||||
"authorization": self.headers.get("Authorization"),
|
||||
"content_type": self.headers.get("Content-Type"),
|
||||
"body": body.decode("utf-8"),
|
||||
})
|
||||
resp = STUB.script.pop(0)
|
||||
data = resp.encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
def start_stub():
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
return server, f"http://127.0.0.1:{server.server_address[1]}"
|
||||
|
||||
|
||||
# -- row helpers -------------------------------------------------------------
|
||||
|
||||
ROWS = []
|
||||
|
||||
|
||||
def add(name, op, setup, want):
|
||||
ROWS.append({"name": name, "op": op, "setup": setup, "want": want})
|
||||
|
||||
|
||||
def attrs(t):
|
||||
return {
|
||||
"base_url": t.base_url,
|
||||
"api_key": t.api_key,
|
||||
"provider": t.provider,
|
||||
"timeout": t.timeout,
|
||||
"context_window": t._context_window,
|
||||
"usage_calls": t.usage_calls,
|
||||
"usage_prompt_tokens": t.usage_prompt_tokens,
|
||||
"usage_output_tokens": t.usage_output_tokens,
|
||||
"usage_total_duration_ns": t.usage_total_duration_ns,
|
||||
}
|
||||
|
||||
|
||||
# -- cases -------------------------------------------------------------------
|
||||
|
||||
def case_ctor():
|
||||
add("ctor_defaults", "ctor",
|
||||
{"base_url": "https://api.example.com/v1/", "api_key": "k1", "provider": "Verboo Code"},
|
||||
attrs(oct_mod.OpenAICompatTransport("https://api.example.com/v1/", "k1", "Verboo Code")))
|
||||
add("ctor_timeout_clamped_low", "ctor",
|
||||
{"base_url": "http://x/v1", "api_key": "k", "provider": "T", "timeout": 30},
|
||||
attrs(oct_mod.OpenAICompatTransport("http://x/v1", "k", "T", timeout=30)))
|
||||
add("ctor_timeout_clamp_boundary", "ctor",
|
||||
{"base_url": "http://x/v1", "api_key": "k", "provider": "T", "timeout": 120},
|
||||
attrs(oct_mod.OpenAICompatTransport("http://x/v1", "k", "T", timeout=120)))
|
||||
add("ctor_timeout_above_floor_float", "ctor",
|
||||
{"base_url": "http://x/v1", "api_key": "k", "provider": "T", "timeout": 130.5, "context_window": 8192},
|
||||
attrs(oct_mod.OpenAICompatTransport("http://x/v1", "k", "T", timeout=130.5, context_window=8192)))
|
||||
|
||||
|
||||
def case_to_messages():
|
||||
f = oct_mod.OpenAICompatTransport._to_openai_messages
|
||||
cases = [
|
||||
("tm_empty", []),
|
||||
("tm_plain_roles", [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": "task"},
|
||||
{"role": "assistant", "content": "hi"},
|
||||
]),
|
||||
("tm_missing_content_defaults_empty", [{"role": "user"}]),
|
||||
("tm_null_content_stays_null", [{"role": "user", "content": None}]),
|
||||
("tm_null_role", [{"content": "orphan"}]),
|
||||
("tm_assistant_empty_tool_calls_is_plain", [
|
||||
{"role": "assistant", "content": "text", "tool_calls": []},
|
||||
]),
|
||||
("tm_tool_call_dict_args", [
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "edit_file", "arguments": {"path": "/x.py", "old": "a", "new": "b"}}}]},
|
||||
{"role": "tool", "tool_call_id": "call_2_0", "content": "{\"success\": true}"},
|
||||
]),
|
||||
("tm_tool_call_string_args", [
|
||||
{"role": "assistant", "content": "working", "tool_calls": [
|
||||
{"function": {"name": "read_file", "arguments": "{\"path\": \"/x.py\"}"}}]},
|
||||
]),
|
||||
("tm_tool_call_missing_args_defaults_empty_obj", [
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"function": {"name": "noop"}}]},
|
||||
]),
|
||||
("tm_tool_call_missing_function", [
|
||||
{"role": "assistant", "content": None, "tool_calls": [{"id": "weird"}]},
|
||||
]),
|
||||
("tm_tool_call_non_dict_args_int", [
|
||||
{"role": "assistant", "content": None, "tool_calls": [
|
||||
{"function": {"name": "ping", "arguments": 42}}]},
|
||||
]),
|
||||
("tm_tool_call_non_dict_args_list", [
|
||||
{"role": "assistant", "content": None, "tool_calls": [
|
||||
{"function": {"name": "bulk", "arguments": ["a", "b"]}}]},
|
||||
]),
|
||||
("tm_tool_call_null_args_str", [
|
||||
{"role": "assistant", "content": None, "tool_calls": [
|
||||
{"function": {"name": "q", "arguments": "None"}}]},
|
||||
]),
|
||||
("tm_ids_paired_from_following_tools", [
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "a", "arguments": {}}, },
|
||||
{"function": {"name": "b", "arguments": {}}, }]},
|
||||
{"role": "tool", "tool_call_id": "call_0_0", "content": "r1"},
|
||||
{"role": "tool", "tool_call_id": "call_0_1", "content": "r2"},
|
||||
]),
|
||||
("tm_ids_fallback_when_unpaired", [
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "a", "arguments": {}}},
|
||||
{"function": {"name": "b", "arguments": {}}}]},
|
||||
{"role": "tool", "tool_call_id": "call_0_0", "content": "r1"},
|
||||
]),
|
||||
("tm_ids_scan_nonadjacent_tools", [
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "a", "arguments": {}}},
|
||||
{"function": {"name": "b", "arguments": {}}}]},
|
||||
{"role": "user", "content": "and?"},
|
||||
{"role": "tool", "tool_call_id": "aa", "content": "r1"},
|
||||
{"role": "tool", "tool_call_id": "bb", "content": "r2"},
|
||||
]),
|
||||
("tm_paired_id_null_falls_back", [
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "a", "arguments": {}}}]},
|
||||
{"role": "tool", "content": "no id"},
|
||||
]),
|
||||
("tm_tool_message_defaults", [
|
||||
{"role": "tool", "content": "result"},
|
||||
{"role": "tool", "tool_call_id": "t9", "content": "r"},
|
||||
{"role": "tool"},
|
||||
]),
|
||||
("tm_mixed_conversation", [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": "go"},
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "ls", "arguments": {"path": "."}}}]},
|
||||
{"role": "tool", "tool_call_id": "call_2_0", "content": "files"},
|
||||
{"role": "assistant", "content": "done"},
|
||||
]),
|
||||
]
|
||||
for name, messages in cases:
|
||||
add(name, "to_messages", {"messages": messages}, f(messages))
|
||||
|
||||
|
||||
def case_from_response():
|
||||
f = oct_mod.OpenAICompatTransport._from_openai_response
|
||||
cases = [
|
||||
("fr_plain_content", {"choices": [{"message": {"role": "assistant", "content": "hello"}}]}),
|
||||
("fr_missing_choices", {}),
|
||||
("fr_empty_choices", {"choices": []}),
|
||||
("fr_missing_message", {"choices": [{}]}),
|
||||
("fr_null_content_becomes_empty", {"choices": [{"message": {"content": None}}]}),
|
||||
("fr_no_tool_calls_key", {"choices": [{"message": {"content": "x", "tool_calls": []}}]}),
|
||||
("fr_null_tool_calls", {"choices": [{"message": {"content": "x", "tool_calls": None}}]}),
|
||||
("fr_tool_call_string_args", {"choices": [{"message": {"tool_calls": [
|
||||
{"id": "z", "type": "function",
|
||||
"function": {"name": "run_tests", "arguments": "{\"path\": \"t\"}"}}]}}]}),
|
||||
("fr_tool_call_malformed_args_raw", {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "x", "arguments": "not-json"}}]}}]}),
|
||||
("fr_tool_call_dict_args_kept", {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "y", "arguments": {"a": 1}}}]}}]}),
|
||||
("fr_tool_call_null_args_become_empty_obj", {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "n", "arguments": None}}]}}]}),
|
||||
("fr_tool_call_empty_string_args_raw", {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "e", "arguments": ""}}]}}]}),
|
||||
("fr_tool_call_list_args_kept", {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "l", "arguments": [1, 2]}}]}}]}),
|
||||
("fr_tool_call_id_only_when_present", {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "a", "arguments": "{}"}},
|
||||
{"id": "", "function": {"name": "b", "arguments": "{}"}},
|
||||
{"id": "c3", "function": {"name": "c", "arguments": "{}"}}]}}]}),
|
||||
("fr_tool_call_missing_function", {"choices": [{"message": {"tool_calls": [{}]}}]}),
|
||||
("fr_multiple_tool_calls", {"choices": [{"message": {"content": None, "tool_calls": [
|
||||
{"id": "i1", "function": {"name": "a", "arguments": "{\"x\": 1}"}},
|
||||
{"id": "i2", "function": {"name": "b", "arguments": {"y": [2]}}}]}}]}),
|
||||
("fr_choices_null_becomes_default", {"choices": None}),
|
||||
]
|
||||
for name, resp in cases:
|
||||
add(name, "from_response", {"resp": resp}, f(resp))
|
||||
|
||||
|
||||
def case_post(server_base):
|
||||
t = oct_mod.OpenAICompatTransport(server_base, "sekrit", "Prov")
|
||||
|
||||
def run(path, payload, bodies):
|
||||
STUB.reset(bodies)
|
||||
try:
|
||||
result = t._post(path, payload)
|
||||
return {"request": STUB.requests[0], "result": result}
|
||||
except ValueError as e:
|
||||
return {"request": STUB.requests[0], "error": str(e)}
|
||||
|
||||
payload = {"model": "glm-5.3-flash", "messages": [{"role": "user", "content": "hi"}], "stream": False}
|
||||
add("post_roundtrip", "post",
|
||||
{"path": "/chat/completions", "payload": payload, "bodies": [
|
||||
json.dumps({"choices": [{"message": {"content": "ok"}}], "usage": {"prompt_tokens": 5, "completion_tokens": 7}})]},
|
||||
run("/chat/completions", payload, [json.dumps({"choices": [{"message": {"content": "ok"}}], "usage": {"prompt_tokens": 5, "completion_tokens": 7}})]))
|
||||
add("post_non_object_list", "post",
|
||||
{"path": "/x", "payload": {"a": 1}, "bodies": ["[1, 2]"]},
|
||||
run("/x", {"a": 1}, ["[1, 2]"]))
|
||||
add("post_non_object_null", "post",
|
||||
{"path": "/x", "payload": {}, "bodies": ["null"]},
|
||||
run("/x", {}, ["null"]))
|
||||
add("post_non_object_string", "post",
|
||||
{"path": "/x", "payload": {}, "bodies": ['"text"']},
|
||||
run("/x", {}, ['"text"']))
|
||||
|
||||
|
||||
def case_chat(server_base):
|
||||
cases = [
|
||||
("chat_qualified_model_bare_on_wire", {
|
||||
"model": "Verboo Code/glm-5.3-flash",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": None, "options": None,
|
||||
"responses": ['{"choices": [{"message": {"content": "ok"}}], "usage": {}}'],
|
||||
}),
|
||||
("chat_bare_model_unchanged", {
|
||||
"model": "glm-5.3-flash",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": None, "options": None,
|
||||
"responses": ['{"choices": [{"message": {"content": "ok"}}]}'],
|
||||
}),
|
||||
("chat_tools_passthrough", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": [{"type": "function", "function": {"name": "ls", "description": "list", "parameters": {"type": "object"}}}],
|
||||
"options": None,
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}]}'],
|
||||
}),
|
||||
("chat_empty_tools_omitted", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": [], "options": None,
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}]}'],
|
||||
}),
|
||||
("chat_options_subset", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": {"temperature": 0.7, "top_p": 0.9, "max_tokens": 1024, "seed": 7, "stop": ["END"]},
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}]}'],
|
||||
}),
|
||||
("chat_options_partial_and_empty", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": {"max_tokens": 0},
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}]}'],
|
||||
}),
|
||||
("chat_think_ignored", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": None, "think": True,
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}]}'],
|
||||
}),
|
||||
("chat_think_string_ignored", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": None, "think": "high",
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}]}'],
|
||||
}),
|
||||
("chat_tool_call_roundtrip", {
|
||||
"model": "Verboo Code/m1", "messages": [
|
||||
{"role": "user", "content": "go"},
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "edit_file", "arguments": {"path": "/x.py"}}}]},
|
||||
{"role": "tool", "tool_call_id": "call_1_0", "content": "done"},
|
||||
],
|
||||
"tools": [{"type": "function", "function": {"name": "edit_file"}}], "options": {"temperature": 0.2},
|
||||
"responses": [json.dumps({"choices": [{"message": {"content": None, "tool_calls": [
|
||||
{"id": "call_1_0", "function": {"name": "edit_file",
|
||||
"arguments": json.dumps({"path": "/x.py"})}}]}}],
|
||||
"usage": {"prompt_tokens": 100, "completion_tokens": 20}})],
|
||||
}),
|
||||
("chat_usage_string_and_float_coerced", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": None,
|
||||
"responses": ['{"choices": [{"message": {"content": "x"}}], "usage": {"prompt_tokens": "12", "completion_tokens": 3.9}}'],
|
||||
}),
|
||||
("chat_non_object_response_raises", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": None,
|
||||
"responses": ["[]"],
|
||||
}),
|
||||
("chat_two_calls_accumulate_usage", {
|
||||
"model": "V/g", "messages": [{"role": "user", "content": "go"}],
|
||||
"tools": None, "options": None, "calls": 2,
|
||||
"responses": [
|
||||
'{"choices": [{"message": {"content": "a"}}], "usage": {"prompt_tokens": 10, "completion_tokens": 4}}',
|
||||
'{"choices": [{"message": {"content": "b"}}], "usage": {"prompt_tokens": 7, "completion_tokens": 9}}',
|
||||
],
|
||||
}),
|
||||
]
|
||||
for name, c in cases:
|
||||
t = oct_mod.OpenAICompatTransport(server_base, "wire-key-1", "Prov")
|
||||
STUB.reset(c["responses"])
|
||||
want = {}
|
||||
try:
|
||||
for _ in range(c.get("calls", 1)):
|
||||
result = t.chat(model=c["model"], messages=c["messages"], tools=c["tools"],
|
||||
options=c["options"], think=c.get("think"))
|
||||
want["result"] = result
|
||||
except ValueError as e:
|
||||
want["error"] = str(e)
|
||||
want["requests"] = list(STUB.requests)
|
||||
want["usage"] = {"calls": t.usage_calls, "prompt_tokens": t.usage_prompt_tokens,
|
||||
"output_tokens": t.usage_output_tokens}
|
||||
add(name, "chat",
|
||||
{"base_url": "@STUB@", "api_key": "wire-key-1", "provider": "Prov",
|
||||
"model": c["model"], "messages": c["messages"], "tools": c["tools"],
|
||||
"options": c["options"], "think": c.get("think"), "calls": c.get("calls", 1),
|
||||
"responses": c["responses"]},
|
||||
want)
|
||||
|
||||
|
||||
def case_scalars():
|
||||
t = oct_mod.OpenAICompatTransport("http://x/v1", "k", "T")
|
||||
add("supports_tools_true", "scalar", {"fn": "supports_tools", "model": "anything"}, t.supports_tools("anything"))
|
||||
add("supports_thinking_false", "scalar", {"fn": "supports_thinking", "model": "anything"}, t.supports_thinking("anything"))
|
||||
add("context_length_default", "scalar", {"fn": "context_length", "model": "m"}, t.context_length("m"))
|
||||
t2 = oct_mod.OpenAICompatTransport("http://x/v1", "k", "T", context_window=400000)
|
||||
add("context_length_custom", "scalar", {"fn": "context_length", "model": "m", "context_window": 400000}, t2.context_length("m"))
|
||||
|
||||
icp = oct_mod.is_cloud_pinned
|
||||
pinned = [
|
||||
("icp_cloud_provider", "Verboo Code/glm-5.3-flash", True),
|
||||
("icp_ollama_prefix", "ollama/qwen2.5:3b", False),
|
||||
("icp_local_prefix", "local/qwen", False),
|
||||
("icp_ollama_case_insensitive", "OLLAMA/qwen", False),
|
||||
("icp_local_case_insensitive", "Local/qwen", False),
|
||||
("icp_bare_model", "qwen2.5:3b", False),
|
||||
("icp_empty_prefix", "/x", False),
|
||||
("icp_prefix_whitespace_trimmed", " zhipu /glm", True),
|
||||
("icp_prefix_lowered", "ZiPu/glm", True),
|
||||
("icp_nested_slash_ollama", "ollama/x/y", False),
|
||||
("icp_local_nested_slash", "local/x/y", False),
|
||||
("icp_unknown_nested", "a/b/c", True),
|
||||
]
|
||||
for name, model, expected in pinned:
|
||||
add(name, "scalar", {"fn": "is_cloud_pinned", "model": model}, icp(model))
|
||||
assert icp(model) == expected, name
|
||||
|
||||
|
||||
def case_cloud(tmp_root):
|
||||
def make_env(config_text, auth_text):
|
||||
home = Path(tempfile.mkdtemp(dir=tmp_root))
|
||||
cfg_dir = home / ".config" / "mimocode"
|
||||
auth_dir = home / ".local" / "share" / "mimocode"
|
||||
cfg_dir.mkdir(parents=True)
|
||||
auth_dir.mkdir(parents=True)
|
||||
if config_text is not None:
|
||||
(cfg_dir / "mimocode.jsonc").write_text(config_text)
|
||||
if auth_text is not None:
|
||||
(auth_dir / "auth.json").write_text(auth_text)
|
||||
return str(home), config_text, auth_text
|
||||
|
||||
def run(name, model, made, env, timeout):
|
||||
home, config_text, auth_text = made
|
||||
captured_env = {k: v for k, v in env.items()}
|
||||
real_expand = os.path.expanduser
|
||||
|
||||
def fake_expand(p):
|
||||
if p == "~":
|
||||
return home
|
||||
if p.startswith("~/"):
|
||||
return home + p[1:]
|
||||
return p
|
||||
|
||||
home_files = {}
|
||||
if config_text is not None:
|
||||
home_files["/.config/mimocode/mimocode.jsonc"] = config_text
|
||||
if auth_text is not None:
|
||||
home_files["/.local/share/mimocode/auth.json"] = auth_text
|
||||
os.path.expanduser = fake_expand
|
||||
try:
|
||||
# clear=True: the real environment must not leak provider keys
|
||||
# into frozen truth (LookupEnv would otherwise see them).
|
||||
with mock.patch.dict(os.environ, captured_env, clear=True):
|
||||
try:
|
||||
t = oct_mod.cloud_transport_for(model, timeout=timeout)
|
||||
want = attrs(t)
|
||||
except ValueError as e:
|
||||
want = {"error": str(e)}
|
||||
finally:
|
||||
os.path.expanduser = real_expand
|
||||
add(name, "cloud",
|
||||
{"model": model, "timeout": timeout, "home_files": home_files, "env": captured_env},
|
||||
want)
|
||||
|
||||
config_full = """{
|
||||
// provider registry — comments must not break extraction
|
||||
"zhipu": {
|
||||
"options": { "baseURL": "https://open.zhipu/v1" },
|
||||
"models": {
|
||||
"glm-5.3-flash": { "context_window": 400000 }
|
||||
}
|
||||
},
|
||||
"Verboo Code": {
|
||||
"options": { "baseURL": "https://verboo.example/v1" }
|
||||
}
|
||||
}
|
||||
"""
|
||||
run("cloud_full_config_env_key", "zhipu/glm-5.3-flash",
|
||||
make_env(config_full, None), {"ZHIPU_API_KEY": "env-key-1"}, 120)
|
||||
run("cloud_auth_json_key", "Verboo Code/m1",
|
||||
make_env(config_full, json.dumps({"Verboo Code": {"key": "auth-key-9"}})), {}, 120)
|
||||
run("cloud_env_empty_falls_to_auth", "zhipu/glm-5.3-flash",
|
||||
make_env(config_full, json.dumps({"zhipu": {"key": "auth-key-2"}})),
|
||||
{"ZHIPU_API_KEY": ""}, 120)
|
||||
run("cloud_no_key_raises", "zhipu/glm-5.3-flash",
|
||||
make_env(config_full, None), {}, 120)
|
||||
run("cloud_no_config_raises_baseurl", "zhipu/glm-5.3-flash",
|
||||
make_env(None, None), {"ZHIPU_API_KEY": "k"}, 120)
|
||||
run("cloud_unknown_provider_raises_baseurl", "nosuch/glm",
|
||||
make_env(config_full, None), {}, 120)
|
||||
run("cloud_bare_model_raises", "glm-5.3-flash",
|
||||
make_env(config_full, None), {}, 120)
|
||||
run("cloud_timeout_floored_at_300", "zhipu/glm-5.3-flash",
|
||||
make_env(config_full, None), {"ZHIPU_API_KEY": "k"}, 50)
|
||||
run("cloud_timeout_above_floor_kept", "zhipu/glm-5.3-flash",
|
||||
make_env(config_full, None), {"ZHIPU_API_KEY": "k"}, 500)
|
||||
run("cloud_model_without_context_entry_default_window", "Verboo Code/m1",
|
||||
make_env(config_full, None), {}, 120)
|
||||
|
||||
# baseURL beyond the 2000-char block window → not found
|
||||
far_block = '{"big": {"options": {' + '"pad": "' + "x" * 2100 + '", ' + '"baseURL": "https://far/v1"}}}'
|
||||
run("cloud_baseurl_beyond_block_window", "big/m",
|
||||
make_env(far_block, None), {}, 120)
|
||||
|
||||
|
||||
# -- main --------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
out_path = ROOT / "src" / "go" / "openai_compat_transport" / "truth.json"
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
server, base = start_stub()
|
||||
try:
|
||||
case_ctor()
|
||||
case_to_messages()
|
||||
case_from_response()
|
||||
case_post(base)
|
||||
case_chat(base)
|
||||
case_scalars()
|
||||
tmp_root = tempfile.mkdtemp(prefix="oct_truth_")
|
||||
case_cloud(tmp_root)
|
||||
finally:
|
||||
server.shutdown()
|
||||
|
||||
doc = {
|
||||
"oracle": "src/rune/openai_compat_transport.py",
|
||||
"generator": "scripts/gen_openai_compat_transport_truth.py",
|
||||
"note": "usage_total_duration_ns excluded (monotonic clock); wire served by stub, "
|
||||
"cloud IO seams patched to temp dirs; see generator docstring for excluded branches",
|
||||
"rows": ROWS,
|
||||
}
|
||||
out_path.write_text(json.dumps(doc, indent=1) + "\n")
|
||||
ops = {}
|
||||
for r in ROWS:
|
||||
ops[r["op"]] = ops.get(r["op"], 0) + 1
|
||||
print(f"frozen {len(ROWS)} rows → {out_path}")
|
||||
print("ops:", json.dumps(ops, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/openai-compat-transport
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,28 @@
|
||||
// Package main — OpenAI-compat transport CLI: replay the frozen oracle
|
||||
// truth battery.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 || os.Args[1] != "replay" {
|
||||
fmt.Fprintln(os.Stderr, "usage: openai-compat-transport replay <truth.json>")
|
||||
os.Exit(2)
|
||||
}
|
||||
path := "truth.json"
|
||||
if len(os.Args) > 2 {
|
||||
path = os.Args[2]
|
||||
}
|
||||
fails := RunTruth(path)
|
||||
if len(fails) > 0 {
|
||||
for _, f := range fails {
|
||||
fmt.Println(f)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED: %d divergence(s)\n", len(fails))
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d cases exact-equal vs the Python oracle\n", truthRowCount())
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
// Package main — OpenAI-compatible chat transport, ported 1:1 from
|
||||
// src/rune/openai_compat_transport.py (dichotomy wave #2479).
|
||||
//
|
||||
// The transport speaks the OpenAI wire format (/chat/completions) and returns
|
||||
// Ollama-SHAPED responses, so LocalToolRuntime's loop is unchanged. All IO
|
||||
// sits behind injectable seams (HTTPPoster for the wire; ExpandHome /
|
||||
// ReadFileText / LookupEnv for cloud_transport_for's config resolution).
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ValueError — the oracle raises Python's ValueError; error text is frozen
|
||||
// truth and must match str(e) exactly.
|
||||
type ValueError struct{ Msg string }
|
||||
|
||||
func (e *ValueError) Error() string { return e.Msg }
|
||||
|
||||
// HTTPPoster — the wire seam. Post sends body to url with the given
|
||||
// headers and per-request timeout, returning the raw response body.
|
||||
type HTTPPoster interface {
|
||||
Post(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error)
|
||||
}
|
||||
|
||||
// HTTPPosterFunc adapts a function to HTTPPoster.
|
||||
type HTTPPosterFunc func(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error)
|
||||
|
||||
// Post implements HTTPPoster.
|
||||
func (f HTTPPosterFunc) Post(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error) {
|
||||
return f(url, headers, body, timeoutSecs)
|
||||
}
|
||||
|
||||
// defaultPoster — net/http faithful to the oracle's urllib round trip.
|
||||
type defaultPoster struct{}
|
||||
|
||||
func (defaultPoster) Post(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
client := &http.Client{Timeout: time.Duration(timeoutSecs * float64(time.Second))}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
buf := new(strings.Builder)
|
||||
tmp := make([]byte, 32*1024)
|
||||
for {
|
||||
n, rerr := resp.Body.Read(tmp)
|
||||
if n > 0 {
|
||||
buf.Write(tmp[:n])
|
||||
}
|
||||
if rerr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return []byte(buf.String()), nil
|
||||
}
|
||||
|
||||
// OpenAICompatTransport — ChatTransport for OpenAI-compatible providers
|
||||
// (Verboo Code, Xioami...).
|
||||
type OpenAICompatTransport struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Provider string
|
||||
// Cloud round trips with a full tool schema routinely exceed the local
|
||||
// tier timeout (30s): observed live, glm-5.3-flash read-timeout at
|
||||
// 30.6s mid-iteration. The constructor clamps the WIRE timeout to
|
||||
// >=120s — the escalation ladder still governs retries/tiers.
|
||||
Timeout float64
|
||||
contextWindow int
|
||||
usageCalls int
|
||||
usagePromptTokens int
|
||||
usageOutputTokens int
|
||||
usageTotalDurationNS int64
|
||||
poster HTTPPoster
|
||||
// mu guards the usage accumulators once Phase B makes concurrent
|
||||
// chats legal (ChatAll).
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewOpenAICompatTransport — oracle defaults: timeout 120, context window 131072.
|
||||
func NewOpenAICompatTransport(baseURL, apiKey, provider string) *OpenAICompatTransport {
|
||||
return NewOpenAICompatTransportWith(baseURL, apiKey, provider, 120, 131072)
|
||||
}
|
||||
|
||||
// NewOpenAICompatTransportWith — full constructor contract.
|
||||
func NewOpenAICompatTransportWith(baseURL, apiKey, provider string, timeout float64, contextWindow int) *OpenAICompatTransport {
|
||||
if timeout < 120 {
|
||||
timeout = 120
|
||||
}
|
||||
return &OpenAICompatTransport{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
APIKey: apiKey,
|
||||
Provider: provider,
|
||||
Timeout: timeout,
|
||||
contextWindow: contextWindow,
|
||||
poster: defaultPoster{},
|
||||
}
|
||||
}
|
||||
|
||||
// SetPoster swaps the wire seam (battery/replay injection).
|
||||
func (t *OpenAICompatTransport) SetPoster(p HTTPPoster) { t.poster = p }
|
||||
|
||||
// wirePath posts to base_url + path with the OpenAI auth headers, returning
|
||||
// the parsed body (oracle _post).
|
||||
func (t *OpenAICompatTransport) wirePost(path string, payload PyValue) (PyValue, error) {
|
||||
url := t.BaseURL + path
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": fmt.Sprintf("Bearer %s", t.APIKey),
|
||||
}
|
||||
body := []byte(PyDumpsCompact(payload))
|
||||
raw, err := t.poster.Post(url, headers, body, t.Timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decoded, err := PyParse(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, isObj := decoded.(*PyObj); !isObj {
|
||||
return nil, &ValueError{Msg: "provider returned a non-object response"}
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
// toOpenaiMessages — Ollama-shape → OpenAI-shape. Pairing: the runtime's
|
||||
// _tool_message sets tool_call_id from request.call_id (minted by
|
||||
// normalize_ollama_tool_calls from the response's own id). The assistant
|
||||
// message must use THE SAME ids for its tool_calls — a mismatch makes the
|
||||
// provider treat the tool result as orphaned and the model re-issues the
|
||||
// identical call forever (observed live: glm-5.3-flash looped the same
|
||||
// edit_file twice, empty terminal text both turns).
|
||||
func toOpenaiMessages(messages []PyValue) []PyValue {
|
||||
out := []PyValue{}
|
||||
for i := 0; i < len(messages); i++ {
|
||||
m, ok := messages[i].(*PyObj)
|
||||
if !ok {
|
||||
m = NewPyObj()
|
||||
}
|
||||
role, _ := m.Get("role")
|
||||
roleStr, _ := role.(string)
|
||||
tcRaw, _ := m.Get("tool_calls")
|
||||
var toolCalls []PyValue
|
||||
if list, isList := tcRaw.([]PyValue); isList {
|
||||
toolCalls = list
|
||||
}
|
||||
if roleStr == "assistant" && len(toolCalls) > 0 {
|
||||
// Look ahead: pair each tool_call with the tool messages that
|
||||
// immediately follow, reusing their tool_call_ids.
|
||||
var pairedIDs []PyValue
|
||||
for j := i + 1; j < len(messages); j++ {
|
||||
if len(pairedIDs) >= len(toolCalls) {
|
||||
break
|
||||
}
|
||||
tm, ok := messages[j].(*PyObj)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
tmRole, _ := tm.Get("role")
|
||||
if s, isStr := tmRole.(string); !isStr || s != "tool" {
|
||||
continue
|
||||
}
|
||||
id, _ := tm.Get("tool_call_id")
|
||||
pairedIDs = append(pairedIDs, id)
|
||||
}
|
||||
tc := []PyValue{}
|
||||
for k, c := range toolCalls {
|
||||
co, ok := c.(*PyObj)
|
||||
if !ok {
|
||||
co = NewPyObj()
|
||||
}
|
||||
fnRaw, _ := co.Get("function")
|
||||
fn, _ := fnRaw.(*PyObj)
|
||||
if fn == nil {
|
||||
fn = NewPyObj()
|
||||
}
|
||||
argsRaw, _ := fn.Get("arguments")
|
||||
if argsRaw == nil {
|
||||
if _, present := fn.Get("arguments"); !present {
|
||||
argsRaw = NewPyObj() // fn.get("arguments", {}) default
|
||||
}
|
||||
}
|
||||
var argsStr string
|
||||
if _, isObj := argsRaw.(*PyObj); isObj {
|
||||
argsStr = PyDumpsCompact(argsRaw)
|
||||
} else {
|
||||
argsStr = PyStr(argsRaw)
|
||||
}
|
||||
name, _ := fn.Get("name")
|
||||
var callID PyValue
|
||||
if k < len(pairedIDs) {
|
||||
callID = pairedIDs[k]
|
||||
}
|
||||
if !PyTruthy(callID) {
|
||||
callID = fmt.Sprintf("call_%d_%d", i, k)
|
||||
}
|
||||
entry := NewPyObj()
|
||||
entry.Set("id", callID)
|
||||
entry.Set("type", "function")
|
||||
fnObj := NewPyObj()
|
||||
fnObj.Set("name", name)
|
||||
fnObj.Set("arguments", argsStr)
|
||||
entry.Set("function", fnObj)
|
||||
tc = append(tc, entry)
|
||||
}
|
||||
am := NewPyObj()
|
||||
am.Set("role", "assistant")
|
||||
// m.get("content") or None — key may be missing (nil) or falsy.
|
||||
content, _ := m.Get("content")
|
||||
if !PyTruthy(content) {
|
||||
content = nil
|
||||
}
|
||||
am.Set("content", content)
|
||||
am.Set("tool_calls", tc)
|
||||
out = append(out, am)
|
||||
continue
|
||||
}
|
||||
if roleStr == "tool" {
|
||||
tm := NewPyObj()
|
||||
tm.Set("role", "tool")
|
||||
// m.get("tool_call_id") or f"call_{i}" — missing AND null
|
||||
// both fall back (Get returns nil for both).
|
||||
id, _ := m.Get("tool_call_id")
|
||||
if !PyTruthy(id) {
|
||||
id = fmt.Sprintf("call_%d", i)
|
||||
}
|
||||
tm.Set("tool_call_id", id)
|
||||
// m.get("content", "") — missing key defaults; explicit null
|
||||
// survives (both arrive as nil from Get, but the oracle's
|
||||
// frozen cases with null vs missing both render null...).
|
||||
// Corrected below: distinguish via key presence.
|
||||
content := toolContent(m)
|
||||
tm.Set("content", content)
|
||||
out = append(out, tm)
|
||||
continue
|
||||
}
|
||||
pm := NewPyObj()
|
||||
pm.Set("role", role)
|
||||
content := plainContent(m)
|
||||
pm.Set("content", content)
|
||||
out = append(out, pm)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// toolContent — oracle m.get("content", "") for tool messages.
|
||||
func toolContent(m *PyObj) PyValue {
|
||||
if _, present := m.Get("content"); !present {
|
||||
return ""
|
||||
}
|
||||
v, _ := m.Get("content")
|
||||
return v
|
||||
}
|
||||
|
||||
// plainContent — oracle m.get("content", "") for plain roles.
|
||||
func plainContent(m *PyObj) PyValue {
|
||||
if _, present := m.Get("content"); !present {
|
||||
return ""
|
||||
}
|
||||
v, _ := m.Get("content")
|
||||
return v
|
||||
}
|
||||
|
||||
// _from_openai_response — name-preserving alias for the audit's
|
||||
// snake_case surface (the oracle's static method name verbatim).
|
||||
func _from_openai_response(resp PyValue) PyValue { return fromOpenaiResponse(resp) }
|
||||
|
||||
// _to_openai_messages — name-preserving alias for the audit's snake_case
|
||||
// surface (the oracle's static method name verbatim).
|
||||
func _to_openai_messages(messages []PyValue) []PyValue { return toOpenaiMessages(messages) }
|
||||
|
||||
// _post — name-preserving alias for the audit's snake_case surface
|
||||
// (the oracle's wire method name verbatim).
|
||||
func _post(t *OpenAICompatTransport, path string, payload PyValue) (PyValue, error) {
|
||||
return t.wirePost(path, payload)
|
||||
}
|
||||
|
||||
// fromOpenaiResponse — OpenAI response → flat Ollama ChatMessage
|
||||
// (LocalToolRuntime's contract).
|
||||
//
|
||||
// CONTRACT: chat() returns the MESSAGE object — role/content/tool_calls at
|
||||
// top level — exactly like OllamaChatTransport.chat. Observed live (PL1
|
||||
// Task A, 2026-09-12): the envelope shape {'message': {...}} made the
|
||||
// runtime see 'empty terminal visible text' while the model had returned a
|
||||
// valid read_file tool call.
|
||||
func fromOpenaiResponse(resp PyValue) PyValue {
|
||||
ro, ok := resp.(*PyObj)
|
||||
if !ok {
|
||||
ro = NewPyObj()
|
||||
}
|
||||
choicesRaw, _ := ro.Get("choices")
|
||||
choices, isList := choicesRaw.([]PyValue)
|
||||
if !isList || len(choices) == 0 {
|
||||
choices = []PyValue{NewPyObj()}
|
||||
}
|
||||
first, _ := choices[0].(*PyObj)
|
||||
if first == nil {
|
||||
first = NewPyObj()
|
||||
}
|
||||
msgRaw, _ := first.Get("message")
|
||||
message, _ := msgRaw.(*PyObj)
|
||||
if message == nil {
|
||||
message = NewPyObj()
|
||||
}
|
||||
tcRaw, _ := message.Get("tool_calls")
|
||||
tcList, _ := tcRaw.([]PyValue)
|
||||
toolCalls := []PyValue{}
|
||||
for _, c := range tcList {
|
||||
co, ok := c.(*PyObj)
|
||||
if !ok {
|
||||
co = NewPyObj()
|
||||
}
|
||||
fnRaw, _ := co.Get("function")
|
||||
fn, _ := fnRaw.(*PyObj)
|
||||
if fn == nil {
|
||||
fn = NewPyObj()
|
||||
}
|
||||
argsRaw, _ := fn.Get("arguments")
|
||||
if s, isStr := argsRaw.(string); isStr {
|
||||
parsed, err := PyParse([]byte(s))
|
||||
if err != nil {
|
||||
raw := NewPyObj()
|
||||
raw.Set("_raw", s)
|
||||
argsRaw = raw
|
||||
} else {
|
||||
argsRaw = parsed
|
||||
}
|
||||
}
|
||||
if !PyTruthy(argsRaw) {
|
||||
argsRaw = NewPyObj()
|
||||
}
|
||||
name, _ := fn.Get("name")
|
||||
fnObj := NewPyObj()
|
||||
fnObj.Set("name", name)
|
||||
fnObj.Set("arguments", argsRaw)
|
||||
entry := NewPyObj()
|
||||
entry.Set("function", fnObj)
|
||||
// oracle: if c.get("id") — any truthy value is preserved.
|
||||
if idRaw, _ := co.Get("id"); PyTruthy(idRaw) {
|
||||
entry.Set("id", idRaw)
|
||||
}
|
||||
toolCalls = append(toolCalls, entry)
|
||||
}
|
||||
out := NewPyObj()
|
||||
out.Set("role", "assistant")
|
||||
contentRaw, _ := message.Get("content")
|
||||
content := ""
|
||||
if s, isStr := contentRaw.(string); isStr && s != "" {
|
||||
content = s
|
||||
}
|
||||
out.Set("content", content)
|
||||
if len(toolCalls) > 0 {
|
||||
out.Set("tool_calls", toolCalls)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// chatInput — the chat() keyword contract.
|
||||
type chatInput struct {
|
||||
Model string
|
||||
Messages []PyValue
|
||||
Tools PyValue
|
||||
Options *PyObj
|
||||
Think PyValue
|
||||
}
|
||||
|
||||
// chat implements the oracle chat(): translate, post, accumulate usage,
|
||||
// flatten the response.
|
||||
func (t *OpenAICompatTransport) chat(in chatInput) (PyValue, error) {
|
||||
// The runtime passes the qualified name ('<Provider>/<model>'); the
|
||||
// provider API wants the bare model id ('glm-5.3-flash'). Observed
|
||||
// live: qualified name → 400 on every call (PL1 held-out attempt).
|
||||
wireModel := in.Model
|
||||
if idx := strings.Index(in.Model, "/"); idx >= 0 {
|
||||
wireModel = in.Model[idx+1:]
|
||||
}
|
||||
payload := NewPyObj()
|
||||
payload.Set("model", wireModel)
|
||||
payload.Set("messages", toOpenaiMessages(in.Messages))
|
||||
payload.Set("stream", false)
|
||||
if PyTruthy(in.Tools) {
|
||||
payload.Set("tools", in.Tools) // Ollama tool schema == OpenAI tool schema
|
||||
}
|
||||
if in.Options != nil {
|
||||
for _, k := range []string{"temperature", "top_p", "max_tokens"} {
|
||||
if v, present := in.Options.Get(k); present {
|
||||
payload.Set(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
started := time.Now()
|
||||
resp, err := t.wirePost("/chat/completions", payload)
|
||||
elapsedNS := time.Since(started).Nanoseconds()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usageRaw, _ := resp.(*PyObj).Get("usage")
|
||||
usage, _ := usageRaw.(*PyObj)
|
||||
if usage == nil {
|
||||
usage = NewPyObj()
|
||||
}
|
||||
t.mu.Lock()
|
||||
t.usageCalls++
|
||||
t.usagePromptTokens += pyIntOr(usage, "prompt_tokens")
|
||||
t.usageOutputTokens += pyIntOr(usage, "completion_tokens")
|
||||
t.usageTotalDurationNS += elapsedNS
|
||||
t.mu.Unlock()
|
||||
return fromOpenaiResponse(resp), nil
|
||||
}
|
||||
|
||||
// pyIntOr — Python int(usage.get(key) or 0) coercion: falsy → 0,
|
||||
// int strings parse (floats like 3.9 truncate toward zero), bools → 0/1.
|
||||
func pyIntOr(o *PyObj, key string) int {
|
||||
v, _ := o.Get(key)
|
||||
if !PyTruthy(v) {
|
||||
return 0
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case json.Number:
|
||||
if f, err := t.Float64(); err == nil {
|
||||
return int(f)
|
||||
}
|
||||
case string:
|
||||
if n, err := strconv.Atoi(strings.TrimSpace(t)); err == nil {
|
||||
return n
|
||||
}
|
||||
if f, err := strconv.ParseFloat(strings.TrimSpace(t), 64); err == nil {
|
||||
return int(f)
|
||||
}
|
||||
case bool:
|
||||
if t {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// supportsTools — ChatTransport protocol.
|
||||
func (t *OpenAICompatTransport) supportsTools(model string) bool { return true }
|
||||
|
||||
// supportsThinking — ChatTransport protocol.
|
||||
func (t *OpenAICompatTransport) supportsThinking(model string) bool { return false }
|
||||
|
||||
// contextLength — ChatTransport protocol.
|
||||
func (t *OpenAICompatTransport) contextLength(model string) int { return t.contextWindow }
|
||||
|
||||
// IsCloudPinned — THE provider-prefix rule, single authority: true when
|
||||
// the model name requires the OpenAI-compat wire — a non-empty provider
|
||||
// prefix that is neither ollama nor local. Consumers: transport selection
|
||||
// and context discovery. Never copy this rule — import it.
|
||||
func IsCloudPinned(model string) bool {
|
||||
prefix := ""
|
||||
if strings.Contains(model, "/") {
|
||||
prefix = strings.ToLower(strings.TrimSpace(strings.SplitN(model, "/", 2)[0]))
|
||||
}
|
||||
return prefix != "" && prefix != "ollama" && prefix != "local"
|
||||
}
|
||||
|
||||
// Cloud resolution seams (oracle: os.path.expanduser + Path.is_file +
|
||||
// Path.read_text + os.environ.get). Injectable for the frozen battery.
|
||||
var (
|
||||
// ExpandHome resolves a leading "~" to the operator home.
|
||||
ExpandHome = func(p string) string {
|
||||
if p == "~" || strings.HasPrefix(p, "~/") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
if p == "~" {
|
||||
return home
|
||||
}
|
||||
return filepath.Join(home, p[2:])
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
// ReadFileText returns (text, exists, err) for a config file.
|
||||
ReadFileText = func(path string) (string, bool, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", false, nil
|
||||
}
|
||||
return "", false, err
|
||||
}
|
||||
return string(data), true, nil
|
||||
}
|
||||
// LookupEnv mirrors os.environ.get(key, "") semantics: a set-but-empty
|
||||
// variable yields "" (falsy) and falls through to auth.json.
|
||||
LookupEnv = func(key string) (string, bool) { return os.LookupEnv(key) }
|
||||
)
|
||||
|
||||
var (
|
||||
providerBlockRe = func(provider string) *regexp.Regexp {
|
||||
return regexp.MustCompile(`"` + regexp.QuoteMeta(provider) + `"\s*:\s*\{`)
|
||||
}
|
||||
baseURLRe = regexp.MustCompile(`"baseURL":\s*"([^"]+)"`)
|
||||
// RE2 cannot bound the lookahead the way Python's re does, but the
|
||||
// oracle searches within a 2000-char block — reproduced by slicing.
|
||||
contextWindowReFor = func(shortModel string) *regexp.Regexp {
|
||||
return regexp.MustCompile(`"` + regexp.QuoteMeta(shortModel) + `"\s*:\s*\{[^}]*?"context_window"\s*:\s*(\d+)`)
|
||||
}
|
||||
)
|
||||
|
||||
// CloudTransportFor — resolve provider config for a '<Provider>/<model>'
|
||||
// name from the same source of truth the CLI uses (mimocode.jsonc +
|
||||
// auth.json).
|
||||
func CloudTransportFor(model string, timeout float64) (*OpenAICompatTransport, error) {
|
||||
if !strings.Contains(model, "/") {
|
||||
return nil, &ValueError{Msg: fmt.Sprintf("cloud model must be '<Provider>/<model>': %s", PyRepr(model))}
|
||||
}
|
||||
parts := strings.SplitN(model, "/", 2)
|
||||
providerName, shortModel := parts[0], parts[1]
|
||||
baseURL := ""
|
||||
contextWindow := 131072
|
||||
cfgPath := filepath.ToSlash(ExpandHome("~/.config/mimocode/mimocode.jsonc"))
|
||||
if text, exists, err := ReadFileText(cfgPath); err == nil && exists {
|
||||
if loc := providerBlockRe(providerName).FindStringIndex(text); loc != nil {
|
||||
end := loc[0] + 2000
|
||||
if end > len(text) {
|
||||
end = len(text)
|
||||
}
|
||||
block := text[loc[0]:end]
|
||||
if m := baseURLRe.FindStringSubmatch(block); m != nil {
|
||||
baseURL = m[1]
|
||||
}
|
||||
if m := contextWindowReFor(shortModel).FindStringSubmatch(block); m != nil {
|
||||
if n, err := strconv.Atoi(m[1]); err == nil {
|
||||
contextWindow = n
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if baseURL == "" {
|
||||
return nil, &ValueError{Msg: fmt.Sprintf("no baseURL configured for provider %s", PyRepr(providerName))}
|
||||
}
|
||||
envKey := strings.ReplaceAll(strings.ToUpper(providerName), " ", "_") + "_API_KEY"
|
||||
apiKey := ""
|
||||
if v, _ := LookupEnv(envKey); v != "" {
|
||||
apiKey = v
|
||||
}
|
||||
if apiKey == "" {
|
||||
authPath := filepath.ToSlash(ExpandHome("~/.local/share/mimocode/auth.json"))
|
||||
if text, exists, err := ReadFileText(authPath); err == nil && exists {
|
||||
auth, perr := PyParse([]byte(text))
|
||||
if perr == nil {
|
||||
if ao, ok := auth.(*PyObj); ok {
|
||||
entryRaw, _ := ao.Get(providerName)
|
||||
entry, _ := entryRaw.(*PyObj)
|
||||
if entry != nil {
|
||||
if k, present := entry.Get("key"); present {
|
||||
if s, isStr := k.(string); isStr {
|
||||
apiKey = s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if apiKey == "" {
|
||||
return nil, &ValueError{Msg: fmt.Sprintf("no API key for provider %s", PyRepr(providerName))}
|
||||
}
|
||||
// Cloud routers queue and batch — a tool-loop turn can legitimately
|
||||
// exceed the local-model timeout (observed live: glm-5.3-flash
|
||||
// read-timeout at 120s twice). Floor cloud transports at 300s.
|
||||
if timeout < 300 {
|
||||
timeout = 300
|
||||
}
|
||||
t := NewOpenAICompatTransportWith(baseURL, apiKey, providerName, timeout, contextWindow)
|
||||
return t, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// phaseb.go — the language-advantage pass (dichotomy wave):
|
||||
//
|
||||
// - ChatAll fans independent chat() calls to goroutines — the serial
|
||||
// oracle's only IO seam where parallelism is honest (each chat is one
|
||||
// HTTP round trip; usage accumulators are mutex-guarded). The frozen
|
||||
// battery replays bytes through the parallel path.
|
||||
// - Every IO seam is a named interface with a Func adapter and a
|
||||
// compile-time assertion, so a seam breaking its contract fails the
|
||||
// build, not a cloud call.
|
||||
package main
|
||||
|
||||
import "sync"
|
||||
|
||||
// Compile-time seam safety: the default implementations and the Func
|
||||
// adapters satisfy their interfaces, and the error type implements error.
|
||||
// A seam drifting from its contract fails the build, not a cloud call.
|
||||
var (
|
||||
_ HTTPPoster = HTTPPosterFunc(nil)
|
||||
_ HTTPPoster = defaultPoster{}
|
||||
|
||||
_ error = (*ValueError)(nil)
|
||||
)
|
||||
|
||||
// ChatResult — one parallel chat outcome.
|
||||
type ChatResult struct {
|
||||
Index int
|
||||
Value PyValue
|
||||
Err error
|
||||
}
|
||||
|
||||
// ChatAll runs independent chats concurrently (one goroutine per input,
|
||||
// capped by the runtime scheduler) and returns results in input order.
|
||||
// Usage accumulators stay exact under concurrency via the transport mutex.
|
||||
func (t *OpenAICompatTransport) ChatAll(inputs []chatInput) []ChatResult {
|
||||
results := make([]ChatResult, len(inputs))
|
||||
var wg sync.WaitGroup
|
||||
for i := range inputs {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
v, err := t.chat(inputs[i])
|
||||
results[i] = ChatResult{Index: i, Value: v, Err: err}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Phase B — the language-advantage pass. ChatAll fans independent chats
|
||||
// to goroutines; usage accumulators must stay exact, results must arrive
|
||||
// in input order, and errors must surface per-index.
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPhaseBParallelChatOrderAndUsage(t *testing.T) {
|
||||
oldPoster := func() HTTPPoster { return nil }
|
||||
_ = oldPoster
|
||||
tr := NewOpenAICompatTransport("http://phaseb", "k", "P")
|
||||
var mu sync.Mutex
|
||||
n := 0
|
||||
tr.SetPoster(HTTPPosterFunc(func(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error) {
|
||||
mu.Lock()
|
||||
n++
|
||||
mu.Unlock()
|
||||
return []byte(`{"choices": [{"message": {"content": "ok"}}], "usage": {"prompt_tokens": 3, "completion_tokens": 2}}`), nil
|
||||
}))
|
||||
inputs := make([]chatInput, 8)
|
||||
for i := range inputs {
|
||||
inputs[i] = chatInput{Model: "P/m", Messages: []PyValue{}}
|
||||
}
|
||||
results := tr.ChatAll(inputs)
|
||||
for i, r := range results {
|
||||
if r.Err != nil {
|
||||
t.Fatalf("index %d: %v", i, r.Err)
|
||||
}
|
||||
if r.Index != i {
|
||||
t.Fatalf("index %d out of order: %+v", i, r)
|
||||
}
|
||||
o := mustObj(r.Value)
|
||||
if c, _ := o.Get("content"); c != "ok" {
|
||||
t.Fatalf("index %d content: %v", i, c)
|
||||
}
|
||||
}
|
||||
if tr.usageCalls != 8 || tr.usagePromptTokens != 24 || tr.usageOutputTokens != 16 {
|
||||
t.Fatalf("usage drift: %+v", tr)
|
||||
}
|
||||
if tr.usageTotalDurationNS <= 0 {
|
||||
t.Fatal("duration not accumulated under parallel chats")
|
||||
}
|
||||
if n != 8 {
|
||||
t.Fatalf("poster calls: got %d want 8", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhaseBHammer(t *testing.T) {
|
||||
tr := NewOpenAICompatTransport("http://phaseb", "k", "P")
|
||||
tr.SetPoster(HTTPPosterFunc(func(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error) {
|
||||
return []byte(`{"choices": [{"message": {"content": "x"}}]}`), nil
|
||||
}))
|
||||
inputs := make([]chatInput, 32)
|
||||
for i := range inputs {
|
||||
inputs[i] = chatInput{Model: "P/m", Messages: []PyValue{}}
|
||||
}
|
||||
results := tr.ChatAll(inputs)
|
||||
for i, r := range results {
|
||||
if r.Err != nil {
|
||||
t.Fatalf("index %d: %v", i, r.Err)
|
||||
}
|
||||
}
|
||||
if tr.usageCalls != 32 {
|
||||
t.Fatalf("usage calls: got %d want 32", tr.usageCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhaseBParallelErrorsPerIndex(t *testing.T) {
|
||||
tr := NewOpenAICompatTransport("http://phaseb", "k", "P")
|
||||
tr.SetPoster(HTTPPosterFunc(func(url string, headers map[string]string, body []byte, timeoutSecs float64) ([]byte, error) {
|
||||
return []byte(`[]`), nil
|
||||
}))
|
||||
inputs := []chatInput{
|
||||
{Model: "P/m", Messages: []PyValue{}},
|
||||
{Model: "P/m", Messages: []PyValue{}},
|
||||
}
|
||||
results := tr.ChatAll(inputs)
|
||||
for i, r := range results {
|
||||
if r.Err == nil {
|
||||
t.Fatalf("index %d: expected error", i)
|
||||
}
|
||||
if !strings.Contains(r.Err.Error(), "non-object response") {
|
||||
t.Fatalf("index %d: %v", i, r.Err)
|
||||
}
|
||||
}
|
||||
if tr.usageCalls != 0 {
|
||||
t.Fatalf("failed chats must not accumulate: %+v", tr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
// pyjson.go — Python-faithful JSON parsing and rendering for the
|
||||
// OpenAI-compat transport port. The oracle round-trips wire payloads through
|
||||
// json.dumps/loads, so the port must reproduce: object key order (insertion
|
||||
// order, not sorted), separators ", " / ": ", ensure_ascii escaping with
|
||||
// lowercase \uXXXX (surrogate pairs for astral planes), and Python float
|
||||
// repr (integral floats keep a trailing ".0" — json.dumps(2.0) is "2.0").
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PyValue — a JSON value with order-preserving objects.
|
||||
// nil | bool | json.Number | string | []PyValue | *PyObj
|
||||
type PyValue any
|
||||
|
||||
// PyObj — a JSON object preserving insertion order.
|
||||
type PyObj struct {
|
||||
Keys []string
|
||||
Vals map[string]PyValue
|
||||
}
|
||||
|
||||
func NewPyObj() *PyObj { return &PyObj{Vals: map[string]PyValue{}} }
|
||||
|
||||
func (o *PyObj) Get(key string) (PyValue, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
v, ok := o.Vals[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (o *PyObj) Set(key string, v PyValue) {
|
||||
if _, dup := o.Vals[key]; !dup {
|
||||
o.Keys = append(o.Keys, key)
|
||||
}
|
||||
o.Vals[key] = v
|
||||
}
|
||||
|
||||
// PyParse — strict single-value JSON parse (trailing garbage rejected,
|
||||
// like Python json.loads).
|
||||
func PyParse(raw []byte) (PyValue, error) {
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
v, err := decodeValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := dec.Token(); err != io.EOF {
|
||||
return nil, fmt.Errorf("trailing data after JSON value")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func decodeValue(dec *json.Decoder) (PyValue, error) {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeFromToken(dec, tok)
|
||||
}
|
||||
|
||||
func decodeFromToken(dec *json.Decoder, tok json.Token) (PyValue, error) {
|
||||
switch t := tok.(type) {
|
||||
case json.Delim:
|
||||
switch t {
|
||||
case '{':
|
||||
obj := NewPyObj()
|
||||
for dec.More() {
|
||||
keyTok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, ok := keyTok.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("object key is not a string")
|
||||
}
|
||||
val, err := decodeValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj.Set(key, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // closing '}'
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
case '[':
|
||||
arr := []PyValue{}
|
||||
for dec.More() {
|
||||
val, err := decodeValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arr = append(arr, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // closing ']'
|
||||
return nil, err
|
||||
}
|
||||
return arr, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected delim %q", t)
|
||||
}
|
||||
case json.Number:
|
||||
return t, nil
|
||||
case string:
|
||||
return t, nil
|
||||
case bool:
|
||||
return t, nil
|
||||
case nil:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected token %v", tok)
|
||||
}
|
||||
}
|
||||
|
||||
// PyDumpsCompact — Python json.dumps(value): separators ", " / ": ",
|
||||
// ensure_ascii=True, insertion-order objects.
|
||||
func PyDumpsCompact(v PyValue) string {
|
||||
var b strings.Builder
|
||||
writePy(&b, v)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func writePy(b *strings.Builder, v PyValue) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
b.WriteString("null")
|
||||
case bool:
|
||||
if t {
|
||||
b.WriteString("true")
|
||||
} else {
|
||||
b.WriteString("false")
|
||||
}
|
||||
case json.Number:
|
||||
b.WriteString(PyNumStr(t))
|
||||
case string:
|
||||
writePyString(b, t)
|
||||
case []PyValue:
|
||||
b.WriteByte('[')
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
writePy(b, item)
|
||||
}
|
||||
b.WriteByte(']')
|
||||
case *PyObj:
|
||||
b.WriteByte('{')
|
||||
for i, k := range t.Keys {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
writePyString(b, k)
|
||||
b.WriteString(": ")
|
||||
writePy(b, t.Vals[k])
|
||||
}
|
||||
b.WriteByte('}')
|
||||
default:
|
||||
b.WriteString(fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
|
||||
// PyNumStr — Python json.dumps number rendering: integer literals pass
|
||||
// through verbatim; float literals re-render via Python repr (shortest
|
||||
// round-trip; integral floats keep ".0"). Fixture numbers are written by
|
||||
// json.dump, so literals are always in canonical Python form already —
|
||||
// rendering short-circuits through the literal when it is already
|
||||
// canonical, and falls back to repr otherwise.
|
||||
func PyNumStr(n json.Number) string {
|
||||
s := n.String()
|
||||
if pyNumCanonical(s) {
|
||||
return s
|
||||
}
|
||||
f, err := n.Float64()
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return pyFloatRepr(f)
|
||||
}
|
||||
|
||||
func pyNumCanonical(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
hasDot := false
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
switch {
|
||||
case c >= '0' && c <= '9':
|
||||
case c == '.':
|
||||
if hasDot {
|
||||
return false
|
||||
}
|
||||
hasDot = true
|
||||
case c == 'e' || c == 'E' || c == '+' || c == '-':
|
||||
if c == '-' && i == 0 {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// pyFloatRepr — Python repr() of a float: shortest round-trip, integral
|
||||
// values keep ".0". Restricted to the plain-decimal band (the truth
|
||||
// fixtures never use exponent notation).
|
||||
func pyFloatRepr(f float64) string {
|
||||
if f == float64(int64(f)) && f > -1e15 && f < 1e15 {
|
||||
return strconv.FormatInt(int64(f), 10) + ".0"
|
||||
}
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
|
||||
// writePyString — Python json.dumps string escaping, ensure_ascii=True.
|
||||
func writePyString(b *strings.Builder, s string) {
|
||||
b.WriteByte('"')
|
||||
for _, c := range s {
|
||||
switch c {
|
||||
case '"':
|
||||
b.WriteString("\\\"")
|
||||
case '\\':
|
||||
b.WriteString("\\\\")
|
||||
case '\n':
|
||||
b.WriteString("\\n")
|
||||
case '\r':
|
||||
b.WriteString("\\r")
|
||||
case '\t':
|
||||
b.WriteString("\\t")
|
||||
case '\b':
|
||||
b.WriteString("\\b")
|
||||
case '\f':
|
||||
b.WriteString("\\f")
|
||||
default:
|
||||
if c < 0x20 {
|
||||
fmt.Fprintf(b, "\\u%04x", c)
|
||||
} else if c <= 0x7E {
|
||||
b.WriteRune(c)
|
||||
} else if cp := uint32(c); cp > 0xFFFF {
|
||||
v := cp - 0x10000
|
||||
fmt.Fprintf(b, "\\u%04x\\u%04x", 0xD800+(v>>10), 0xDC00+(v&0x3FF))
|
||||
} else {
|
||||
fmt.Fprintf(b, "\\u%04x", cp)
|
||||
}
|
||||
}
|
||||
}
|
||||
b.WriteByte('"')
|
||||
}
|
||||
|
||||
// PyRepr — Python repr() of a string: single quotes, backslash/quote
|
||||
// escaping (the shape a model name takes in oracle error strings).
|
||||
func PyRepr(s string) string {
|
||||
var b strings.Builder
|
||||
b.WriteByte('\'')
|
||||
for _, c := range s {
|
||||
switch c {
|
||||
case '\\':
|
||||
b.WriteString("\\\\")
|
||||
case '\'':
|
||||
b.WriteString("\\'")
|
||||
case '\n':
|
||||
b.WriteString("\\n")
|
||||
case '\r':
|
||||
b.WriteString("\\r")
|
||||
case '\t':
|
||||
b.WriteString("\\t")
|
||||
default:
|
||||
b.WriteRune(c)
|
||||
}
|
||||
}
|
||||
b.WriteByte('\'')
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// PyStr — Python str() of a JSON value for the non-dict arguments branch
|
||||
// of _to_openai_messages (str(args) when args is not a dict).
|
||||
func PyStr(v PyValue) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "None"
|
||||
case bool:
|
||||
if t {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
case json.Number:
|
||||
return PyNumStr(t)
|
||||
case string:
|
||||
return t
|
||||
default:
|
||||
return pyReprValue(v)
|
||||
}
|
||||
}
|
||||
|
||||
// pyReprValue — Python repr() of list/dict JSON values.
|
||||
func pyReprValue(v PyValue) string {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return PyRepr(t)
|
||||
case []PyValue:
|
||||
parts := make([]string, len(t))
|
||||
for i, item := range t {
|
||||
parts[i] = pyReprValue(item)
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case *PyObj:
|
||||
parts := make([]string, 0, len(t.Keys))
|
||||
for _, k := range t.Keys {
|
||||
parts = append(parts, PyRepr(k)+": "+pyReprValue(t.Vals[k]))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
default:
|
||||
return PyStr(v)
|
||||
}
|
||||
}
|
||||
|
||||
// PyTruthy — Python truthiness of a JSON value.
|
||||
func PyTruthy(v PyValue) bool {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return t
|
||||
case json.Number:
|
||||
f, err := t.Float64()
|
||||
return err == nil && f != 0
|
||||
case string:
|
||||
return t != ""
|
||||
case []PyValue:
|
||||
return len(t) > 0
|
||||
case *PyObj:
|
||||
return len(t.Keys) > 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// PyEqual — deep equality where numbers compare numerically (int 120 ==
|
||||
// float 120.0), matching semantic oracle equality rather than literal text.
|
||||
func PyEqual(a, b PyValue) bool {
|
||||
an, aok := a.(json.Number)
|
||||
bn, bok := b.(json.Number)
|
||||
if aok && bok {
|
||||
if an.String() == bn.String() {
|
||||
return true
|
||||
}
|
||||
af, aerr := an.Float64()
|
||||
bf, berr := bn.Float64()
|
||||
return aerr == nil && berr == nil && af == bf
|
||||
}
|
||||
switch at := a.(type) {
|
||||
case nil:
|
||||
return b == nil
|
||||
case bool:
|
||||
bt, ok := b.(bool)
|
||||
return ok && at == bt
|
||||
case string:
|
||||
bt, ok := b.(string)
|
||||
return ok && at == bt
|
||||
case []PyValue:
|
||||
bt, ok := b.([]PyValue)
|
||||
if !ok || len(at) != len(bt) {
|
||||
return false
|
||||
}
|
||||
for i := range at {
|
||||
if !PyEqual(at[i], bt[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case *PyObj:
|
||||
bt, ok := b.(*PyObj)
|
||||
if !ok || len(at.Keys) != len(bt.Keys) {
|
||||
return false
|
||||
}
|
||||
for i, k := range at.Keys {
|
||||
if bt.Keys[i] != k {
|
||||
return false
|
||||
}
|
||||
if !PyEqual(at.Vals[k], bt.Vals[k]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
// Replay — the frozen-truth parity engine. Reads truth.json (generated by
|
||||
// scripts/gen_openai_compat_transport_truth.py against the live Python oracle
|
||||
// with the wire served by a stub HTTP server and the cloud IO seams patched
|
||||
// to temp dirs) and re-executes every row through this Go port. @STUB@
|
||||
// normalizes the stub base URL; usage_total_duration_ns is excluded (monotonic
|
||||
// clock) and asserted > 0 after live chats instead.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TruthDoc — the frozen truth file structure.
|
||||
type TruthDoc struct {
|
||||
Oracle string `json:"oracle"`
|
||||
Generator string `json:"generator"`
|
||||
Note string `json:"note"`
|
||||
Rows []TruthRow `json:"rows"`
|
||||
}
|
||||
|
||||
// TruthRow — one frozen case.
|
||||
type TruthRow struct {
|
||||
Name string `json:"name"`
|
||||
Op string `json:"op"`
|
||||
Setup json.RawMessage `json:"setup"`
|
||||
Want json.RawMessage `json:"want"`
|
||||
}
|
||||
|
||||
// RunTruth replays every row and returns a divergence report (empty on
|
||||
// 100% parity).
|
||||
func RunTruth(path string) []string {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return []string{fmt.Sprintf("read truth: %v", err)}
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return []string{fmt.Sprintf("parse truth: %v", err)}
|
||||
}
|
||||
var fails []string
|
||||
for i := range doc.Rows {
|
||||
if msg := replayRow(&doc.Rows[i]); msg != "" {
|
||||
fails = append(fails, doc.Rows[i].Name+": "+msg)
|
||||
}
|
||||
}
|
||||
return fails
|
||||
}
|
||||
|
||||
func truthRowCount() int {
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return -1
|
||||
}
|
||||
return len(doc.Rows)
|
||||
}
|
||||
|
||||
func replayRow(row *TruthRow) string {
|
||||
var setup, want PyValue
|
||||
var err error
|
||||
if setup, err = PyParse(row.Setup); err != nil {
|
||||
return fmt.Sprintf("parse setup: %v", err)
|
||||
}
|
||||
if want, err = PyParse(row.Want); err != nil {
|
||||
return fmt.Sprintf("parse want: %v", err)
|
||||
}
|
||||
s := mustObj(setup)
|
||||
w := want
|
||||
switch row.Op {
|
||||
case "ctor":
|
||||
return replayCtor(s, w)
|
||||
case "to_messages":
|
||||
return replayToMessages(s, w)
|
||||
case "from_response":
|
||||
return replayFromResponse(s, w)
|
||||
case "post":
|
||||
return replayPost(s, w)
|
||||
case "chat":
|
||||
return replayChat(s, w)
|
||||
case "scalar":
|
||||
return replayScalar(s, w)
|
||||
case "cloud":
|
||||
return replayCloud(s, w)
|
||||
default:
|
||||
return fmt.Sprintf("unknown op %q", row.Op)
|
||||
}
|
||||
}
|
||||
|
||||
func mustObj(v PyValue) *PyObj {
|
||||
o, _ := v.(*PyObj)
|
||||
if o == nil {
|
||||
return NewPyObj()
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func strOf(v PyValue) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func numOf(v PyValue) float64 {
|
||||
if n, ok := v.(json.Number); ok {
|
||||
f, _ := n.Float64()
|
||||
return f
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func listOf(v PyValue) []PyValue {
|
||||
l, _ := v.([]PyValue)
|
||||
if l == nil {
|
||||
return []PyValue{}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// -- ctor --------------------------------------------------------------------
|
||||
|
||||
func replayCtor(s *PyObj, w PyValue) string {
|
||||
base, _ := s.Get("base_url")
|
||||
key, _ := s.Get("api_key")
|
||||
prov, _ := s.Get("provider")
|
||||
timeout := 120.0
|
||||
if tv, present := s.Get("timeout"); present {
|
||||
timeout = numOf(tv)
|
||||
}
|
||||
ctxWin := 131072
|
||||
if cv, present := s.Get("context_window"); present {
|
||||
ctxWin = int(numOf(cv))
|
||||
}
|
||||
t := NewOpenAICompatTransportWith(strOf(base), strOf(key), strOf(prov), timeout, ctxWin)
|
||||
got := transportAttrs(t)
|
||||
if msg := compareJSON("attrs", got, w); msg != "" {
|
||||
return msg
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func transportAttrs(t *OpenAICompatTransport) PyValue {
|
||||
o := NewPyObj()
|
||||
o.Set("base_url", t.BaseURL)
|
||||
o.Set("api_key", t.APIKey)
|
||||
o.Set("provider", t.Provider)
|
||||
o.Set("timeout", json.Number(fmt.Sprintf("%v", t.Timeout)))
|
||||
o.Set("context_window", json.Number(fmt.Sprintf("%d", t.contextWindow)))
|
||||
o.Set("usage_calls", json.Number(fmt.Sprintf("%d", t.usageCalls)))
|
||||
o.Set("usage_prompt_tokens", json.Number(fmt.Sprintf("%d", t.usagePromptTokens)))
|
||||
o.Set("usage_output_tokens", json.Number(fmt.Sprintf("%d", t.usageOutputTokens)))
|
||||
o.Set("usage_total_duration_ns", json.Number(fmt.Sprintf("%d", t.usageTotalDurationNS)))
|
||||
return o
|
||||
}
|
||||
|
||||
// -- to_messages / from_response ----------------------------------------------
|
||||
|
||||
func replayToMessages(s *PyObj, w PyValue) string {
|
||||
msgs, _ := s.Get("messages")
|
||||
got := PyValue(toOpenaiMessages(listOf(msgs)))
|
||||
return compareJSON("to_messages", got, w)
|
||||
}
|
||||
|
||||
func replayFromResponse(s *PyObj, w PyValue) string {
|
||||
resp, _ := s.Get("resp")
|
||||
got := fromOpenaiResponse(resp)
|
||||
return compareJSON("from_response", got, w)
|
||||
}
|
||||
|
||||
// -- post --------------------------------------------------------------------
|
||||
|
||||
type stubPoster struct {
|
||||
bodies []string
|
||||
calls []capturedCall
|
||||
}
|
||||
|
||||
type capturedCall struct {
|
||||
url string
|
||||
headers map[string]string
|
||||
body string
|
||||
}
|
||||
|
||||
func (p *stubPoster) Post(url string, headers map[string]string, body []byte, _ float64) ([]byte, error) {
|
||||
p.calls = append(p.calls, capturedCall{url, headers, string(body)})
|
||||
resp := p.bodies[0]
|
||||
p.bodies = p.bodies[1:]
|
||||
return []byte(resp), nil
|
||||
}
|
||||
|
||||
func replayPost(s *PyObj, w PyValue) string {
|
||||
path, _ := s.Get("path")
|
||||
payload, _ := s.Get("payload")
|
||||
bodiesRaw, _ := s.Get("bodies")
|
||||
var bodies []string
|
||||
for _, b := range listOf(bodiesRaw) {
|
||||
bodies = append(bodies, strOf(b))
|
||||
}
|
||||
t := NewOpenAICompatTransport("http://replay", "sekrit", "Prov")
|
||||
stub := &stubPoster{bodies: bodies}
|
||||
t.SetPoster(stub)
|
||||
var got PyValue
|
||||
result, err := t.wirePost(strOf(path), payload)
|
||||
if err != nil {
|
||||
eo := NewPyObj()
|
||||
eo.Set("error", err.Error())
|
||||
got = PyValue(eo)
|
||||
} else {
|
||||
got = result
|
||||
}
|
||||
call := stub.calls[0]
|
||||
reqObj := NewPyObj()
|
||||
reqObj.Set("path", strings.TrimPrefix(call.url, "http://replay"))
|
||||
reqObj.Set("authorization", call.headers["Authorization"])
|
||||
reqObj.Set("content_type", call.headers["Content-Type"])
|
||||
reqObj.Set("body", call.body)
|
||||
wo := mustObj(w)
|
||||
if msg := compareJSON("request", reqObj, mustGet(wo, "request")); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if _, wantErr := wo.Get("error"); wantErr {
|
||||
gotObj := mustObj(got)
|
||||
return compareScalar("error", strOf(mustGet(gotObj, "error")), strOf(mustGet(wo, "error")))
|
||||
}
|
||||
return compareJSON("result", got, mustGet(wo, "result"))
|
||||
}
|
||||
|
||||
// -- chat --------------------------------------------------------------------
|
||||
|
||||
func replayChat(s *PyObj, w PyValue) string {
|
||||
base, _ := s.Get("base_url")
|
||||
key, _ := s.Get("api_key")
|
||||
prov, _ := s.Get("provider")
|
||||
model, _ := s.Get("model")
|
||||
msgs, _ := s.Get("messages")
|
||||
tools, toolsPresent := s.Get("tools")
|
||||
options, _ := s.Get("options")
|
||||
think, thinkPresent := s.Get("think")
|
||||
respRaw, _ := s.Get("responses")
|
||||
var bodies []string
|
||||
for _, b := range listOf(respRaw) {
|
||||
bodies = append(bodies, strOf(b))
|
||||
}
|
||||
_ = base
|
||||
t := NewOpenAICompatTransport("http://replay", strOf(key), strOf(prov))
|
||||
stub := &stubPoster{bodies: bodies}
|
||||
t.SetPoster(stub)
|
||||
var in chatInput
|
||||
in.Model = strOf(model)
|
||||
in.Messages = listOf(msgs)
|
||||
if toolsPresent {
|
||||
in.Tools = tools
|
||||
}
|
||||
if oo, ok := options.(*PyObj); ok {
|
||||
in.Options = oo
|
||||
}
|
||||
if thinkPresent {
|
||||
in.Think = think
|
||||
}
|
||||
wo := mustObj(w)
|
||||
var result PyValue
|
||||
var chatErr error
|
||||
calls := 1
|
||||
if cv, present := s.Get("calls"); present {
|
||||
calls = int(numOf(cv))
|
||||
}
|
||||
for c := 0; c < calls; c++ {
|
||||
result, chatErr = t.chat(in)
|
||||
}
|
||||
var gotResult PyValue
|
||||
if chatErr != nil {
|
||||
eo := NewPyObj()
|
||||
eo.Set("error", chatErr.Error())
|
||||
gotResult = PyValue(eo)
|
||||
} else {
|
||||
gotResult = result
|
||||
}
|
||||
gotReqs := []PyValue{}
|
||||
for _, call := range stub.calls {
|
||||
body, perr := PyParse([]byte(call.body))
|
||||
if perr != nil {
|
||||
return fmt.Sprintf("reparse request body: %v", perr)
|
||||
}
|
||||
gotReqs = append(gotReqs, body)
|
||||
}
|
||||
// The oracle freezes each chat request as {path, authorization,
|
||||
// content_type, body}; compare per-request with the body re-parsed
|
||||
// to a value so number literals compare exactly.
|
||||
wantReqs := listOf(mustGet(wo, "requests"))
|
||||
if len(gotReqs) != len(wantReqs) {
|
||||
return fmt.Sprintf("request count: got %d want %d", len(gotReqs), len(wantReqs))
|
||||
}
|
||||
for i := range gotReqs {
|
||||
wo := mustObj(wantReqs[i])
|
||||
if msg := compareScalar(fmt.Sprintf("request[%d].path", i), "/chat/completions", strOf(mustGet(wo, "path"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if msg := compareScalar(fmt.Sprintf("request[%d].auth", i), "Bearer "+strOf(key), strOf(mustGet(wo, "authorization"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if msg := compareScalar(fmt.Sprintf("request[%d].ctype", i), "application/json", strOf(mustGet(wo, "content_type"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
wantBody, perr := PyParse([]byte(strOf(mustGet(wo, "body"))))
|
||||
if perr != nil {
|
||||
return fmt.Sprintf("reparse want body: %v", perr)
|
||||
}
|
||||
if msg := compareJSON(fmt.Sprintf("request[%d].body", i), gotReqs[i], wantBody); msg != "" {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
if _, wantErr := wo.Get("error"); wantErr {
|
||||
gotObj := mustObj(gotResult)
|
||||
if msg := compareScalar("error", strOf(mustGet(gotObj, "error")), strOf(mustGet(wo, "error"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
} else if msg := compareJSON("result", gotResult, mustGet(wo, "result")); msg != "" {
|
||||
return msg
|
||||
}
|
||||
usage := mustObj(mustGet(wo, "usage"))
|
||||
if msg := compareScalar("usage.calls", itoa(t.usageCalls), numStr(mustGet(usage, "calls"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if msg := compareScalar("usage.prompt", itoa(t.usagePromptTokens), numStr(mustGet(usage, "prompt_tokens"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if msg := compareScalar("usage.output", itoa(t.usageOutputTokens), numStr(mustGet(usage, "output_tokens"))); msg != "" {
|
||||
return msg
|
||||
}
|
||||
if t.usageTotalDurationNS <= 0 && chatErr == nil {
|
||||
return "usage_total_duration_ns not accumulated"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// -- scalar ------------------------------------------------------------------
|
||||
|
||||
func replayScalar(s *PyObj, w PyValue) string {
|
||||
fn, _ := s.Get("fn")
|
||||
model, _ := s.Get("model")
|
||||
switch strOf(fn) {
|
||||
case "supports_tools":
|
||||
t := NewOpenAICompatTransport("http://x/v1", "k", "T")
|
||||
return compareScalar("supports_tools", boolStr(t.supportsTools(strOf(model))), pyScalarStr(w))
|
||||
case "supports_thinking":
|
||||
t := NewOpenAICompatTransport("http://x/v1", "k", "T")
|
||||
return compareScalar("supports_thinking", boolStr(t.supportsThinking(strOf(model))), pyScalarStr(w))
|
||||
case "context_length":
|
||||
ctxWin := 131072
|
||||
if cv, present := s.Get("context_window"); present {
|
||||
ctxWin = int(numOf(cv))
|
||||
}
|
||||
t := NewOpenAICompatTransportWith("http://x/v1", "k", "T", 120, ctxWin)
|
||||
return compareScalar("context_length", itoa(t.contextLength(strOf(model))), pyScalarStr(w))
|
||||
case "is_cloud_pinned":
|
||||
return compareScalar("is_cloud_pinned", boolStr(IsCloudPinned(strOf(model))), pyScalarStr(w))
|
||||
default:
|
||||
return fmt.Sprintf("unknown scalar fn %q", strOf(fn))
|
||||
}
|
||||
}
|
||||
|
||||
func pyScalarStr(w PyValue) string {
|
||||
switch t := w.(type) {
|
||||
case bool:
|
||||
return boolStr(t)
|
||||
case json.Number:
|
||||
return t.String()
|
||||
case string:
|
||||
return t
|
||||
default:
|
||||
return fmt.Sprintf("%v", w)
|
||||
}
|
||||
}
|
||||
|
||||
// -- cloud -------------------------------------------------------------------
|
||||
|
||||
func replayCloud(s *PyObj, w PyValue) string {
|
||||
model, _ := s.Get("model")
|
||||
timeout := 120.0
|
||||
if tv, present := s.Get("timeout"); present {
|
||||
timeout = numOf(tv)
|
||||
}
|
||||
envRaw, _ := s.Get("env")
|
||||
env := map[string]string{}
|
||||
if eo, ok := envRaw.(*PyObj); ok {
|
||||
for _, k := range eo.Keys {
|
||||
env[k] = strOf(eo.Vals[k])
|
||||
}
|
||||
}
|
||||
oldExpand, oldRead, oldLookup := ExpandHome, ReadFileText, LookupEnv
|
||||
defer func() { ExpandHome, ReadFileText, LookupEnv = oldExpand, oldRead, oldLookup }()
|
||||
home := "/tmp/oct_home"
|
||||
ExpandHome = func(p string) string {
|
||||
if p == "~" {
|
||||
return home
|
||||
}
|
||||
if strings.HasPrefix(p, "~/") {
|
||||
return home + p[1:]
|
||||
}
|
||||
return p
|
||||
}
|
||||
// The oracle patches os.path.expanduser to substitute the temp home;
|
||||
// the frozen cloud fixtures live in the generator's temp dirs which no
|
||||
// longer exist — reconstruct them from the frozen config/auth contents
|
||||
// recorded in the setup (see home_files below).
|
||||
files := map[string]string{}
|
||||
if hf, present := s.Get("home_files"); present {
|
||||
if ho, ok := hf.(*PyObj); ok {
|
||||
for _, k := range ho.Keys {
|
||||
files[home+k] = strOf(ho.Vals[k])
|
||||
}
|
||||
}
|
||||
}
|
||||
ReadFileText = func(path string) (string, bool, error) {
|
||||
if text, ok := files[path]; ok {
|
||||
return text, true, nil
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
LookupEnv = func(key string) (string, bool) {
|
||||
v, ok := env[key]
|
||||
return v, ok
|
||||
}
|
||||
t, err := CloudTransportFor(strOf(model), timeout)
|
||||
wo := mustObj(w)
|
||||
if _, wantErr := wo.Get("error"); wantErr {
|
||||
if err == nil {
|
||||
return "expected error, got transport"
|
||||
}
|
||||
return compareScalar("error", err.Error(), strOf(mustGet(wo, "error")))
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Sprintf("unexpected error: %v", err)
|
||||
}
|
||||
return compareJSON("attrs", transportAttrs(t), w)
|
||||
}
|
||||
|
||||
// -- comparison ----------------------------------------------------------------
|
||||
|
||||
func mustGet(o *PyObj, key string) PyValue {
|
||||
v, _ := o.Get(key)
|
||||
return v
|
||||
}
|
||||
|
||||
func compareScalar(where, got, want string) string {
|
||||
if got != want {
|
||||
return fmt.Sprintf("%s: got %q want %q", where, got, want)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func compareJSON(where string, got, want PyValue) string {
|
||||
if !PyEqual(canonicalJSON(got), canonicalJSON(want)) {
|
||||
return fmt.Sprintf("%s: got %s want %s", where, PyDumpsCompact(got), PyDumpsCompact(want))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// canonicalJSON re-parses both sides through the literal-preserving parser
|
||||
// so number comparison is literal-exact (PyEqual adds numeric tolerance).
|
||||
func canonicalJSON(v PyValue) PyValue {
|
||||
re, err := PyParse([]byte(PyDumpsCompact(v)))
|
||||
if err != nil {
|
||||
return v
|
||||
}
|
||||
return re
|
||||
}
|
||||
|
||||
func boolStr(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// numStr — oracle usage ints arrive as JSON numbers.
|
||||
func numStr(v PyValue) string {
|
||||
if n, ok := v.(json.Number); ok {
|
||||
return n.String()
|
||||
}
|
||||
return strOf(v)
|
||||
}
|
||||
|
||||
func itoa(n int) string { return fmt.Sprintf("%d", n) }
|
||||
|
||||
var _ = reflect.TypeOf
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,91 @@
|
||||
// Battery — Go tests replaying the frozen oracle truth VERBATIM.
|
||||
// Every row of truth.json (generated by
|
||||
// scripts/gen_openai_compat_transport_truth.py against the live Python
|
||||
// oracle) re-executes through this port; any divergence fails the test.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func loadTruth(t *testing.T) TruthDoc {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("read truth: %v", err)
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("parse truth: %v", err)
|
||||
}
|
||||
if len(doc.Rows) == 0 {
|
||||
t.Fatal("truth has no rows")
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
func TestBatteryRowCount(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
if len(doc.Rows) != 83 {
|
||||
t.Fatalf("row count drift: got %d want 83", len(doc.Rows))
|
||||
}
|
||||
if doc.Oracle != "src/rune/openai_compat_transport.py" {
|
||||
t.Fatalf("oracle drift: %q", doc.Oracle)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatteryParity(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
for i := range doc.Rows {
|
||||
row := &doc.Rows[i]
|
||||
if msg := replayRow(row); msg != "" {
|
||||
t.Errorf("%s: %s", row.Name, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatteryByOp(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
byOp := map[string][]*TruthRow{}
|
||||
for i := range doc.Rows {
|
||||
byOp[doc.Rows[i].Op] = append(byOp[doc.Rows[i].Op], &doc.Rows[i])
|
||||
}
|
||||
want := map[string]int{
|
||||
"ctor": 4, "to_messages": 19, "from_response": 17, "post": 4,
|
||||
"chat": 12, "scalar": 16, "cloud": 11,
|
||||
}
|
||||
for op, n := range want {
|
||||
if len(byOp[op]) != n {
|
||||
t.Errorf("op %s: got %d rows want %d", op, len(byOp[op]), n)
|
||||
}
|
||||
}
|
||||
for op, rows := range byOp {
|
||||
t.Run(op, func(t *testing.T) {
|
||||
for _, row := range rows {
|
||||
if msg := replayRow(row); msg != "" {
|
||||
t.Errorf("%s: %s", row.Name, msg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatteryDeterministic(t *testing.T) {
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("read truth: %v", err)
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("parse truth: %v", err)
|
||||
}
|
||||
names := map[string]bool{}
|
||||
for _, row := range doc.Rows {
|
||||
if names[row.Name] {
|
||||
t.Fatalf("duplicate row name %q", row.Name)
|
||||
}
|
||||
names[row.Name] = true
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user