If your team uses Aider, Cline, Continue, Copilot CLI, Goose, or Windsurf — plus any OpenAI-compatible backend like Azure, groq, fireworks, mistral, or together.ai — you now have a single provenance capture layer for all of them. It shipped this week in lineagelens-proxy/adapters/openai_chat.py.
But writing that adapter revealed something worth documenting: "speaks the Chat Completions format" does not mean "expresses code edits the same way." There are three distinct patterns in the wild, and missing any one of them produces silent capture gaps.
The three edit expression patterns
Pattern A: Tool-call edits
The canonical OpenAI way. The model emits choices[].message.tool_calls[], each with a function name and JSON-serialized arguments. File edits arrive as structured arguments to tools like write_file, str_replace_editor, or apply_patch.
In streaming mode this means assembling argument JSON fragments across SSE chunks — each tool_calls[].function.arguments delta is a partial string that only makes sense when concatenated with every prior delta for the same tool_calls[].index. The adapter accumulates these per (choice_index, tool_call_index) before parsing.
The adapter also handles the legacy singular function_call field, which OpenAI deprecated but older tool integrations still emit.
Pattern B: Text-content edits
This is where it gets interesting. Several major tools — Aider being the most prominent — do not use tool calls at all. They send edits as structured text inside the assistant message content. Three sub-formats exist:
Aider SEARCH/REPLACE blocks:
<<<<< SEARCH,=====,>>>>> REPLACEdelimiters. The filename is on the line immediately before the opening fence, not inside the block itself. The adapter has to look backwards in the preceding text to find it.Unified diffs: Standard
--- a/file/+++ b/file/@@format. The adapter parses these into per-file edit records.Fenced code blocks: The fallback. A block with a
path=hint in the info string, or a# file: path/to/filecomment on the first line of the code. If neither is present, the block is still captured asfile_path="proxy-capture"— never silently dropped.
The priority order matters: apply-patch DSL is tried first, then Aider SEARCH/REPLACE, then unified diff. Fenced code blocks only run when none of the structured formats matched. This ensures a single edit is never recorded twice.
Pattern C: Mixed responses
A single response can carry both tool calls and structured text content. This is more common than you might expect — some tools emit a tool call for the primary edit and then add context or diff output as text. The adapter runs both paths against every response.
The session key problem
When two developers are using the same proxy simultaneously and happen to send requests that produce overlapping tool_call_id values, you get an aliasing bug: two unrelated edits get cross-attributed.
The fix is a session fingerprint derived from the request, not a counter or timestamp:
def _openai_chat_session_key(body_dict: dict, headers: dict) -> str:
system = ""
for msg in (body_dict.get("messages") or []):
if isinstance(msg, dict) and msg.get("role") in ("system", "developer"):
system = _content_to_text(msg.get("content"))
break
auth = ""
for header_name in ("authorization", "x-api-key", "Authorization", "X-Api-Key"):
v = headers.get(header_name)
if v:
auth = v[:24]
break
raw = f"openai-chat|{system[:4096]}|{auth}"
return hashlib.sha256(raw.encode("utf-8", errors="replace")).hexdigest()[:16]
SHA-256 over the system message prefix and the first 24 characters of the auth header. The result is a 16-hex-char fingerprint. The pending-edits store keys on (session_key, tool_call_id) tuples — making aliasing across concurrent sessions geometrically unlikely (~1 in 10^19).
The fail-open rule
Every parse path in the adapter is wrapped in exception handling that fails open: a JSON decode error, a malformed SSE chunk, a tool-call argument that does not map to any known edit shape — none of these surface as errors to the forwarding path. The response always passes through to the tool.
# Everything is fail-open: a parse error never raises into the forwarding path.
This is not a compromise. It is the design requirement. A governance layer that crashes the developer's tool gets disabled within hours of installation. The only viable architecture for a capture layer that developers leave running is one that can never interrupt the thing they care about — their coding session.
What this enables for Risk Discovery
The concrete payoff is in the lineagelens report CLI. With full coverage across Aider, Cline, Windsurf, and every groq/mistral/Azure backend, you can now run:
lineagelens report . --unreviewed --category auth
And get back every AI-written line in your auth paths — regardless of which tool wrote it — that has never been reviewed. This query is only meaningful when your capture layer actually covers your whole stack. An adapter that misses Aider's text-content edits silently excludes every Aider-generated file from that result.
The path annotation on every captured record looks like this:
tool_name: "aider_search_replace" | "text_codeblock" | "str_replace_editor"
file_path: "src/routes/auth.py"
verb: "replace" | "write" | "add"
prompt_context: { model: "gpt-4o", system: "...", messages: [...] }
Three fields that a git blame will never give you: which tool made this change, what the model was, and what the developer asked for.
What is not yet covered
The adapter comment is explicit about the gaps:
# TODO(bedrock/vertex): Claude-via-Bedrock uses /model/{id}/invoke[-with-response-stream]
# on *.bedrock-runtime.*.amazonaws.com and Gemini-via-Vertex uses
# :generateContent / :streamGenerateContent on *-aiplatform.googleapis.com.
# Those are NOT chat/completions and are NOT captured here — they need their own adapters.
If your team routes through Bedrock or Vertex, this adapter does not cover you. Separate adapters are needed. This is the right design — assuming coverage for endpoints you have not explicitly tested is worse than documenting the gap.
Getting started
The adapter is part of the LineageLens proxy, which runs alongside your AI coding tool and intercepts traffic at the network layer. No tool modification required for tools that already target a configurable API endpoint.
Cross-reference: for Hashnode readers, I wrote about the broader proxy modularization and what each adapter handles at lineage-website.vercel.app.
Full source at lineage-website.vercel.app.
Which tools in your stack are you most worried about provenance gaps for — the chat-completions tools, or the proprietary backends (Cursor, GitHub Copilot) that cannot be proxied at all?
Top comments (0)