Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0e967d4
feat(api): abort signal and timeout support for anthropic, anthropic-…
easonliang28 Aug 19, 2026
dfbc1d8
ci: re-run e2e-mock on flaky restart-persistence (uses openrouter; th…
easonliang28 Aug 19, 2026
81a75d4
fix(api): address CodeRabbit review on anthropic family abort handling
easonliang28 Aug 19, 2026
af279dd
fix(api): type xAI streaming request body with the OpenAI SDK type
easonliang28 Aug 20, 2026
3aec038
Merge branch 'main' into feat/abort-r1-anthropic-family
easonLiangWorldedtech Aug 30, 2026
afed085
fix(api): detach external abort listeners and cover timeoutMs 0
easonliang28 Sep 2, 2026
e290664
Merge remote-tracking branch 'upstream/main' into feat/abort-r1-anthr…
easonliang28 Sep 2, 2026
8afead4
refactor(api): map xai tool_choice to Responses API shape instead of …
easonliang28 Sep 2, 2026
9b23d30
fix(api): flatten xai allowed_tools entries for the Responses API
easonliang28 Sep 2, 2026
8ead9bb
Merge branch 'main' into feat/abort-r1-anthropic-family
edelauna Sep 3, 2026
a741615
test(api): cover abort-bridge and request-body mutation gaps
easonliang28 Sep 5, 2026
7ee64dd
Merge remote-tracking branch 'upstream/main' into feat/abort-r1-anthr…
easonliang28 Sep 5, 2026
262ec50
fix(api): remove dead code flagged by the mutation gate in anthropic,…
easonliang28 Sep 5, 2026
3d92d06
fix(api): attach anthropic abort signal via spread to drop the surviv…
easonliang28 Sep 5, 2026
895660b
fix(api): detach external abort listeners when anthropic/minimax requ…
easonliang28 Sep 5, 2026
fca60ad
Merge branch main into feat/abort-r1-anthropic-family
easonliang28 Sep 6, 2026
e8247a9
Merge branch 'main' into feat/abort-r1-anthropic-family
easonLiangWorldedtech Sep 15, 2026
de668d3
Merge remote-tracking branch 'upstream/main' into fix-pr-1293
easonliang28 Sep 27, 2026
ac0e151
test: assert non-cached Anthropic path rejects with AbortError on ext…
easonliang28 Sep 27, 2026
287d8b5
Merge branch 'main' into feat/abort-r1-anthropic-family
easonLiangWorldedtech Sep 28, 2026
b45fdf7
Merge remote-tracking branch 'upstream/main' into feat/abort-r1-anthr…
Oct 2, 2026
fe3c75c
Merge commit 'refs/shapes/upstream-main' into feat/abort-r1-anthropic…
Oct 2, 2026
9006f71
Merge branch 'main' into feat/abort-r1-anthropic-family
easonLiangWorldedtech Oct 5, 2026
1d75329
Merge upstream main (285f6687a) into feat/abort-r1-anthropic-family
easonliang28 Oct 13, 2026
8521fa8
Address review: shared abort bridge, canonical xAI aborts, beta join,…
easonliang28 Oct 13, 2026
8e62457
Merge upstream/main (58341d882) into feat/abort-r1-anthropic-family
easonliang28 Oct 14, 2026
67b5966
chore(api): remove Stryker suppression comments from touched files
easonliang28 Oct 14, 2026
888cefe
fix(api): normalize stream-iteration aborts in the xAI responses path
easonliang28 Oct 14, 2026
0f23d98
fix(api): reject non-finite and cap overflowing mergeAbortSignalAndTi…
easonliang28 Oct 14, 2026
f160a99
fix(api): round fractional timeouts before AbortSignal.timeout
easonliang28 Oct 14, 2026
efb3b4f
test(api): assert the rounded delay reaching AbortSignal.timeout
easonliang28 Oct 14, 2026
81f3e56
fix(anthropic-family): forward timeoutMs only when the SDK can take it
Oct 21, 2026
f549adc
Merge remote-tracking branch 'upstream/main' into feat/abort-r1-anthr…
easonLiangWorldedtech Oct 22, 2026
08556c7
style: apply repo Prettier to the merge resolution
easonLiangWorldedtech Oct 22, 2026
6f02b7c
refactor(api): share one SDK timeout conversion across the anthropic …
easonLiangWorldedtech Oct 22, 2026
d1c4636
style: apply repo Prettier to the timeout helper import
easonLiangWorldedtech Oct 22, 2026
7043629
test(providers): pin timeout rounding and the timer cap across the an…
easonLiangWorldedtech Oct 23, 2026
ca3db83
fix(api): lint request timeouts and spec loops, fix existing violations
edelauna Oct 24, 2026
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
26 changes: 25 additions & 1 deletion packages/config-eslint/api-provider-hygiene.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
// Paths are relative to the `src` package, where this config is loaded.

const sharedAbortHelpers =
"mergeAbortSignalAndTimeout|mergeAbortSignals|throwIfAborted|isRequestAborted|createAbortError|rejectOnAbort|resolveModelWithAbort"
"mergeAbortSignalAndTimeout|mergeAbortSignals|throwIfAborted|isRequestAborted|createAbortError|rejectOnAbort|resolveModelWithAbort|bridgeExternalAbortSignal|hasUsableTimeout|toSdkTimeoutMs"

// Names that usually hold a secret or an endpoint that can embed credentials.
// `cacheKey` and `contextTokens` must not match.
const sensitiveName = "^(url|apiKey|secret|password)$|(Url|URL|ApiKey|Secret|Token)$"

// A per-request `timeoutMs` from CompletePromptOptions. `this.timeoutMs` is the client-level
// timeout from getApiRequestTimeout, which is already valid, so it must not match.
const rawRequestTimeout =
":matches(Identifier[name='timeoutMs'], MemberExpression[property.name='timeoutMs']:not([object.type='ThisExpression']), ChainExpression[expression.property.name='timeoutMs'])"

export const apiProviderHygieneSelectors = [
{
selector: `:matches(FunctionDeclaration, VariableDeclarator)[id.name=/^(${sharedAbortHelpers})$/]`,
Expand All @@ -20,6 +25,16 @@ export const apiProviderHygieneSelectors = [
message:
"Use mergeAbortSignalAndTimeout for a variable delay. AbortSignal.timeout throws on Infinity and fractions, and clamps large values to 1 ms.",
},
{
selector: `:matches(Property[key.name='timeout'], AssignmentExpression[left.property.name='timeout']) > ${rawRequestTimeout}`,
message:
"Do not forward a request timeoutMs as is. A SDK treats 0 as an immediate abort and throws on negative, non-finite or fractional values. Use hasUsableTimeout and toSdkTimeoutMs.",
},
{
selector: "Literal[value=2147483647]",
message:
"Do not write the maximum timer delay by hand. Use MAX_TIMEOUT_MS or toSdkTimeoutMs from providers/utils/abort-signal.",
},
{
selector: `CallExpression[callee.object.name='console'] TemplateLiteral > :matches(Identifier[name=/${sensitiveName}/], MemberExpression[property.name=/${sensitiveName}/])`,
message:
Expand All @@ -35,3 +50,12 @@ export const apiProviderHygieneConfig = {
"no-restricted-syntax": ["error", ...apiProviderHygieneSelectors],
},
}

// Spec files under src/api. A loop of expects in one test stops at the first failing case and hides the rest.
export const apiProviderTestSelectors = [
{
selector: "ForOfStatement[right.type='ArrayExpression']:has(CallExpression[callee.name='expect'])",
message:
"Use it.each for repeated cases. A loop of expects in one test stops at the first failing case and does not name it.",
},
]
88 changes: 87 additions & 1 deletion packages/config-eslint/api-provider-hygiene.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, it } from "node:test"
import { Linter } from "eslint"
import typescriptEslint from "typescript-eslint"

import { apiProviderHygieneConfig } from "./api-provider-hygiene.js"
import { apiProviderHygieneConfig, apiProviderTestSelectors } from "./api-provider-hygiene.js"

const linter = new Linter({ configType: "flat" })
const config = [{ files: ["**/*.ts"], languageOptions: { parser: typescriptEslint.parser } }, apiProviderHygieneConfig]
Expand Down Expand Up @@ -88,3 +88,89 @@ describe("api provider hygiene: sensitive values in logs", () => {
})
}
})

describe("api provider hygiene: request timeout forwarding", () => {
for (const code of [
"create(body, { timeout: options.timeoutMs })",
"create(body, { timeout: options?.timeoutMs })",
"create(body, { signal, timeout: timeoutMs })",
"requestOptions.timeout = options.timeoutMs",
"requestOptions.timeout = timeoutMs",
]) {
it(`flags: ${code}`, () => {
assert.equal(lint(code).length, 1)
})
}

for (const code of [
"new OpenAI({ timeout: this.timeoutMs })",
"requestOptions.timeout = toSdkTimeoutMs(timeoutMs)",
"create(body, { timeout: toSdkTimeoutMs(options.timeoutMs) })",
"if (hasUsableTimeout(timeoutMs)) requestOptions.timeout = toSdkTimeoutMs(timeoutMs)",
"const timeoutMs = options?.timeoutMs",
"mergeAbortSignalAndTimeout(signal, options?.timeoutMs)",
]) {
it(`allows: ${code}`, () => {
assert.deepEqual(lint(code), [])
})
}
})

describe("api provider hygiene: timeout helpers", () => {
for (const code of [
"const MAX = 2147483647",
"Math.min(Math.ceil(timeoutMs), 2_147_483_647)",
"function hasUsableTimeout(t?: number) { return true }",
"const toSdkTimeoutMs = (t: number) => t",
"function bridgeExternalAbortSignal() {}",
]) {
it(`flags: ${code}`, () => {
assert.equal(lint(code).length, 1)
})
}

it("allows importing the timeout helpers", () => {
assert.deepEqual(
lint('import { hasUsableTimeout, toSdkTimeoutMs } from "./utils/abort-signal"\ntoSdkTimeoutMs(1)'),
[],
)
})

it("allows the literal in the helper module", () => {
assert.deepEqual(lint("export const MAX_TIMEOUT_MS = 2_147_483_647", "api/providers/utils/abort-signal.ts"), [])
})
})

describe("api provider hygiene: spec loops", () => {
const testLinter = new Linter({ configType: "flat" })
const testConfig = [
{ files: ["**/*.ts"], languageOptions: { parser: typescriptEslint.parser } },
{
files: ["api/**/__tests__/**/*.spec.ts"],
rules: { "no-restricted-syntax": ["error", ...apiProviderTestSelectors] },
},
]
const lintSpec = (code) =>
testLinter
.verify(code, testConfig, { filename: "api/providers/__tests__/example.spec.ts" })
.map((m) => m.message)

for (const code of [
"it('x', () => { for (const v of [0, -5]) { expect(f(v)).toBe(1) } })",
"it('x', async () => { for (const v of [0, -5]) { await run(v); expect(f(v)).toBe(1) } })",
]) {
it(`flags a loop of expects over a literal array: ${code}`, () => {
assert.equal(lintSpec(code).length, 1)
})
}

for (const code of [
"it.each([0, -5])('x %s', (v) => { expect(f(v)).toBe(1) })",
"it('x', () => { for (const v of [0, -5]) { run(v) } })",
"it('x', () => { for (const v of cases) { expect(f(v)).toBe(1) } })",
]) {
it(`allows: ${code}`, () => {
assert.deepEqual(lintSpec(code), [])
})
}
})
Loading
Loading