Skip to content
Closed
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
10 changes: 7 additions & 3 deletions src/claude/outbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,16 @@ export function anthropicErrorType(status: number): string {
* (devlog/_plan/261009_claude_1m_default/010).
*/
const CLAUDE_PROMPT_TOO_LONG_RE = /prompt is too long|input is too long for requested model/i;
const STATED_CONTEXT_COUNTS_RE = /maximum context length is (\d+) tokens[\s\S]*?(?:resulted in|you requested) (\d+) tokens/i;
const MAX_OVERFLOW_MESSAGE_CHARS = 64 * 1024;
const STATED_CONTEXT_LIMIT_RE = /maximum context length is (\d+) tokens/i;
const STATED_REQUEST_COUNT_RE = /(?:resulted in|you requested) (\d+) tokens/i;

export function claudePromptTooLongMessage(message: string): string {
message = message.slice(0, MAX_OVERFLOW_MESSAGE_CHARS);
if (CLAUDE_PROMPT_TOO_LONG_RE.test(message)) return message;
const counts = STATED_CONTEXT_COUNTS_RE.exec(message);
if (counts) return `prompt is too long: ${counts[2]} tokens > ${counts[1]} maximum`;
const limit = STATED_CONTEXT_LIMIT_RE.exec(message);
const requested = limit && STATED_REQUEST_COUNT_RE.exec(message.slice(limit.index + limit[0].length));
if (limit && requested) return `prompt is too long: ${requested[1]} tokens > ${limit[1]} maximum`;
return `prompt is too long: ${message}`;
}

Expand Down
11 changes: 7 additions & 4 deletions src/server/responses/core-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,13 @@ export function bindRouteReasoningReplayScope(args: {
if (owner) parsed._nativeReasoningBoundOwner = owner;
else delete parsed._nativeReasoningBoundOwner;
if (parsed._nativeReasoningReplay?.size) {
const rejected = new Set<string>();
for (const [blob, tag] of parsed._nativeReasoningReplay) {
if (!nativeReasoningTagMatches(nativeReasoningTag(owner, blob), tag)) {
stripNativeReasoningBlob(parsed, blob);
rejected.add(blob);
}
}
if (rejected.size) stripNativeReasoningBlobs(parsed, rejected);
}
const continuationDestinationIdentity = providerContinuationDestinationIdentity(parsed, provider);
const continuationOwner = providerContinuationOwnerFromReplayIdentity(
Expand Down Expand Up @@ -169,11 +171,11 @@ export function bindRouteReasoningReplayScope(args: {
bindProviderContinuationForRoute(parsed, continuationOwner);
}

function stripNativeReasoningBlob(parsed: OcxParsedRequest, blob: string): void {
function stripNativeReasoningBlobs(parsed: OcxParsedRequest, rejected: ReadonlySet<string>): void {
const input = (parsed._rawBody as { input?: unknown } | undefined)?.input;
if (Array.isArray(input)) {
for (const item of input) {
if (item && typeof item === "object" && item.type === "reasoning" && item.encrypted_content === blob) {
if (item && typeof item === "object" && item.type === "reasoning" && rejected.has(item.encrypted_content)) {
delete item.encrypted_content;
delete item.id;
}
Expand All @@ -187,7 +189,8 @@ function stripNativeReasoningBlob(parsed: OcxParsedRequest, blob: string): void
const item: unknown = JSON.parse(part.signature);
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const reasoning = item as Record<string, unknown>;
if (reasoning.type !== "reasoning" || reasoning.encrypted_content !== blob) continue;
if (reasoning.type !== "reasoning" || typeof reasoning.encrypted_content !== "string"
|| !rejected.has(reasoning.encrypted_content)) continue;
delete reasoning.encrypted_content;
delete reasoning.id;
part.signature = JSON.stringify(reasoning);
Expand Down
2 changes: 1 addition & 1 deletion structure/runtime.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Runtime

The minute sweep checks persisted activation deadlines locally; only missing deadlines trigger metadata discovery. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract). Codex control-plane ids are defined in the dependency-free `src/codex/control-plane-models.ts`; `src/router.ts` routes these exact ids unchanged to canonical OpenAI, retaining the existing error when no OpenAI provider is enabled. They do not widen the public native roster. Catalog requests and scheduled source refreshes follow the [nonblocking catalog observation contract](catalog.md#shared-catalog). Async runtime persistence rejects observed selection/epoch changes and returns detached frozen results; bounded exec rejects promptly and releases output readers on deadline or abort, independently awaits actual child exit, and escalates SIGTERM to SIGKILL after the shared 2-second grace (Windows termination is forceful).
The minute sweep checks persisted activation deadlines locally; only missing deadlines trigger metadata discovery. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract). Codex control-plane ids are defined in the dependency-free `src/codex/control-plane-models.ts`; `src/router.ts` routes these exact ids unchanged to canonical OpenAI, retaining the existing error when no OpenAI provider is enabled. They do not widen the public native roster. Catalog requests and scheduled source refreshes follow the [nonblocking catalog observation contract](catalog.md#shared-catalog). Async runtime persistence rejects observed selection/epoch changes and returns detached frozen results; bounded exec rejects promptly and releases output readers on deadline or abort, independently awaits actual child exit, and escalates SIGTERM to SIGKILL after the shared 2-second grace (Windows termination is forceful). Claude overflow normalization examines at most 64 Ki characters, extracts stated context/request counts in separate forward scans, retains the bounded rewritten prefix, and leaves other error classifications intact; the managed native Messages SSE route follows the same contract (`tests/claude-integration/claude-prompt-too-long.test.ts`).

## Bun runtime and test runner

Expand Down
2 changes: 1 addition & 1 deletion structure/transports/responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ again held its entry for the life of the process.
A replayed compaction item carries an `encrypted_content` blob only its minting backend can decode, and the client replays it on every later turn. The proxy's own `ocx1:` envelopes are transparent base64, so they always lower to plain user messages. A
native blob is relayed only when there is no known serving-identity mismatch and the destination is known to decode native blobs — the canonical ChatGPT forward surface, the official OpenAI API, or a provider with the explicit
`decodesNativeCompactionBlobs` capability. The destination gate alone is insufficient because more than one backend, including OpenAI and xAI, mints native blobs: a destination can decode its own blob without being able to decode the previous backend's.
The same serving-identity mismatch signal therefore strips reasoning `encrypted_content` and degrades native compaction blobs through the existing opaque-note path. Claude-native reasoning adds a per-blob tag check at every route binding: only the
The same serving-identity mismatch signal therefore strips reasoning `encrypted_content` and degrades native compaction blobs through the existing opaque-note path. Claude-native reasoning batches rejected blob tags into one scrub pass: only the
matching durable endpoint and generation-bearing credential retain raw and parsed carriers. Without recorded thread identity, the destination gate remains. Noncanonical forward providers receive no caller credentials and may point at any backend. On any
other routed destination the blob also degrades to the same opaque note the bridged parser uses, because forwarding it there fails the turn and the item outlives the failure in the client transcript, repeating on every later turn including the compaction
turn the proxy itself drives. With `store: false`, request sanitization strips ids from every input item, including compact-wire items, matching codex-rs (`core/src/client.rs:918-925`). Compact-wire items remain exempt from response-side field backfill.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,60 @@ describe("Claude native reasoning provenance", () => {
expect(nativeReasoningOwnerForRoute({ provider: oauth, oauthCredentialSnapshot: { accountId: "slot" } })).toBeUndefined();
});

test("rejects many distinct blobs in one history pass and preserves valid replay until a route switch", () => {
const selected = provider();
const owner = nativeReasoningOwnerForRoute({ provider: selected });
const payload = turn();
for (let i = 0; i < 128; i++) {
const blob = `ciphertext-${i}`;
payload.messages.push({ role: "assistant", content: [
{ type: "thinking", thinking: "Visible plan.", signature: encodeReasoningEnvelope({ nat: {
enc: blob, model: MODEL, id: `rs_${i}`, tag: i === 0 ? nativeReasoningTag(owner, blob)! : "invalid",
} }) },
{ type: "text", text: "Done." },
] } as typeof payload.messages[number]);
}
const translated = anthropicToResponsesTranslation(payload);
const parsed = parseRequest(translated.body);
parsed._nativeReasoningReplay = translated.nativeReasoningReplay;
expect(parsed._nativeReasoningReplay?.size).toBe(128);
let signatureReads = 0;
let carriers = 0;
for (const message of parsed.context.messages) {
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
for (const part of message.content) {
if (part.type !== "thinking" || typeof part.signature !== "string") continue;
carriers++;
let value = part.signature;
Object.defineProperty(part, "signature", {
enumerable: true, configurable: true,
get() { signatureReads++; return value; },
set(next: string) { value = next; },
});
}
}
expect(carriers).toBe(128);
const bind = (target: OcxProviderConfig) => {
signatureReads = 0;
bindRouteReasoningReplayScope({ parsed, providerName: "a", provider: target, adapterName: "openai-responses" });
expect(signatureReads).toBeLessThanOrEqual(2 * carriers);
};
bind(selected);
for (const history of [parsed._rawBody, parsed.context]) {
const encoded = JSON.stringify(history);
expect(encoded).toContain("ciphertext-0");
expect(encoded).not.toContain("ciphertext-1");
}
bind(provider(undefined, "key-b"));
bind(selected);
for (const history of [parsed._rawBody, parsed.context]) {
const encoded = JSON.stringify(history);
expect(encoded).not.toContain("ciphertext-");
expect(encoded).not.toContain("rs_127");
expect(encoded).toContain("Visible plan.");
}
});

test("OAuth generation changes strip both replay carriers while the same generation keeps them", () => {
const oauth = { ...provider(), authMode: "oauth" } as OcxProviderConfig;
const minted = nativeReasoningOwnerForRoute({ provider: oauth, oauthCredentialSnapshot: { accountId: "slot", generation: 1 } })!;
Expand Down
22 changes: 22 additions & 0 deletions tests/claude-integration/claude-prompt-too-long.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ describe("claudePromptTooLongMessage", () => {
expect(CLAUDE_CODE_GAP_RE.test(message)).toBe(false);
});

test("bounds repeated context prefixes and scans a request count only after the first limit", () => {
const repeated = "maximum context length is 1 tokens; ".repeat(4_000);
expect(claudePromptTooLongMessage(repeated)).toBe(`prompt is too long: ${repeated.slice(0, 64 * 1024)}`);
expect(claudePromptTooLongMessage("you requested 99 tokens; maximum context length is 10 tokens"))
.not.toContain("99 tokens > 10 maximum");
expect(claudePromptTooLongMessage("maximum context length is 10 tokens; maximum context length is 20 tokens; you requested 30 tokens"))
.toBe("prompt is too long: 30 tokens > 10 maximum");
});

test("leaves Anthropic wording unchanged", () => {
for (const text of ["prompt is too long: 1000 tokens > 900 maximum", "Input is too long for requested model."]) {
expect(claudePromptTooLongMessage(text)).toBe(text);
Expand Down Expand Up @@ -223,6 +232,19 @@ describe("native Messages lane", () => {
expect(data.error.message).toBe("prompt is too long: Your request exceeded model token limit: 262144");
});

test("bounds oversized overflow messages on the managed native SSE route", async () => {
const message = "maximum context length is 1 tokens; ".repeat(4_000);
const frame = `event: error\ndata: ${JSON.stringify({ type: "error", error: {
type: "invalid_request_error", message,
} })}\n\n`;
const { response, text } = await send(nativeConfig(() => new Response(frame, {
headers: { "content-type": "text/event-stream" },
})), true);
expect(response.status).toBe(200);
const data = JSON.parse(text.split("\n").find(line => line.startsWith("data:"))!.slice(5));
expect(data.error.message).toBe(`prompt is too long: ${message.slice(0, 64 * 1024)}`);
});

test("stall detection still times raw upstream bytes, not whole rewritten frames", async () => {
// One large delta frame arrives in fragments over ~1.6s with a 1s stall limit. Each fragment
// must reset the deadline; a frame-buffering rewrite placed before the tap would not.
Expand Down
Loading