Native API: distinguish provider finish_reason from inferred EOF completion (follow-up to #3669)
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 45/100
- Loại issue
- Tính năng
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- go
- Lĩnh vực
- backend-api-design
Hướng nghiên cứu
Start by tracing the native API stream completion path and the OpenAI-compatible SSE response handling that sets finish_reason and processes [DONE] or EOF. Compare the explicit-stop, done-only, bare-EOF, and truncated-body cases from the reproduction. Done means the emitted completion or diagnostic preserves whether the provider supplied the finish reason and distinguishes missing terminal signals; the issue names no source files or tests.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Description
The native API currently reports stream_stopped {reason: "normal", finish_reason: "stop"} both when an OpenAI-compatible provider explicitly sends finish_reason: "stop" and when a complete HTTP response ends with neither a non-null finish_reason nor [DONE]. The emitted completion does not identify whether stop was observed from the provider or inferred at EOF.
This is a follow-up to #3669 and #3668. I understand that stopping a content-only bare-EOF turn was an intentional compatibility change to avoid invoking the model repeatedly with unchanged history. This report does not request reverting that stop decision or restoring automatic continuation/retry loops. It asks for distinguishable termination provenance/diagnostics, and a separate review of the no-finish_reason/no-[DONE] case versus the [DONE]-only gateway compatibility case.
Expected Behavior
- Preserve the anti-loop behavior from #3669 where required.
- Expose whether the provider actually supplied the reported finish reason or the runtime inferred it. An optional provenance/observed flag or a diagnostic event would be sufficient; specific field names are left to maintainers.
- Distinguish a
[DONE]-only response from one that has neither terminal signal. Please consider an explicit incomplete/unknown outcome or diagnostic for the latter instead of making it indistinguishable from a provider-confirmedstop. - Do not infer semantic task completion from prose or add automatic model re-entry as a workaround.
Actual Behavior and Verification Results
Fresh runs of the self-contained fixture below produced identical results on the unmodified official Windows amd64 and Linux amd64 v1.150.0 binaries:
| Case | Synthetic upstream response | Native reason | Native finish_reason | Native error |
|---|---|---|---|---|
explicit_stop |
Text + explicit stop + [DONE], no usage |
normal |
stop |
none |
done_only |
Text + [DONE], no non-null finish reason |
normal |
stop |
none |
bare_eof |
Text only; no finish reason and no [DONE]; complete HTTP body |
normal |
stop |
none |
transport_cut |
Same text, but HTTP Content-Length deliberately exceeds bytes sent | error |
absent | model_error |
The first three cases each made one fixture-provider request. The transport-truncation control made three requests before reporting model_error (the engine's existing retry behavior; the harness has no retry loop). Every case intentionally omits usage, including the valid explicit-stop control. Thus absence of usage alone does not identify a missing finish reason.
The bare_eof fixture sends an accurate HTTP Content-Length and closes normally. It is not an incomplete HTTP transfer: the missing evidence is at the SSE/application-protocol layer. The transport-cut control demonstrates the different handling of an actually truncated HTTP body.
Steps to Reproduce
- Save the following as
repro.py. - Use an official Docker Agent v1.150.0 standalone binary.
- Run
python3 repro.py /path/to/docker-agent(Windows:python repro.py path/to/docker-agent.exe).
Python 3 standard library only. The script starts a loopback-only synthetic provider and a separate native API process with temporary config/data/session storage. It does not call a real model, load an existing agent configuration, inspect a native database, or execute tools/MCPs. Its temporary native assets are removed after the test process stops.
Self-contained reproduction
"""Python 3 stdlib only. Usage: python repro.py /path/to/docker-agent"""
import hashlib
import json
import os
from pathlib import Path
import socket
import subprocess
import sys
import tempfile
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.request import Request, ProxyHandler, build_opener
CASES = ("explicit_stop", "done_only", "bare_eof", "transport_cut")
calls = {name: 0 for name in CASES}
http = build_opener(ProxyHandler({}))
def frame(delta, finish=None):
return "data: " + json.dumps({
"id": "fixture", "object": "chat.completion.chunk",
"choices": [{"index": 0, "delta": delta, "finish_reason": finish}]
}) + "\n\n"
class Provider(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, *args):
pass
def do_POST(self):
request = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
name = [m["content"] for m in request["messages"] if m["role"] == "user"][-1]
calls[name] += 1
body = frame({"role": "assistant", "content": "fixture text"})
if name == "explicit_stop":
body += frame({}, "stop")
if name in ("explicit_stop", "done_only"):
body += "data: [DONE]\n\n"
body = body.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(body) + (1000 if name == "transport_cut" else 0)))
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(body)
self.wfile.flush()
self.close_connection = True
def request(base, method, path, payload=None):
data = None if payload is None else json.dumps(payload).encode("utf-8")
return http.open(Request(base + path, data, {"Content-Type": "application/json"}, method=method), timeout=30)
def main():
binary = str(Path(sys.argv[1]).resolve())
print(json.dumps({"platform": sys.platform,
"version": subprocess.check_output([binary, "version"], text=True).strip(),
"sha256": hashlib.sha256(Path(binary).read_bytes()).hexdigest()}), flush=True)
provider = ThreadingHTTPServer(("127.0.0.1", 0), Provider)
threading.Thread(target=provider.serve_forever, daemon=True).start()
try:
with tempfile.TemporaryDirectory(prefix="docker-agent-eof-") as root:
with socket.socket() as reserve:
reserve.bind(("127.0.0.1", 0))
port = reserve.getsockname()[1]
config = Path(root) / "repro.yaml"
config.write_text("""version: '15'
providers:
fixture:
api_type: openai_chatcompletions
base_url: http://127.0.0.1:%d/v1
token_key: EOF_FIXTURE_TOKEN
models:
fixture:
provider: fixture
model: fixture-model
max_tokens: 128
agents:
root:
model: fixture
instruction: Fixture only.
max_iterations: 1
""" % provider.server_port, encoding="utf-8")
env = dict(os.environ, EOF_FIXTURE_TOKEN="local-fixture-only", DOCKER_AGENT_DISABLE_DESKTOP_PROXY="1")
command = [binary, "serve", "api", str(config), "--listen", "127.0.0.1:%d" % port,
"--session-db", str(Path(root) / "native.db"),
"--config-dir", str(Path(root) / "config"), "--data-dir", str(Path(root) / "data")]
with open(Path(root) / "engine.log", "w") as log:
process = subprocess.Popen(command, env=env, stdout=log, stderr=log,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0)
try:
base = "http://127.0.0.1:%d" % port
for _ in range(100):
try:
with request(base, "GET", "/api/ping"):
break
except OSError:
if process.poll() is not None:
raise RuntimeError("fixture engine exited")
time.sleep(0.1)
else:
raise RuntimeError("fixture engine did not start")
for name in CASES:
with request(base, "POST", "/api/sessions", {"title": "EOF fixture", "working_dir": root}) as response:
session = json.load(response)["id"]
with request(base, "POST", "/api/sessions/%s/agent/repro/root" % session,
{"messages": [{"role": "user", "content": name}]}) as response:
lines = response.read().decode("utf-8").splitlines()
events = [json.loads(line[5:].strip()) for line in lines if line.startswith("data:") and line[5:].strip() != "[DONE]"]
stop = next(e for e in reversed(events) if e["type"] == "stream_stopped")
print(json.dumps({"case": name, "providerRequests": calls[name],
"reason": stop.get("reason"), "finish_reason": stop.get("finish_reason"),
"errors": [e.get("code") for e in events if e["type"] == "error"],
"hasLastMessageUsage": any(e.get("usage", {}).get("last_message") is not None
for e in events if e["type"] == "token_usage")}), flush=True)
finally:
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
finally:
provider.shutdown()
provider.server_close()
if __name__ == "__main__":
main()
Docker Agent Version
v1.150.0, commit 5277e7842058fc3fbb9943a6e88bc05a1c8e6fe2.
Official binary SHA-256:
- Windows amd64:
144191a21a25c0c1ecfa8c0b320af00c8373187a27fccd73256a7fc39de3d6c3 - Linux amd64:
791fa6bac9f829847f00ab948f838bf54d11e0830988a5e5bce88d65ea65f2d1
OS / Model
Windows amd64 and Ubuntu/WSL2 Linux amd64; public CLI + native HTTP API. Model: a local deterministic Chat Completions fixture, no real provider or credentials.
Captured Verification Output
Windows
{"platform":"win32","version":"docker-agent version v1.150.0\nCommit: 5277e7842058fc3fbb9943a6e88bc05a1c8e6fe2","sha256":"144191a21a25c0c1ecfa8c0b320af00c8373187a27fccd73256a7fc39de3d6c3"}
{"case":"explicit_stop","providerRequests":1,"reason":"normal","finish_reason":"stop","errors":[],"hasLastMessageUsage":false}
{"case":"done_only","providerRequests":1,"reason":"normal","finish_reason":"stop","errors":[],"hasLastMessageUsage":false}
{"case":"bare_eof","providerRequests":1,"reason":"normal","finish_reason":"stop","errors":[],"hasLastMessageUsage":false}
{"case":"transport_cut","providerRequests":3,"reason":"error","finish_reason":null,"errors":["model_error"],"hasLastMessageUsage":false}
Linux
{"platform":"linux","version":"docker-agent version v1.150.0\nCommit: 5277e7842058fc3fbb9943a6e88bc05a1c8e6fe2","sha256":"791fa6bac9f829847f00ab948f838bf54d11e0830988a5e5bce88d65ea65f2d1"}
{"case":"explicit_stop","providerRequests":1,"reason":"normal","finish_reason":"stop","errors":[],"hasLastMessageUsage":false}
{"case":"done_only","providerRequests":1,"reason":"normal","finish_reason":"stop","errors":[],"hasLastMessageUsage":false}
{"case":"bare_eof","providerRequests":1,"reason":"normal","finish_reason":"stop","errors":[],"hasLastMessageUsage":false}
{"case":"transport_cut","providerRequests":3,"reason":"error","finish_reason":null,"errors":["model_error"],"hasLastMessageUsage":false}
Impact and Limits
A downstream integration cannot tell a provider-confirmed stop from this EOF inference using these completion fields alone, which can affect completion-triggered workflows and diagnosis of apparent early turn endings.
An observed early-ending downstream session motivated this investigation, but its original upstream SSE was not retained. This report does not claim that the EOF fallback caused that production incident, that the provider failed to emit a terminal signal in that incident, or that a tool call was lost. All reproducible findings and output above come from synthetic local fixtures. No production prompts, logs, identifiers, addresses, configuration, or credentials are included.
Related: #3669 / #3668. Native API event contract: https://docker.github.io/docker-agent/features/api-server/
- Ngôn ngữ chính
- Go
- Star
- 4.4k
- Fork
- 508
- Merge trung bình
- 8 giờ 6 phút
- Pull request đã merge (30 ngày)
- 209
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Có Dockerfile hoặc tệp Docker Compose
- Không có mẫu pull request
- Không có hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của docker/docker-agent
-
pkg/hooks tests fail with a misleading assertion when jq is not installedCó thể đã có người làm @dwin-gharibi đã nhận 54 ngày trước. Đang mởarea/testing
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 86/100
docker/docker-agent#4052 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`remove_directory` and `create_directory` hide work they already did when a later path failsCó thể đã có người làm @dwin-gharibi đã nhận 72 ngày trước. Đang mởarea/tools
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
docker/docker-agent#3933 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`edit_file` silently edits the first match when `oldText` occurs more than onceCó thể đã có người làm @dwin-gharibi đã nhận 72 ngày trước. Đang mởarea/tools
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
docker/docker-agent#3929 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[Flaky test] TestHandleStream_ContextCancellationCó thể đã có người làm @aheritier đã nhận 1 ngày trước. Đang mởarea/testing automated flaky-test status/needs-triage
docker/docker-agent#4617 · 2 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Epic: support Gemini 4 Argon via native Google GenerateContentCó thể đã có người làm @aheritier đã nhận 1 ngày trước. Đang mởarea/docs area/models area/providers/gemini area/runtime automated
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 12/100
docker/docker-agent#4600 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của docker/docker-agent
Issue tương tự
-
Docs: how to get more careful answers (Effort, reasoning models, Reasoning level, Deliberate)Đang mởdocumentation priority:p1
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
yeixio/toskar-core#697 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
keyxmakerx/Chronicle#1532 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
The acquisition fixture's search path skips the argument table production enforces (regression from #285)Có thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Judgment-Pack/judgment-pack-gateway#293 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[Bug]: 【优化】对话消息底部增加发送时间展示Đang mởbug desktop v3 windows
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
esengine/DeepSeek-Reasonix#13697 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
good first issue help wanted
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 95/100
eugenioenko/ttt#959 ·
Maintainer thường phản hồi trong vòng 1 ngày