Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
672 changes: 672 additions & 0 deletions src/core/pin.ts

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions src/core/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
chatCompletionsUrl,
openAIChatToAnthropicResponse,
} from './messages-chat-bridge.js';
import { pinCommandResponse } from './pin.js';
import { parseGoogleModelFromPath, transformGoogleGenerateContent } from './google.js';
import { isGeminiModel } from './gemini-model-profiles.js';
import { resolveGptProfile } from './gpt-model-profiles.js';
Expand Down Expand Up @@ -1149,6 +1150,22 @@ export function createProxy(config: ProxyConfig = {}) {
// Fail-closed: unreadable model → no compression, not a risky guess.
const model = googleModel ?? readModelField(bodyIn);
requestModel = model ?? undefined;
// A turn whose only content is `@pxpipe pin` / `@pxpipe unpin` is
// configuration, not a question. Answer it here: forwarding it would bill
// a full prefix to have the model paraphrase a list the proxy already
// holds, and the reply would be a guess about state it cannot see.
if (isMessages) {
const pinReply = pinCommandResponse(bodyIn);
if (pinReply) {
return new Response(pinReply.body, {
status: 200,
headers: {
'content-type': pinReply.contentType,
'cache-control': 'no-cache',
},
});
}
}
// /v1/messages is only a wire schema: Claude Code can target a non-
// Anthropic model (for example GPT-5.6 Sol). Do not apply Claude's
// renderer or Anthropic count_tokens merely because the route is
Expand Down
10 changes: 10 additions & 0 deletions src/core/tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ export interface TrackEvent {
baseline_imaged_tokens?: number;
/** Provider-specific estimate of pxpipe-added native text. */
native_injected_tokens?: number;
/** Chars re-emitted as the pin footer on the last user message.
* These are MOVED, not copied: the source lines are stripped from the
* cacheable prefix and re-sent as plain text at the tail, so they are paid
* at full input price every turn instead of cache-read price once. Not part
* of orig_chars or compressed_chars, so any savings figure that ignores it
* overstates the win by this much per turn. */
pin_chars?: number;
/** TEXT chars in the outgoing body (all text blocks, incl. non-compressed tool_results).
* With image_pixels, a regression over cold-miss events solves chars_per_token (α) and pixels_per_token (β). */
outgoing_text_chars?: number;
Expand Down Expand Up @@ -218,6 +225,9 @@ export function toTrackEvent(ev: ProxyEvent): TrackEvent {
if (info.outgoingTextChars !== undefined && info.outgoingTextChars > 0) {
out.outgoing_text_chars = info.outgoingTextChars;
}
if (info.pinChars !== undefined && info.pinChars > 0) {
out.pin_chars = info.pinChars;
}
if (info.responsesComposition) {
out.responses_composition = info.responsesComposition;
}
Expand Down
66 changes: 62 additions & 4 deletions src/core/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
renderCellWidth,
type RenderStyle,
} from './render.js';
import { appendPinBlock, canAppendPinBlock, foldPins, stripPinCommands, type Pin } from './pin.js';
import { factSheetText } from './factsheet.js';
import { stripSchemaDescriptions, schemaHasStructure } from './schema-strip.js';
import { bytesToBase64 } from './png.js';
Expand Down Expand Up @@ -563,6 +564,12 @@ export interface TransformInfo {
/** Total TEXT chars in the outgoing body (system + messages, excluding image base64).
* Denominator for empirical chars-per-token regression on cold-miss events. */
outgoingTextChars?: number;
/** User-pinned instructions relocated to the request tail, and the chars that
* cost. Both absent when nothing is pinned. Uncached by construction (the
* block lands after every breakpoint), so these chars are paid every turn. */
pinChars?: number;
/** Pin folding threw and was skipped. The body still goes out unpinned. */
pinError?: string;
/** OpenAI Responses only: local o200k decomposition of the ORIGINAL request
* before pxpipe rewrites it. No provider count_tokens call. Categories are
* mutually exclusive text-token estimates; imageParts counts native images. */
Expand Down Expand Up @@ -1490,6 +1497,19 @@ function approxBlockBytes(blk: ImageBlock): number {
// --- main transform --------------------------------------------------------


/**
* Emit the user's pins at the tail, immediately before serialization.
*
* Last, so nothing the transform does afterwards can bury them again, and after
* every cache_control breakpoint, so the appended bytes are always in the
* re-read suffix and can never invalidate a cached prefix.
*/
function applyPins(req: MessagesRequest, info: TransformInfo, pins: Pin[]): void {
if (pins.length === 0 || !Array.isArray(req.messages)) return;
const chars = appendPinBlock(req.messages, pins);
if (chars > 0) info.pinChars = chars;
}

/**
* Run history-image compression on `req.messages` and finalize the body.
* Called from both the main path AND early-exit paths (below_min_chars,
Expand All @@ -1501,6 +1521,7 @@ async function runHistoryCollapseAndFinalize(
o: Required<TransformOptions>,
opts: TransformOptions,
droppedCodepoints: Map<number, number>,
pins: Pin[],
): Promise<{ body: Uint8Array; info: TransformInfo; collapsed: boolean }> {
let collapsedFlag = false;
if (Array.isArray(req.messages) && req.messages.length > 0) {
Expand Down Expand Up @@ -1568,6 +1589,7 @@ async function runHistoryCollapseAndFinalize(
info.historyReason = histInfo.reason;
}
}
applyPins(req, info, pins);
info.outgoingTextChars = countOutgoingTextChars(req);
const outBody = new TextEncoder().encode(JSON.stringify(req));
return { body: outBody, info, collapsed: collapsedFlag };
Expand Down Expand Up @@ -1633,6 +1655,35 @@ export async function transformRequest(
return { body, info };
}

// 0. User-pinned instructions. Fold the transcript's pin commands, then remove
// them from the outbound copy — the client's own transcript is untouched, so
// the next request still carries every command and re-derives the same state.
// The surviving pins are re-emitted at the very tail (applyPins, below), after
// every cache breakpoint, where the model reads them last.
// `pinsRewrote` is what the early-exit paths below test: when either the
// strip or the tail append changed `req`, the original bytes no longer
// describe the request and must not be the ones forwarded.
// Strip and append are one move, so both are skipped unless the tail can
// take the block; otherwise the strip would delete the rules outright.
let pins: Pin[] = [];
let pinsRewrote = false;
if (Array.isArray(req.messages) && canAppendPinBlock(req.messages)) {
try {
pins = foldPins(req.messages);
const stripped = stripPinCommands(req.messages);
pinsRewrote = pins.length > 0
|| stripped.length !== req.messages.length
|| stripped.some((m, i) => m !== req.messages![i]);
req.messages = stripped;
} catch (e) {
// A malformed message must not fail the request: pins are an optimization,
// the untouched body is still valid.
pins = [];
pinsRewrote = false;
info.pinError = String((e as Error)?.message ?? e);
}
}

// 1. Pull system text out. Split into:
// - billingLine: Claude Code's per-turn random header (must NOT be cached).
// - dynamicText: <env>/<context>/... blocks (per-turn, kept as text).
Expand Down Expand Up @@ -1788,12 +1839,15 @@ export async function transformRequest(
// If history collapses, we flip `info.compressed = true` and let the
// library wrapper return reason='applied'; otherwise this still
// populates `outgoingTextChars` for the regression denominator.
const finalized = await runHistoryCollapseAndFinalize(req, info, o, opts, droppedCodepoints);
const finalized = await runHistoryCollapseAndFinalize(req, info, o, opts, droppedCodepoints, pins);
if (finalized.collapsed) {
info.compressed = true;
return { body: finalized.body, info };
}
return { body, info };
// `body` is the original bytes. If the pin pass edited `req`, those bytes
// describe a request we are no longer sending, so forwarding them would put
// the raw `@pxpipe pin` line back and drop the tail block.
return { body: pinsRewrote ? finalized.body : body, info };
}

// Break-even check guards even the slab (rare edge: tiny tool docs + tiny slab < 10k chars).
Expand Down Expand Up @@ -1857,12 +1911,15 @@ export async function transformRequest(
info.reason = `not_profitable (slab=${combined.length} chars)`;
bumpPassthrough(info, 'not_profitable');
// Slab not profitable but history may still be collapsable — try before returning.
const finalized = await runHistoryCollapseAndFinalize(req, info, o, opts, droppedCodepoints);
const finalized = await runHistoryCollapseAndFinalize(req, info, o, opts, droppedCodepoints, pins);
if (finalized.collapsed) {
info.compressed = true;
return { body: finalized.body, info };
}
return { body, info };
// `body` is the original bytes. If the pin pass edited `req`, those bytes
// describe a request we are no longer sending, so forwarding them would put
// the raw `@pxpipe pin` line back and drop the tail block.
return { body: pinsRewrote ? finalized.body : body, info };
}

// Instruction header co-renders into the same PNG (+1.04pp L1 OCR vs baseline;
Expand Down Expand Up @@ -2242,6 +2299,7 @@ export async function transformRequest(
}
info.droppedCodepointsTop = out;
}
applyPins(req, info, pins);
info.outgoingTextChars = countOutgoingTextChars(req);
const outBody = new TextEncoder().encode(JSON.stringify(req));
return { body: outBody, info };
Expand Down
6 changes: 6 additions & 0 deletions src/dashboard/fragments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,12 @@ export function renderStatsTableFragment(p: FullStatsPayload): string {
tr('original chars', numFmt(s.origCharsTotal)) +
tr('image bytes', numFmt(s.imageBytesTotal)) +
tr('bytes / char', charRatio) +
(s.pinEvents
? tr(
'pin footer (uncached)',
`${numFmt(s.pinCharsTotal ?? 0)} chars / ${numFmt(s.pinEvents)} req`,
)
: '') +
tr('latency p50 / p95', `${numFmt(s.durationP50)} / ${numFmt(s.durationP95)} ms`) +
tr('first-byte p50 / p95', `${numFmt(s.firstByteP50)} / ${numFmt(s.firstByteP95)} ms`) +
`</tbody></table>`
Expand Down
2 changes: 2 additions & 0 deletions src/dashboard/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ export interface FullStatsSummary {
eventsWithBaseline: number;
origCharsTotal: number;
imageBytesTotal: number;
pinCharsTotal?: number;
pinEvents?: number;
durationP50: number;
durationP95: number;
firstByteP50: number;
Expand Down
26 changes: 26 additions & 0 deletions src/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export interface Summary {
* from the text path by rendering to PNG. */
origCharsTotal: number;
imageBytesTotal: number;
/** Sum of pin_chars: text pxpipe moved out of the cacheable prefix and
* re-emitted as the tail footer. Paid at full input price every turn, so it
* is a recurring charge against the one-time imaging win above. */
pinCharsTotal: number;
/** Requests that carried a pin footer. Zero means pins cost nothing. */
pinEvents: number;
/** Aggregated Anthropic token usage. */
inputTokensTotal: number;
outputTokensTotal: number;
Expand Down Expand Up @@ -59,6 +65,8 @@ export function newSummary(): Summary {
passthrough: 0,
origCharsTotal: 0,
imageBytesTotal: 0,
pinCharsTotal: 0,
pinEvents: 0,
inputTokensTotal: 0,
outputTokensTotal: 0,
cacheCreateTokensTotal: 0,
Expand Down Expand Up @@ -89,6 +97,13 @@ export function fold(s: Summary, ev: TrackEvent): Summary {
if (ev.reason) s.skipReasons.set(ev.reason, (s.skipReasons.get(ev.reason) ?? 0) + 1);
}

// Outside the compressed branch on purpose: the pin footer is appended on
// both the compressed and passthrough paths, so it is charged either way.
if (typeof ev.pin_chars === 'number' && ev.pin_chars > 0) {
s.pinCharsTotal += ev.pin_chars;
s.pinEvents++;
}

if (typeof ev.duration_ms === 'number') s.durationMs.push(ev.duration_ms);
if (typeof ev.first_byte_ms === 'number') s.firstByteMs.push(ev.first_byte_ms);

Expand Down Expand Up @@ -181,6 +196,15 @@ export function renderTextReport(s: Summary): string {
const ratio =
s.origCharsTotal > 0 ? (s.imageBytesTotal / s.origCharsTotal).toFixed(3) : '—';
lines.push(` bytes/char ratio: ${ratio}`);
if (s.pinEvents > 0) {
const perTurn = Math.round(s.pinCharsTotal / s.pinEvents);
lines.push(
` pin footer: ${fmtN(s.pinCharsTotal)} chars over ${fmtN(s.pinEvents)} req (~${fmtN(perTurn)}/req)`,
);
lines.push(
' moved out of the cached prefix, so charged as fresh input every turn',
);
}
lines.push('');

lines.push('Anthropic token usage:');
Expand Down Expand Up @@ -299,6 +323,8 @@ export function summaryToJson(s: Summary): Record<string, unknown> {
passthrough: s.passthrough,
origCharsTotal: s.origCharsTotal,
imageBytesTotal: s.imageBytesTotal,
pinCharsTotal: s.pinCharsTotal,
pinEvents: s.pinEvents,
inputTokensTotal: s.inputTokensTotal,
outputTokensTotal: s.outputTokensTotal,
cacheCreateTokensTotal: s.cacheCreateTokensTotal,
Expand Down