fix: complete streamed patch repair responses
This commit is contained in:
@@ -67,6 +67,68 @@ def generation_max_tokens(role):
|
||||
return min(max(configured, 64), 8192)
|
||||
|
||||
|
||||
def sse_text(value):
|
||||
"""Return text from OpenAI- or Anthropic-shaped SSE content values."""
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, list):
|
||||
return "".join(sse_text(item.get("text") or item.get("content") or "") for item in value if isinstance(item, dict))
|
||||
return ""
|
||||
|
||||
|
||||
def decode_provider_sse(raw, provider):
|
||||
"""Collapse an OpenAI-compatible server-sent-event response into a chat payload.
|
||||
|
||||
OmniRoute may stream even where the caller requested a non-streaming chat
|
||||
completion. This decoder accepts only JSON `data:` events, keeps no raw
|
||||
response content in diagnostics, and preserves provider usage when the
|
||||
gateway supplies it.
|
||||
"""
|
||||
text_parts, usage, event_count = [], None, 0
|
||||
for line in raw.decode("utf-8", "replace").splitlines():
|
||||
if not line.startswith("data:"):
|
||||
continue
|
||||
data = line[5:].strip()
|
||||
if not data or data == "[DONE]":
|
||||
continue
|
||||
try:
|
||||
event = json.loads(data)
|
||||
except json.JSONDecodeError as exc:
|
||||
fail(f"provider_sse_invalid {provider} {type(exc).__name__} bytes={len(raw)}")
|
||||
if not isinstance(event, dict):
|
||||
fail(f"provider_sse_invalid {provider} event_type={type(event).__name__}")
|
||||
if event.get("error"):
|
||||
fail(f"provider_sse_error {provider}")
|
||||
event_count += 1
|
||||
if isinstance(event.get("usage"), dict):
|
||||
usage = event["usage"]
|
||||
for choice in event.get("choices", []):
|
||||
if not isinstance(choice, dict):
|
||||
continue
|
||||
delta = choice.get("delta") if isinstance(choice.get("delta"), dict) else {}
|
||||
message = choice.get("message") if isinstance(choice.get("message"), dict) else {}
|
||||
piece = sse_text(delta.get("content") or delta.get("text") or message.get("content") or event.get("output_text") or "")
|
||||
if piece:
|
||||
text_parts.append(piece)
|
||||
# Some compatible gateways forward Anthropic's event shape directly.
|
||||
delta = event.get("delta") if isinstance(event.get("delta"), dict) else {}
|
||||
if not event.get("choices"):
|
||||
piece = sse_text(delta.get("text") or delta.get("content") or event.get("output_text") or "")
|
||||
if piece:
|
||||
text_parts.append(piece)
|
||||
text = "".join(text_parts).strip()
|
||||
if event_count == 0 or not text:
|
||||
fail(f"provider_sse_incomplete {provider} events={event_count} bytes={len(raw)}")
|
||||
payload = {"choices": [{"message": {"content": text}}]}
|
||||
if usage is not None:
|
||||
payload["usage"] = usage
|
||||
else:
|
||||
# A missing usage event is explicit, not invented. It permits the
|
||||
# governed patch gate to complete while telemetry records it as unknown.
|
||||
payload["_casan_stream_usage_missing"] = True
|
||||
return payload
|
||||
|
||||
|
||||
def decode_provider_json(response, provider):
|
||||
"""Decode a provider response without mislabelling an empty 2xx body.
|
||||
|
||||
@@ -86,6 +148,8 @@ def decode_provider_json(response, provider):
|
||||
content_type = ""
|
||||
if not raw:
|
||||
fail(f"provider_response_empty {provider} content_type={content_type or 'unknown'}")
|
||||
if content_type == "text/event-stream":
|
||||
return decode_provider_sse(raw, provider)
|
||||
try:
|
||||
payload = json.loads(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
@@ -367,6 +431,9 @@ def call_openai_compatible(model_name, prompt, role):
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"temperature": 0 if role in ("classify", "judge") else 0.2,
|
||||
"max_tokens": generation_max_tokens(role),
|
||||
# Explicitly request a regular JSON completion. Some gateways still
|
||||
# return SSE; decode_provider_json handles that response safely.
|
||||
"stream": False,
|
||||
}
|
||||
data = json.dumps(body).encode()
|
||||
req = urllib.request.Request(
|
||||
@@ -386,15 +453,19 @@ def call_openai_compatible(model_name, prompt, role):
|
||||
"input_tokens": input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
"latency_ms": latency_ms,
|
||||
"usage_available": not bool(payload.get("_casan_stream_usage_missing")),
|
||||
}
|
||||
|
||||
|
||||
def parse_openai_payload(payload):
|
||||
try:
|
||||
text = (payload["choices"][0]["message"]["content"] or "").strip()
|
||||
usage = payload["usage"]
|
||||
input_tokens = int(usage["prompt_tokens"])
|
||||
output_tokens = int(usage["completion_tokens"])
|
||||
usage = payload.get("usage")
|
||||
if usage is None and payload.get("_casan_stream_usage_missing"):
|
||||
input_tokens, output_tokens = 0, 0
|
||||
else:
|
||||
input_tokens = int(usage["prompt_tokens"])
|
||||
output_tokens = int(usage["completion_tokens"])
|
||||
except (KeyError, IndexError, TypeError, ValueError) as exc:
|
||||
fail(f"provider_usage_invalid openai {type(exc).__name__}: {str(exc)[:80]}")
|
||||
return text, input_tokens, output_tokens
|
||||
@@ -515,6 +586,7 @@ def main():
|
||||
"total_tokens": total,
|
||||
"latency_ms": result["latency_ms"],
|
||||
"temperature": 0 if args.role in ("classify", "judge") else 0.2,
|
||||
"usage_available": bool(result.get("usage_available", True)),
|
||||
}
|
||||
if verdict is not None:
|
||||
out["verdict"] = verdict
|
||||
@@ -532,6 +604,8 @@ def main():
|
||||
"anthropic": "anthropic_api_real_tokens",
|
||||
"openai-compatible": "openai_compatible_api_real_tokens",
|
||||
}.get(backend, f"{backend}_real_tokens")
|
||||
if not result.get("usage_available", True):
|
||||
cost_source = f"{backend}_usage_unavailable"
|
||||
os.makedirs(os.path.dirname(PROVIDER_LOG), exist_ok=True)
|
||||
usage = {
|
||||
"timestamp": ts, "harness": "L5-provider-telemetry", "provider": backend,
|
||||
@@ -539,7 +613,7 @@ def main():
|
||||
"step": os.environ.get("CASAN_STEP_NAME", args.role), "role": args.role,
|
||||
"input_tokens": result["input_tokens"], "output_tokens": result["output_tokens"],
|
||||
"total_tokens": total, "cost_usd": 0.0, "cost_source": cost_source,
|
||||
"latency_ms": result["latency_ms"], "status": "success",
|
||||
"latency_ms": result["latency_ms"], "usage_available": bool(result.get("usage_available", True)), "status": "success",
|
||||
}
|
||||
open(PROVIDER_LOG, "a", encoding="utf-8").write(json.dumps(usage) + "\n")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user