Conversation
…re output Remove Panel, Rule, Syntax from diff and compare commands. Diffs now delegate to `git --no-pager diff --no-index --color=auto` which respects the user's git config (diff.algorithm, diff.colorMoved, diff.external, color.diff) and does native TTY-aware color auto-detection. Errors and status messages route through Console(stderr=True) so data output on stdout stays clean for piping. `list --json` uses raw print() instead of console.print_json().
_resolve_project() now retries loadCodeAssist on 401 by refreshing the Gemini OAuth token and updating the flow's auth header. Strips phantom metadata dict injected by extract_session_id before wrapping in the v1internal envelope — Google rejects unknown fields at 'request'.
Adds compliance.profile_path config option so all project instances
share a single compliance profiles file from ~/.config/ccproxy/ instead
of each writing to their own {config_dir}/compliance_profiles.json.
…ding Replace the passive compliance observation pipeline with an explicit `ccproxy flows seed --provider X` CLI subcommand. Users now capture reference traffic through the inspector, filter flows with --jq, and seed compliance profiles from curated flows. This gives full control over which flows contribute to profiles. New: ComplianceSeeder addon (ccproxy.seed mitmproxy command), FlowsSeed CLI class, ProfileStore.set_profile(). Removed: observe_flow pipeline, submit_observation, accumulator persistence, min_observations config. Also fixes WireGuard namespace ignore_hosts race — first TLS passthrough to ignore_hosts domains failed intermittently, breaking Gemini CLI OAuth. Added _warmup_ignore_hosts() to prime connections before child command.
apply_compliance previously only ran on reverse proxy flows, meaning WireGuard flows with OAuth token injection (e.g., google-genai SDK through gemini_cli_compat → reroute_gemini) never received compliance profiles. Now also triggers on flows where forward_oauth set flow.metadata["ccproxy.oauth_injected"]. Also fixes test suite: isolate _warmup_ignore_hosts from namespace nsenter tests, add dedicated warmup tests, fix CWD-dependent log path assertion, remove 4 vestigial test files importing deleted modules.
…overwrite headers Replace the merge-based compliance model with a stamp-based one: ComplianceMerger → ComplianceStamper, apply_compliance → stamp_compliance. Headers from the compliance profile now overwrite existing values instead of skipping them, so SDK user-agents are replaced with the canonical CLI fingerprint from the seeded profile. List-valued headers (anthropic-beta) still union tokens. Also adds sentinel-key-only rule to CLAUDE.md and removes stale gemini_cli_compat 429 guidance.
…eline Introduce Envelope as a first-class dataclass shared across the entire compliance lifecycle — extraction, accumulation, persistence, and stamping — eliminating the ProfileFeature* serialize/deserialize dance. - Add Envelope dataclass (headers dict, body_fields dict, system blocks, body_wrapper) as the shared currency between seeding and stamping - Simplify ComplianceProfile to wrap a single Envelope instead of separate ProfileFeatureHeader/BodyField/System lists - Remove ProfileFeatureHeader, ProfileFeatureBodyField, ProfileFeatureSystem, and ObservationBundle classes - Rename extract_observation → extract_envelope, returns Envelope directly with string system prompts normalized to block lists - Rewrite ComplianceStamper as a two-phase prepare/wrap pipeline: prepare_envelope() builds a MaterializedEnvelope (pure, no ctx), wrap() fills it with the incoming request (order-sensitive: metadata inside wrapper, body_fields outside) - Bump store format_version 1 → 2 (degraded-mode handles mismatch)
Removes ComplianceStamper and ProfileStore in favor of SeedStore that persists raw mitmproxy flows. The new husk hook picks a seed, strips content via prepare functions, fills it with incoming request data via fill functions, then applies the result. This separates seed storage from transformation logic.
Replaces stamp_compliance with a seed-based approach: prepare functions strip original content from recorded seeds, fill functions inhabit them with incoming request data. Users compose custom pipelines via the husk hook's prepare and fill params. BREAKING CHANGE: removed stamp_compliance hook, use husk hook with prepare/fill params instead
Removes the profile-based stamping pipeline in favor of direct husk application, simplifying the compliance flow integration.
Envelope extraction logic is no longer needed in the compliance flow.
Deleted unused compliance profile classification logic including header and body field filtering functions.
Migrates from profile-based stamping to a two-phase seed/husk architecture with explicit prepare and fill steps. BREAKING CHANGE: removed `profile_path`, `seed_anthropic`, `additional_header_exclusions`, `additional_body_content_fields`, and `stamper_class` from ComplianceConfig; replaced with `seeds_dir`
The compliance module now uses user-curated seed flows and composable prepare/fill pipelines instead of automatic profile learning, giving users explicit control over request shaping. BREAKING CHANGE: removed `ComplianceProfile`, `ObservationAccumulator`, `ComplianceMerger`; replaced `apply_compliance` hook with `husk` hook requiring `prepare`/`fill` params in config
Restructures compliance processing into explicit prepare and fill stages for better separation of concerns. Moves profile storage from single JSON file to seeds directory. BREAKING CHANGE: config key `compliance.profile_path` renamed to `compliance.seeds_dir`
Priority now defines canonical execution sequence. Read/write tracking becomes a parallelism hint rather than the sole dependency source, preventing spurious cycles.
apply_husk was clobbering host/port/scheme/path set by the redirect handler and wiping auth headers injected by forward_oauth, causing TLS handshake failures (seed had raw IPs) and 401s (auth lost). Now only stamps compliance-relevant headers and body from the seed, preserving transport routing and auth. Also strips model-capability fields (thinking, output_config, context_management) from seeds and fixes FlowRecord serialization in the compliance seeder.
…meterized strip_system_blocks Single prepare fn with Python slice syntax via keep arg (e.g. ":1", "1:", "-2:"). Husk hook resolves "mod.fn(arg)" entries via functools.partial. Pipeline render shows prepare/fill as numbered side-by-side columns with common prefix stripped.
Replace the seed/husk metaphor with a simpler "shape" metaphor: capture a shape, prepare it, fill it, apply it. - compliance/ package → shaping/ - Husk type → Shape, SeedStore → ShapeStore - husk hook → shape hook, ccproxy.seed cmd → ccproxy.shape - ccproxy flows seed → ccproxy flows shape - ComplianceConfig → ShapingConfig, seeds_dir → shapes_dir - All imports, tests, docs, nix config, YAML templates updated
The old _do_seed expected profile-era keys (key, flows_used, headers, body_fields, system). The ccproxy.shape command returns (status, provider, flows_saved, missing). Pre-existing bug, now fixed.
mitmproxy 12 metadata iteration yields FlowMeta objects, not plain strings — .startswith() fails without explicit str() cast.
mitmproxy 12 metadata contains FlowMeta enum keys that FlowWriter cannot serialize. Strip all non-string keys alongside ccproxy runtime keys when capturing shapes.
_resolve_entry("mod.fn(arg)") was using functools.partial(fn, arg)
which bound arg as the first positional — stomping over the shape
parameter. Now introspects the function signature to find the first
defaulted parameter and binds by keyword.
Also strips non-string metadata keys (FlowMeta enums) in
_strip_runtime_metadata for mitmproxy 12 compat.
… deltas
Current ChatGPT often streams the answer inline via the string content path
/message/content/text (content_type text|code, a 'text' field, not a 'parts'
array) followed by bare {"v":"..."} continuation deltas that continue the
last (path, op) with an implied append (recon §2 — the SSE-v1 majority shape).
The intake only handled /message/content/parts/0 and dropped bare string
deltas, rendering those turns empty.
Add a last_path cursor + _TEXT_CONTENT_PATHS (parts/0 + content/text); resolve
bare deltas against the cursor as an append; route both content paths through
the existing suffix-diff logic. Additive — parts/0 streaming is unchanged.
Verified against the recon doc's 'Paris' split-token example (full + split-chunk
+ collect mode) plus a bare-before-content silence check.
Rework the conduit WS reader to handle whatever comes down the line instead of switching on envelope type. _walk_sse_items recursively pulls every encoded_item SSE payload at any depth (message payload.payload, conversation-turn-stream stream-item payload.encoded_item, reply catchups), tagging each with its nearest topic; _decode_encoded_item tolerates plaintext or base64. Turn-topic items are forwarded to the intake; the turn ends on the structural [DONE]/done signal. Never silently drop: every inbound WS message is recorded in a bounded WSFrameCapture ring (new ws_capture.py singleton), forwarded or not, so non-turn / unhandled events (app_notifications, other conversations) stay inspectable. Removed the reply/message/else-continue branching and the two-level encoded_item assumption. _WS_READ_TIMEOUT is reframed as a temporary idle backstop (not a functional terminator; to be removed once live turns prove structural EOS) — the socket is never torn down on a timer. Unit-tested exhaustively against all known frame shapes; LIVE VERIFICATION still pending (intermittent conduit route).
…nostic
Live forced-handoff verification (skip conduit prewarm to provoke the rare
server handoff) confirmed the WS transport works against the real server:
celsius/ws/user -> 200, dialed wss, subscribed the JWT turn_topic_id,
_walk_sse_items structurally extracted 10 items, terminated on structural
[DONE] — the old 'connects but hangs 120s with no content' failure is gone.
But that handoff yielded content=None: forwarded=10 items, zero text. That
signature means the items are NOT plaintext 'data:' SSE — consistent with the
SPA _Nn decode, where encoded_item is a {event,data} object (raw or base64)
whose 'data' field carries the SSE-v1 patch. _decode_encoded_item now unwraps
that shape into an event:/data: line (tolerant, graceful fallback so the
plaintext/base64-SSE shapes still work). Verified end-to-end in unit tests:
a conversation-turn-stream/stream-item frame with a base64 {event,data}
encoded_item now flows extract -> decode -> intake -> text.
Also log every raw WS frame at DEBUG (ws_handoff: RAWFRAME) so the next
handoff is fully diagnosable.
LIVE STATUS: transport verified; the {event,data} content fix is evidence-
grounded (SPA _Nn + the forwarded=10/None signature) and unit-tested, but NOT
yet confirmed on a live handoff — the conduit route fired once in ~55 forced
turns and could not be recaptured. Confirm on the next organic handoff.
Reading the real client code (chatgpt.com sdk.js _Nn) settles the encoding
question: _Nn parses encoded_item AS an SSE stream (data:/event: line parser);
aurora checks 'data: [DONE]' literally inside it. So encoded_item is already
plaintext SSE — there is NO base64/{event,data} wrapper. The previous decoder
branch conflated _Nn's OUTPUT ({event,data}) with the encoding of its INPUT.
_decode_encoded_item is now an honest passthrough that logs any non-SSE payload
rather than transforming it on a guess.
So content=None on the live handoff was never an encoding problem — the intake
was silently dropping the answer. Add telemetry so it never does so unobserved:
count + DEBUG-log content patches that arrive off the tracked final-answer
channel, and assistant text messages NOT adopted as final (the WS catch-up
replays a message already status=finished_successfully, which
_is_final_answer_message rejects — the prime suspect). intake.close() emits a
WARNING when a stream produced NO text despite carrying answer content.
_parse_frame logs unparseable/non-object SSE frames.
The next conduit handoff now diagnoses itself from the logs instead of
returning a silent empty turn.
Extend the never-silently-drop discipline (already on the OpenAI Conversations intake) across the whole lightllm response graph: every intake and render FSM, the SSE pipeline, the buffered transform, and the dispatch glue. Intakes (anthropic / openai / google / perplexity / openai_responses): add frames_seen / frames_unparseable / emitted_events counters, a DEBUG log at each unparseable-frame drop, and a close() WARNING when the stream carried frames but emitted zero IR events (guarded by refusal where applicable). Expose .state for inspection. Renders (openai / anthropic / openai_responses): track events_received / bytes_emitted; WARNING on close() when events arrived but no content bytes were rendered. Expose .state. sse_pipeline: WARNING when EOS produced no content bytes (streaming) or assembled no parts (collect mode). buffered: WARNING when assembling a contentless object, an empty synthetic SSE, or a choiceless ChatCompletion. dispatch (__init__): DEBUG the resolved provider_type/inbound_format → FSM choice; WARNING before each unsupported-type raise. Additive only — no parsing/rendering behavior change. caplog-based tests assert each WARNING fires on a contentless/dropped stream and stays silent on a clean one.
…answers) The WS catch-up replays the assistant message already status=finished_successfully, which _is_final_answer_message rejects — so on a conduit handoff no final_channel was set and every content patch was dropped (the live content=None). Keying adoption on status is wrong for a replay. Fix: adopt the channel where visible content actually lands. handle_add records every assistant content_type=text (non-hidden) channel as a candidate regardless of status; handle_patch lazily adopts a candidate the first time content arrives on it when no in-progress add anchored one. The eager in-progress path is unchanged (inline turns behave exactly as before); historical/finished messages are only adopted if they actually receive content patches, which inline they never do. Grounded in the intake's own logic, not a wire guess. The off-channel / no-text close() telemetry remains the backstop if a future handoff drops content for another reason. Live-unconfirmed (the conduit route fired once in ~185 forced turns); the telemetry will confirm on the next organic handoff.
…ns + images) End-user usage guide for the openai_conversations provider: provider config, credential file (access_token/device_id), gateau cookie export, the sentinel key, a runnable Pydantic AI example (PEP 723, verified live: text + multi-turn session), how sessions work (message-history threading + the optional server-side thread-inject hook), the images API (generations/edits), models, and troubleshooting. The embedded example was run standalone via 'uv run' against a real chatgpt-paid account (turn1='noted', turn2='Kyle').
The image side-trips (file upload, conversation poll, download-metadata) were the one place sending a thin 2-header request (Bearer + oai-device-id) instead of the full browser shape every other backend-api call uses — so the image poll got WAF-403'd 40x (not a stale key; the same bearer 200'd the POST seconds earlier). Root cause: a local _auth_headers helper bypassing the canonical builder; the image path (CHATGPT-006) was built without a live credential and never exercised. Add get_api_headers (the one canonical authenticated chatgpt.com /backend-api identity = full browser shape + Bearer) and route everything through it: the addon's sentinel/conduit/final builders AND image upload/poll/download-metadata. The presigned blob legs (Azure PUT, signed download GET) stay bare — a Bearer there is itself a 403. Live-verified: the image poll now returns HTTP 200 (was 403); the image generates. Lock-in tests assert the canonical shape. Remaining async_status=4 (ChatGPT delivers the finished image via the async notification, not the conversation) is a separate follow-up.
Mirrors talkstream's render_graphs.py: imports the built pydantic-graph FSMs and emits their native render() mermaid — single source of truth, no stubs. Renders all 6 intake + 3 render graphs plus their 13 subgraphs (22 diagrams), with subgraphs shown both as parent step-nodes and expanded individually. docs/lightllm_graphs.md is the generated artifact; regenerate with 'uv run python -m ccproxy.lightllm.graph.render_graphs --markdown'.
… not no-asset) ChatGPT's image turn is async: the conversation reports async_status:3 while generating, then async_status:4 once done — and the image_asset_pointer IS present at that point (live-verified by fetching a completed image conversation: async_status=4 carried a sediment:// pointer, 1536x1024). The poll raised on async_status==4 BEFORE extracting the pointer, so every picture_v2 image was discarded as 'finished without assets'. Fix: extract the pointer each poll and return it when present; async_status==4 is the DONE signal, and a status-4 conversation with no pointer is the genuine failure. Combined with the header unification, the full picture_v2 path now works end-to-end (generate -> async poll -> download-metadata -> presigned download): live-verified by generating a 'graph atlas' image and retrieving the 1.8MB PNG. Test covers both status-4-with-pointer (returns) and status-4-without-pointer (raises).
Establishes convention for reading sibling first-party project docs before working with their APIs, ensuring agents consult curated documentation rather than inferring from source alone.
Consolidates duplicated FSM state slots (usage, raw_extras, finish_reason, provider_response_id) and SSE framing logic from all provider-specific intake/render modules into shared base classes. Adds _usage.py helpers to capture and project token accounting across wire formats, fixing silent usage drops in cross-format transforms.
Nix's python setup hook aggregates all python packages into PYTHONPATH, creating a python 3.14 closure that shadows the project's 3.13 venv and breaks ABI-sensitive imports like mypy. Moving mypy to uv dev dependencies and unsetting PYTHONPATH ensures the uv venv owns the Python environment.
Library-only, inbound-only cross-IR transform bridging Alloy's provider-specialized prompt AST JSON into the pydantic-ai ModelMessage IR lightllm operates on. AST → IR directly (never lowered through a wire format). No InboundFormat, no Context, no dispatch wiring — the proxy pipeline is untouched. parsed_request_from_alloy(prompt_ast, client_view) builds a ParsedRequest a consumer hands to dispatch_dump_sync. Node grouping mirrors the wire adapters; media maps to URL/BinaryContent (no media_type fallback); local file sources and media in system/assistant fail loudly; message metadata follows the VM-003 cache_control forward contract.
…t-aware tool caching Adds the CachePolicy placement engine (Anthropic 4-breakpoint budget, authored-wins, idempotent) with the cache_breakpoints outbound hook (x-ccproxy-cache-policy header DSL / hook params), and makes the Anthropic adapter deferment-aware: ToolDefinition.defer_loading round-trips both directions, the dump side stamps cache_control on the last non-deferred tool only (pydantic-ai contract; Anthropic rejects markers on deferred tools), and the load-side lift fires only for the canonical single-marker boundary shape — every other stamping pattern rides raw_extras["tools"] verbatim.
…, typed-tool wire fidelity Intake: branch tool_search_tool_result blocks to pydantic-ai's _map_tool_search_tool_result_block (success → discovered_tools, error → provider_details) — previously they fell to the unknown-block catch-all and were dropped, orphaning the server_tool_use call side (Anthropic 400s unpaired tool_search_tool_* on replay). Also fixes streamed server_tool_use args: emit with args=None when the block's input is empty so input_json_delta strings can attach, then finalize tool-search args to the canonical ToolSearchArgs dict at content_block_stop (mirrors upstream's streaming path; the buffered synthesizer's full-input start events are unaffected). Adapter: any tool entry whose wire type isn't "custom" now rides the raw_extras["tools"] verbatim override — server/builtin tools (versioned type, side fields like max_uses) were being flattened to bare name+schema on round-trip. ToolDefinitions are still built for typed promotion; the canonical-marker lift is skipped when the typed override fires. Adds the tool_search_tool_bm25/regex_20251119 discriminators to ANTHROPIC_TYPED_TOOLS. Cache engine: census counts cache_control markers riding the tools override, and the tools placement is skipped whenever the override is present (a knob placement would burn budget for a wire no-op — the verbatim array overwrites formatted tools at stitch time). Docs: raw_extras tools triggers, promotion-vs-preservation, census/skip rules, and the tool-search invariants hooks must honor (pair orphaning, tool_reference stranding, dump/render-side limitations).
Replaces hardcoded 120s literals in session_ws and ws_handoff with a shared config field. Idle give-ups now emit a typed ccproxy_idle_timeout terminal frame (finish_reason="error") instead of ending silently, so truncated turns are distinguishable from genuine [DONE] completions.
SSEPipeline._drain_and_terminate() handed the render close() implementations only usage and raw_extras, so every streaming terminator announced a fixed outcome: the OpenAI Chat finish chunk said "stop", the Anthropic message_delta said "end_turn", the Responses postlude said response.completed. A turn cut short at the token ceiling, stopped by a content filter, or killed by an upstream error was byte-for-byte indistinguishable from a clean completion for every provider's cross-format streaming transform. The intake funnel already carried the answer. Thread it: close() gains a finish_reason keyword on the base contract, the pipeline supplies the intake's capture, and one new module owns the per-listener vocabulary — the finish-reason twin of _usage.py, with from_* capture and to_* projection. Both transport paths call it, so a stream:true client and a stream:false client are told the same thing about how the turn ended. Capture gaps closed at the same seam, without which the threading would stay half-dead for two providers: - The Anthropic intake never read message_delta.delta.stop_reason, though its modeled-keys set already claimed stop_reason was modeled. Raw wire values that the IR cannot express (pause_turn, compaction) now ride raw_extras instead of vanishing. - The Google intake never read candidate.finishReason. Capture runs before the contentless-candidate short-circuit, which is the exact frame a MAX_TOKENS cut arrives on. Phantom surfaces removed rather than left lying: the buffered Anthropic assembler's stop_reason parameter that no caller passed is now plumbed; _OPENAI_FINISH_BY_PART was read by nothing; the Chat render state's five-member finish_reason Literal was only ever set to "tool_calls" and becomes the boolean it always was; the Responses render state's documented finish_status was never assigned at all. "error" reaches the OpenAI Chat and Anthropic wires as "error" — neither closed enum has a member for it, and a fabricated completion is the lie this change exists to remove. The buffered Chat body already emitted it; Stainless SDKs parse response enums permissively, so no client hard-fails on it. Regressions drive real SSEPipeline instances end to end; eleven of them fail on the pre-fix code, each crossing the exact former threshold.
Replace the removed ccproxy run --inspect flag with the live --capture interface throughout the inspector skill. Include the existing routine flake input refresh already present in the shared worktree.
DeepSeek's API serves deepseek-v4-pro and deepseek-v4-flash; the declared deepseek-v4 binding resolves to no upstream model, so any request routed to it fails at the provider.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AI Summary
Complete rewrite of ccproxy from a LiteLLM proxy subprocess model to an in-process mitmproxy-based transparent LLM API interceptor. This is the v2.0.0 release (tagged
v2.0.0-rc1).BaseConfigtransformation pipeline, bypassing cost tracking and callback machinery entirely.@hook(reads=..., writes=...)decorator-declared data dependencies, topologically sorted via Kahn's algorithm. Per-request overrides viax-ccproxy-hooksheader.SseTransformerstateful stream callable — parses, transforms per-chunk via LiteLLM's provider iterators, re-serializes as OpenAI-format SSE.cachedContentsAPI, path rewriting forcloudcode-pa.googleapis.com.ccproxy flows list/dump/diff/compare/clearwith multi-page HAR 1.2 output, jq filtering, and sliding-window diff across flow sets.POST /mcp/notifyfor terminal event ingestion, buffered and injected as synthetic tool_use/tool_result pairs.~/.config/ccproxy/(breaking change).initreplacesinstall: CLI rename (breaking change).render_pipeline()builds a full DAG display with parallel groups viarich.columns.Columns.Breaking Changes
~/.ccproxy/→~/.config/ccproxy/ccproxy install→ccproxy init--debugflag replaced by--log-level/-vforward_port/reverse_portreplaced by unified port configmitmconfig section renamed toinspectto_mermaid/to_asciiremoved fromHookDAGTest plan
just testpasses with ≥90% coveragejust lint/just typecheckcleanccproxy run --inspect -- claude --model haiku -p "what's 2+2"ccproxy initcreates config at~/.config/ccproxy/ccproxy flows list,ccproxy flows dump