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
8 changes: 5 additions & 3 deletions config/quality/quality-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@
"tightenSlack": 5
},
"coverage.functions": {
"value": 86.44,
"value": 86.42,
"direction": "up",
"tightenSlack": 5
"tightenSlack": 5,
"_rebaseline_2026_07_17_combo_recovery_hints": "86.44 -> 86.42 (-0.02). PR #7625: adds failureTracker.ts with new functions (+2 function definitions). Coverage denominator grew by 2 functions; numerator unchanged (the 8 coverage shards do not exercise failureTracker.ts). Legitimate drift from feature addition, not regression. Tighten via --require-tighten next cycle."
},
"coverage.branches": {
"value": 78.1,
Expand Down Expand Up @@ -164,10 +165,11 @@
"dedicatedGate": true
},
"zizmorFindings": {
"value": 175,
"value": 176,
"_rebaseline_2026_07_17_v3849_release": "169 -> 175 (+6). Cycle workflow drift (v3.8.48/v3.8.49): npm-publish.yml (new, WS1.3 #7092), electron-release.yml, nightly-compat.yml, nightly-release-green.yml, CI restructures (#7501 full-history base fetch, #7355 main-green, #7202 merge-queue gates, Trunk/Codecov). Breakdown vs v3.8.47: +3 unpinned-uses (@vN convention, deliberate per _scanner_harden_workflows_2026_06_16), +2 cache-poisoning (artifact upload/cache in the OWN electron-release/npm-publish RELEASE workflows -- operator-controlled, not fork-PR exploitable), +1 excessive-permissions (nightly-compat.yml permissions:issues). No new template-injection/artipacked/dangerous-triggers. Measured with zizmor 1.25.2 via `node scripts/check/check-workflows.mjs --ratchet` = 175 on da3a0be69.",
"direction": "down",
"dedicatedGate": true,
"_rebaseline_2026_07_17_combo_recovery_hints": "175 -> 176 (+1). Pre-existing workflow drift on source branch (PR #7625 touches zero workflow files). Measured 176 via CI check-workflows same as unmodified upstream/main tip. No new findings from this PR's changes. Same class as _rebaseline_2026_07_17_v3849_release. Tighten via --update next cycle.",
"_rebaseline_2026_06_23_fastpath_gates": "155 -> 159 (+4). Two new jobs added to .github/workflows/quality.yml (fast-vitest, fast-unit) to run vitest + the full unit suite on the PR->release fast-path (release-acceleration plan, _tasks/release-bench/v3.8.35/PLANO-IMPLEMENTACAO.md). The +4 are unpinned-uses: actions/checkout@v7 + actions/setup-node@v6 in each of the 2 jobs — the SAME deliberate @vN convention as every other workflow (see _scanner_harden_workflows_2026_06_16). SHA-pinning only these would violate the convention. No new template-injection/artipacked/cache-poisoning. Measured locally via `npm run check:workflows -- --ratchet` = 159.",
"_rebaseline_2026_06_23_v3834_release": "152 -> 155 (+3). The 3 new unpinned-uses are in .github/workflows/nightly-release-green.yml (added by #4622 this cycle): actions/checkout@v7, actions/setup-node@v6, actions/upload-artifact@v4 — the SAME deliberate @vN convention as ci.yml's own checkout@v7/setup-node@v6 and every other workflow (see _scanner_harden_workflows_2026_06_16 + _zizmor_rebaseline_2026_06_20_ci_build_artifact_reuse). SHA-pinning only this workflow would violate the convention. The workflow-lint ratchet does NOT run on PR->release fast-gates, so it surfaced only on the release PR; measured locally via `npm run check:workflows -- --ratchet` = 155. No new template-injection/artipacked/cache-poisoning.",
"_rebaseline_2026_07_13_v3847_release_preflight": "159 -> 169 (+10). Findings from cycle-merged workflow changes: #6716 (PR gate restructure), #6781 (unit fast-path shard 2->4), #6788 (TIA tsx loader split), #6881 (electron-updater latest.yml manifests in release assets) — same deliberate @vN unpinned-uses convention as prior rebaselines; no new template-injection/artipacked/cache-poisoning classes. Measured via `npm run check:workflows -- --ratchet` = 169 on the v3.8.47 release pre-flight."
Expand Down
33 changes: 33 additions & 0 deletions open-sse/utils/streamReadinessPolicy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getRegistryEntry } from "../config/providerRegistry.ts";

type StreamReadinessBody = Record<string, unknown> | null | undefined;

export type StreamReadinessPolicyInput = {
Expand Down Expand Up @@ -34,6 +36,27 @@ function estimateBodyChars(body: StreamReadinessBody): number {
return 0;
}
}
// Official Anthropic endpoints — they have stable/quick cold starts, so no
// extra readiness bump is needed.
const OFFICIAL_CLAUDE_FORMAT_PROVIDERS = new Set(["claude", "anthropic"]);

/**
* Third-party Claude-format providers (replicas like Minimax, ZAI,
* bailian-coding-plan, agentrouter, wafer) inherit Anthropic's stream shape
* but their reasoning warm-ups run significantly longer than first-party
* claude/anthropic — enough that a default 80s readiness window 504s before
* the upstream emits its first non-ping event. The `format: "claude"` entry
* in the registry is the single source of truth for "this provider routes
* through the Claude translator", so use it to bump the budget instead of
* hand-curating an allowlist that drifts every time a new replica registers.
*/
function isClaudeFormatReasoningProvider(provider?: string | null): boolean {
if (!provider) return false;
const normalized = provider.toLowerCase();
if (OFFICIAL_CLAUDE_FORMAT_PROVIDERS.has(normalized)) return false;
const entry = getRegistryEntry(normalized);
return entry?.format === "claude";
}

function isCodexGpt5x(provider?: string | null, model?: string | null): boolean {
const normalizedProvider = (provider || "").toLowerCase();
Expand Down Expand Up @@ -125,6 +148,16 @@ export function resolveStreamReadinessTimeout(
reasons.push("codex_gpt_5_5_large_responses");
}

// Third-party Claude-format replicas (Minimax M2.7/M3, ZAI, bailian,
// agentrouter, wafer, …) run long reasoning warm-ups before emitting the
// first SSE event — enough that the default 80s readiness window 504s before
// the upstream speaks. Mirror the codex_gpt_5_5_high_reasoning bump so this
// class of provider cannot be misidentified as a stalled connection.
if (isClaudeFormatReasoningProvider(input.provider) && !codexHighReasoning) {
timeoutMs += 30_000;
reasons.push("claude_format_heavy_reasoning");
}

timeoutMs = Math.min(timeoutMs, maxTimeoutMs);
if (timeoutMs === baseTimeoutMs) reasons.push("base");

Expand Down
105 changes: 105 additions & 0 deletions tests/unit/stream-readiness-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,108 @@ test("preserves zero timeout so readiness checks can be disabled", () => {
assert.equal(result.timeoutMs, 0);
assert.deepEqual(result.reasons, ["disabled"]);
});

test("bumps small requests to third-party Claude-format replicas (Minimax M3, ZAI, bailian, agentrouter) — guards against #3825-class false 504s on long reasoning warm-ups", () => {
// Provider registry lists Minimax with `format: "claude"` — the readiness budget
// must fire UNCONDITIONALLY for those replicas, like the codex_gpt_5_5_high
// bump, because their reasoning warm-ups routinely exceed the default 80s window.
const result = resolveStreamReadinessTimeout({
baseTimeoutMs: 80_000,
provider: "minimax",
model: "MiniMax-M3",
body: { messages: items(3), tools: tools(2) },
});

assert.equal(result.timeoutMs, 110_000);
assert.ok(
result.reasons.includes("claude_format_heavy_reasoning"),
`expected claude_format_heavy_reasoning in reasons, got ${JSON.stringify(result.reasons)}`
);
});

test("bumps ZAI (claude-format replica) readiness budget the same way", () => {
const result = resolveStreamReadinessTimeout({
baseTimeoutMs: 80_000,
provider: "zai",
model: "GLM-5",
body: { messages: items(3), tools: tools(2) },
});

assert.equal(result.timeoutMs, 110_000);
assert.ok(result.reasons.includes("claude_format_heavy_reasoning"));
});

test("does NOT bump official Anthropic first-party providers (claude/anthropic) — they have stable cold starts", () => {
const claudeResult = resolveStreamReadinessTimeout({
baseTimeoutMs: 80_000,
provider: "claude",
model: "claude-opus-4.5",
body: { messages: items(3), tools: tools(2) },
});

const anthropicResult = resolveStreamReadinessTimeout({
baseTimeoutMs: 80_000,
provider: "anthropic",
model: "claude-sonnet-4.5",
body: { messages: items(3), tools: tools(2) },
});

assert.equal(claudeResult.timeoutMs, 80_000);
assert.equal(anthropicResult.timeoutMs, 80_000);
assert.ok(!claudeResult.reasons.includes("claude_format_heavy_reasoning"));
assert.ok(!anthropicResult.reasons.includes("claude_format_heavy_reasoning"));
});

test("does NOT bump OpenAI / non-Claude providers", () => {
const result = resolveStreamReadinessTimeout({
baseTimeoutMs: 80_000,
provider: "openai",
model: "gpt-5",
body: { messages: items(3), tools: tools(2) },
});

assert.equal(result.timeoutMs, 80_000);
assert.ok(!result.reasons.includes("claude_format_heavy_reasoning"));
});

test("does NOT double-bump when codex-high reasoning and Claude-format replica both match", () => {
// Belt-and-braces guard: even if someone extends the codex detection to
// Claude-format providers later, the readiness bump must not stack.
const result = resolveStreamReadinessTimeout({
baseTimeoutMs: 80_000,
provider: "minimax",
model: "MiniMax-M3-high",
body: { messages: items(3), tools: tools(2), reasoning_effort: "high" },
});

// Should be bumped by exactly one reason — claude_format_heavy_reasoning —
// because minimax is not a codex provider, the codex_* path never fires.
assert.equal(result.timeoutMs, 110_000);
assert.ok(result.reasons.includes("claude_format_heavy_reasoning"));
assert.ok(!result.reasons.includes("codex_gpt_5_5_high_reasoning"));
});

test("caps Claude-format replica bump at the configured maxTimeoutMs", () => {
const result = resolveStreamReadinessTimeout({
baseTimeoutMs: 80_000,
maxTimeoutMs: 100_000,
provider: "minimax",
model: "MiniMax-M3",
body: { messages: items(500), tools: tools(20), instructions: "x".repeat(800_000) },
});

assert.equal(result.timeoutMs, 100_000);
assert.ok(result.reasons.includes("claude_format_heavy_reasoning"));
});

test("treats unknown provider names as non-Claude-format (no false positives)", () => {
const result = resolveStreamReadinessTimeout({
baseTimeoutMs: 80_000,
provider: "not-a-real-provider",
model: "anything",
body: { messages: items(3) },
});

assert.equal(result.timeoutMs, 80_000);
assert.ok(!result.reasons.includes("claude_format_heavy_reasoning"));
});