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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ _In development — bullets added per PR; finalized at release._
- **fix(pricing): align Claude Code (`cc`) pricing with current Anthropic per-MTok rates** — the `cc` provider block in the default pricing table had stale numbers across every Claude 4.x family entry — most visibly, `claude-opus-4-5-20251101` was billed at the deprecated Opus 4.1 rate (`input $15` / `output $75`), and `claude-haiku-4-5-20251001` was at half the current Haiku 4.5 rate. The `cached` (cache hit) and `cache_creation` (5-minute cache write) multipliers were also off across Opus 4.6/4.7/4.8, Sonnet 4.5/4.6, Haiku 4.5, and Fable 5. All eight entries now match the rates Anthropic publishes (input, 5m cache write at 1.25x input, cache hit at 0.1x input, output; reasoning billed at the output rate), so cost accounting on the dashboard and per-request usage events stop under- or over-reporting Claude Code spend. (thanks @chulanpro5)
- **fix(executors): sanitize Anthropic-shape content parts before GitHub Copilot `/chat/completions`** — Claude models on GitHub Copilot driven from clients like Cursor IDE (e.g. `gh/claude-sonnet-4.6`) failed with `Provider returned error: type has to be either 'image_url' or 'text' (reset after 30s)` because the client passed through Anthropic-shape content parts (`tool_use`, `tool_result`, `thinking`) untouched, and the Copilot chat-completions endpoint only accepts `text`/`image_url`. `GithubExecutor.transformRequest` now serializes any unsupported part type as `text` (preserving the model's context), drops empty parts, and collapses to `null` when an assistant message's only content was tool_calls — `tool_calls` ride alongside untouched. Codex-family models still route through `/responses` unchanged. (thanks @cngznNN)
- **fix(sse):** refactor stall detection to reduce false positives on slow but progressing streams. (thanks @zakirkun)
- **fix(providers): restore specialty validator dispatch for web-cookie providers** — the generic `/models` web-cookie probe introduced in #4023 silently took precedence over the per-provider specialty validators (qwen-web, grok-web, chatgpt-web, claude-web, gemini-web, copilot-web, deepseek-web, perplexity-web, blackbox-web, muse-spark-web, t3-web, adapta-web, inner-ai). For qwen-web this regressed the body-check fix from #3958 — the generic probe accepted any 200 as valid, where the specialty validator inspects `data.user` on `/api/v2/user` to detect expired sessions that still return 200. Every other specialty web-cookie validator was equally bypassed. The dispatcher now gates the generic probe on a `WEB_COOKIE_PROVIDERS_WITH_SPECIALTY_VALIDATOR` set so providers with bespoke validators reach them again. A new `tests/unit/web-cookie-specialty-dispatch.test.ts` enforces the invariant (every override must really be a registered web-cookie provider, and qwen-web's specialty path must hit `/api/v2/user`, not `/models`).

---

Expand Down
9 changes: 6 additions & 3 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,15 @@
"_rebaseline_2026_06_20_1449_1444_test_route": "Re-baseline providers test route.ts 842->887: combined growth of sibling fixes #1449 (bound OAuth connection-test probe with a timeout) + #1444 (label a deactivated account distinctly from a revoked token), both at the same connection-test chokepoint. Cohesive route handler; not extractable without hiding the test flow.",
"_rebaseline_2026_06_20_1409_1294_models": "Re-baseline src/lib/db/models.ts 1184->1221: combined growth of sibling fixes #1409 (cascade-delete orphaned model aliases when a provider is removed) + #1294 (persist max_input_tokens/max_output_tokens on custom models), both adding CRUD at the existing models domain module. Cohesive db module; not extractable.",
"_rebaseline_2026_06_20_4389_thinking_toolchoice": "Re-baseline base.ts 1387->1399 (#4389): tool_choice-forced thinking guard at the existing Claude wire-image injection chokepoint (effThinking gate avoids the Anthropic 400 when tool_choice forces a tool). Cohesive guard; structural shrink tracked in #3501.",
"_rebaseline_2026_06_20_qwen_web_specialty_dispatch": "Re-baseline validation.ts 4518->4556 (+38) — restore-green dispatch fix for web-cookie providers regressed by #4023. The generic web-cookie /models probe added in #4023 silently took precedence over the per-provider specialty validators (qwen-web/grok-web/chatgpt-web/...), regressing the qwen-web body-check fix from #3958 and the equivalent guards for the other 12 specialty web-cookie providers. Fix gates the generic probe on a Set of providers that have a specialty validator below (WEB_COOKIE_PROVIDERS_WITH_SPECIALTY_VALIDATOR), plus a single test-export const used by the invariant test (tests/unit/web-cookie-specialty-dispatch.test.ts). Cohesive guard at the validateProviderApiKey dispatch chokepoint; not extractable without splitting the dispatcher. Structural shrink for validation.ts tracked in #3501.",
"_rebaseline_2026_06_20_4440_cc_pricing": "Re-baseline pricing.ts 1620->1623 (+3) — drift from PR #4440 (align Claude Code pricing with Anthropic) which was merged with --admin while file-size was already over. Pure pricing data rows; not extractable.",
"_rebaseline_2026_06_20_4443_reasoning_effort": "Re-baseline base.ts 1399->1407 (+8) — drift from PR #4443 (granular reasoning_effort handling for Claude models on Copilot) which was merged with --admin while file-size was already over. Cohesive per-model preserve at the existing serialization chokepoint; not extractable. Structural shrink tracked in #3501.",
"cap": 800,
"frozen": {
"open-sse/translator/request/openai-to-kiro.ts": 807,
"open-sse/config/providerRegistry.ts": 4731,
"open-sse/executors/antigravity.ts": 1687,
"open-sse/executors/base.ts": 1399,
"open-sse/executors/base.ts": 1407,
"open-sse/executors/chatgpt-web.ts": 2870,
"open-sse/executors/claude-web.ts": 1057,
"open-sse/executors/codex.ts": 1449,
Expand Down Expand Up @@ -175,7 +178,7 @@
"src/lib/evals/evalRunner.ts": 961,
"src/lib/memory/retrieval.ts": 1171,
"src/lib/modelsDevSync.ts": 934,
"src/lib/providers/validation.ts": 4518,
"src/lib/providers/validation.ts": 4556,
"src/lib/tailscaleTunnel.ts": 1202,
"src/lib/usage/callLogs.ts": 975,
"src/lib/usage/providerLimits.ts": 949,
Expand All @@ -184,7 +187,7 @@
"src/shared/components/RequestLoggerV2.tsx": 1287,
"src/shared/components/analytics/charts.tsx": 1558,
"src/shared/constants/cliTools.ts": 875,
"src/shared/constants/pricing.ts": 1620,
"src/shared/constants/pricing.ts": 1623,
"src/shared/constants/providers.ts": 3242,
"src/shared/constants/sidebarVisibility.ts": 1100,
"src/shared/services/cliRuntime.ts": 1090,
Expand Down
42 changes: 40 additions & 2 deletions src/lib/providers/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3992,6 +3992,33 @@ export async function validateWebCookieProvider({
}
}

// Web-cookie providers that ship a per-provider specialty validator below
// (registered in SPECIALTY_VALIDATORS inside validateProviderApiKey). They
// must NOT fall into the generic /models web-cookie probe, which would
// regress per-provider body-check guards (see #3958 for qwen-web).
//
// Keep this set in sync with the SPECIALTY_VALIDATORS keys that are also in
// WEB_COOKIE_PROVIDERS — the test in
// `tests/unit/web-cookie-specialty-dispatch.test.ts` enforces the invariant.
const WEB_COOKIE_PROVIDERS_WITH_SPECIALTY_VALIDATOR = new Set<string>([
"adapta-web",
"blackbox-web",
"chatgpt-web",
"claude-web",
"copilot-web",
"deepseek-web",
"gemini-web",
"grok-web",
"inner-ai",
"muse-spark-web",
"perplexity-web",
"qwen-web",
"t3-web",
]);

export const __testing__WEB_COOKIE_PROVIDERS_WITH_SPECIALTY_VALIDATOR =
WEB_COOKIE_PROVIDERS_WITH_SPECIALTY_VALIDATOR;

export async function validateProviderApiKey({ provider, apiKey, providerSpecificData = {} }: any) {
const requiresApiKey = !providerAllowsOptionalApiKey(provider);
const isLocal = isLocalProvider(provider);
Expand All @@ -4000,8 +4027,19 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
return { valid: false, error: "Provider and API key required", unsupported: false };
}

// Web-cookie providers (session-based authentication)
if (WEB_COOKIE_PROVIDERS[provider]) {
// Web-cookie providers (session-based authentication).
//
// Skip the generic /models probe when the provider has its own specialty
// validator below (in SPECIALTY_VALIDATORS) — the specialty validator hits
// the provider's real session endpoint (e.g. qwen-web → /api/v2/user) and
// inspects the body for a real user/session marker, which the generic probe
// cannot do. Without this guard, the generic dispatch silently returns
// `valid:true` on any 200 (regressing the qwen-web body-check fix from #3958
// and the equivalent guards for the other web-cookie providers).
if (
WEB_COOKIE_PROVIDERS[provider] &&
!WEB_COOKIE_PROVIDERS_WITH_SPECIALTY_VALIDATOR.has(provider)
) {
try {
return await validateWebCookieProvider({ provider, apiKey, providerSpecificData });
} catch (error: any) {
Expand Down
64 changes: 64 additions & 0 deletions tests/unit/web-cookie-specialty-dispatch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Regression guard for the dispatch order in validateProviderApiKey:
// PR #4023 added a generic web-cookie /models probe that silently took
// precedence over per-provider specialty validators, regressing the qwen-web
// body-check fix from #3958 (and the equivalent guards for every other
// specialty web-cookie provider). The fix gates the generic probe on a Set
// of providers that have a specialty validator. This test enforces the
// invariant: every key in that Set must really be served by the specialty
// dispatch, never the generic probe.
//
// Drift mode (e.g. someone adds a new specialty web-cookie validator):
// → the new key must also be added to
// WEB_COOKIE_PROVIDERS_WITH_SPECIALTY_VALIDATOR — this test will fail
// until it is, surfacing the regression at PR review time.

import test from "node:test";
import assert from "node:assert/strict";

import {
__testing__WEB_COOKIE_PROVIDERS_WITH_SPECIALTY_VALIDATOR as SPECIALTY_OVERRIDES,
validateProviderApiKey,
} from "../../src/lib/providers/validation.ts";
import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers.ts";

const originalFetch = globalThis.fetch;

test.afterEach(() => {
globalThis.fetch = originalFetch;
});

test("every override is actually a registered web-cookie provider", () => {
for (const provider of SPECIALTY_OVERRIDES) {
assert.ok(
Object.hasOwn(WEB_COOKIE_PROVIDERS, provider),
`override "${provider}" is not in WEB_COOKIE_PROVIDERS — drop it or fix the typo`
);
}
});

test("a specialty web-cookie provider does NOT fall into the generic /models probe", async () => {
// The generic probe hits "<baseUrl>/models" and accepts any non-401/403 as
// valid. For qwen-web specifically the specialty validator hits
// /api/v2/user and inspects the body for a real `user` object. We assert
// the fetch URL: if dispatch lands on the generic probe, the path ends in
// /models; the specialty path ends in /api/v2/user.
const seenUrls: string[] = [];
globalThis.fetch = (async (url: any) => {
seenUrls.push(String(url));
return new Response("{}", {
status: 200,
headers: { "content-type": "application/json" },
});
}) as typeof fetch;

await validateProviderApiKey({ provider: "qwen-web", apiKey: "qwen-token-abc" });

assert.ok(
seenUrls.some((u) => u.endsWith("/api/v2/user")),
`expected specialty validator to call /api/v2/user, got: ${seenUrls.join(", ")}`
);
assert.ok(
!seenUrls.some((u) => u.endsWith("/models")),
`specialty dispatch leaked into the generic /models probe: ${seenUrls.join(", ")}`
);
});
Loading