diff --git a/.castiron.stats.yml b/.castiron.stats.yml index 813cdae9c4..017427695d 100644 --- a/.castiron.stats.yml +++ b/.castiron.stats.yml @@ -1,8 +1,8 @@ schema_version: 1 -generation_id: a993ea45-7f4f-4d48-9eea-27e2f709ca7c -openapi_spec_hash: 1f2d498f08bdd6dacffeb41cee8f44e5 -openapi_transformed_spec_hash: 77075630a8655ccbb8d1f790f19f5731 -config_hash: 7a8181d81e1b9225f8ee6f09171d712e -codegen_sha: 6f2038ca576a63929de017d9145a842c3a09beb1 -codegen_hash: a761b2892108918f3ad49d4929a6b756fa023b3192025bbc2bbcf618dea6c13a -public_codegen_sha: 9fdcc5c60f9247fe11ec25aaa0f486033f738421 +generation_id: 4dd013a4-3586-4b2e-8d90-dd4d6b6d8e18 +openapi_spec_hash: 2c78c708cab402e8904f555026c78020 +openapi_transformed_spec_hash: e513b495f24d8119b952521ffc783d86 +config_hash: 26002a7ad4f9fdd7011b62a2da3511b1 +codegen_sha: b9a21b51f252010a29b36f9dff4118593321d48c +codegen_hash: d7afe679f4861d8aec19f61fefe2c0659d30cc557923b3614ca142d5985d5a4f +public_codegen_sha: 889f63058deba0eb0a04a593a9cf771c0674972a diff --git a/.github/workflows/castiron-custom-code-comment.yml b/.github/workflows/castiron-custom-code-comment.yml index aca3ff77fc..74f04360a2 100644 --- a/.github/workflows/castiron-custom-code-comment.yml +++ b/.github/workflows/castiron-custom-code-comment.yml @@ -182,7 +182,7 @@ jobs: if (!(typeof value === 'string' || Number.isSafeInteger(value)) || !/^[1-9][0-9]*$/.test(String(value))) throw new Error('Invalid review identity'); const identity = String(value); - if (review.state === 'PENDING' || !review.author) continue; + if (['PENDING', 'COMMENTED'].includes(review.state) || !review.author) continue; const previous = latest.get(review.author.login); if (!previous || BigInt(identity) > BigInt(previous.identity)) latest.set(review.author.login, {...review, identity}); } @@ -344,6 +344,97 @@ jobs: return; } + main-change: + name: Invalidate next queue after main advances + if: github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' + runs-on: ubuntu-latest + timeout-minutes: 5 + concurrency: + group: castiron-budget-writes-${{ github.repository_id }} + cancel-in-progress: false + queue: max + permissions: + actions: write + contents: read + pull-requests: read + statuses: write + steps: + - name: Re-evaluate live next queue after main advances + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + // Main pushes only invalidate. The normal authenticated evaluator + // and serialized publisher are the only paths that restore success. + const repo = `${context.repo.owner}/${context.repo.repo}`; + const signal = context.payload.workflow_run; + const {data: run} = await github.rest.actions.getWorkflowRun({...context.repo, run_id: signal.id}); + const {data: workflow} = await github.rest.actions.getWorkflow({ + ...context.repo, workflow_id: 'castiron-custom-code.yml', + }); + if (!Number.isSafeInteger(run.workflow_id) || run.workflow_id !== workflow.id || + workflow.path !== '.github/workflows/castiron-custom-code.yml') return; + if (run.repository.full_name !== repo || run.head_repository?.full_name !== repo || + run.event !== 'push' || run.head_branch !== 'main' || run.status !== 'completed' || + run.id !== signal.id || run.run_attempt !== signal.run_attempt || run.head_sha !== signal.head_sha || + !/^[0-9a-f]{40}$/.test(run.head_sha) || + run.path.split('@', 1)[0] !== '.github/workflows/castiron-custom-code.yml') { + throw new Error('Unexpected or superseded main signal'); + } + const {data: main} = await github.rest.git.getRef({...context.repo, ref: 'heads/main'}); + // A newer main push has its own signal. An old retry must not churn + // candidates already qualified against that newer snapshot. + if (run.head_sha !== main.object.sha) return; + const targets = new Set(); + let cursor = null; + do { + const result = await github.graphql(`query($owner:String!,$name:String!,$branch:String!,$cursor:String) { + repository(owner:$owner,name:$name) { mergeQueue(branch:$branch) { + entries(first:100,after:$cursor) { + nodes { headCommit { oid } } + pageInfo { hasNextPage endCursor } + } + } } + }`, {owner: context.repo.owner, name: context.repo.repo, branch: 'next', cursor}); + const page = result.repository.mergeQueue?.entries; + if (!page) break; + for (const entry of page.nodes) { + if (entry.headCommit === null) continue; // Unprepared entries have no statuses to invalidate. + const sha = entry.headCommit?.oid; + if (!/^[0-9a-f]{40}$/.test(sha || '')) throw new Error('Invalid live queue SHA'); + targets.add(sha); + } + if (!page.pageInfo.hasNextPage) break; + const next = page.pageInfo.endCursor; + if (!next || next === cursor) throw new Error('Invalid queue pagination'); + cursor = next; + } while (true); + const url = `${context.serverUrl}/${repo}/actions/runs/${context.runId}`; + // Invalidate the entire live next queue before slower rerun discovery. + // This also covers bootstraps whose policy pin is now stale. Ordinary + // pinned-policy candidates can regain success through normal checks. + for (const sha of targets) { + for (const name of ['Castiron next / budget-only change', 'Castiron next / custom-code budget']) { + await github.rest.repos.createCommitStatus({...context.repo, sha, context: name, state: 'pending', + description: `Main advanced; reevaluation pending. [main ${main.object.sha}]`, target_url: url}); + } + } + for (const sha of targets) { + const runs = await github.paginate(github.rest.actions.listWorkflowRuns, { + ...context.repo, workflow_id: 'castiron-custom-code.yml', head_sha: sha, event: 'merge_group', per_page: 100, + }); + const matches = runs.filter(item => item.repository.full_name === repo && + item.workflow_id === workflow.id && item.event === 'merge_group' && item.head_sha === sha && + typeof item.head_branch === 'string' && item.head_branch.startsWith('gh-readonly-queue/next/') && + item.path.split('@', 1)[0] === '.github/workflows/castiron-custom-code.yml' && + Number.isSafeInteger(item.id) && item.id > 0); + matches.sort((a, b) => b.id - a.id); + // A running evaluation will check main at publication. Do not + // restart an older completed run when a newer one is in flight. + if (matches[0]?.status === 'completed') { + await github.rest.actions.reRunWorkflow({...context.repo, run_id: matches[0].id}); + } + } + compute: name: Compute trusted custom-code report if: contains(fromJSON('["pull_request", "merge_group"]'), github.event.workflow_run.event) && (github.event.workflow_run.path == '.github/workflows/castiron-custom-code.yml' || startsWith(github.event.workflow_run.path, '.github/workflows/castiron-custom-code.yml@')) @@ -355,6 +446,7 @@ jobs: pull-requests: read outputs: selected-policy-sha: ${{ steps.select-policy.outputs.policy_sha }} + authenticated-main-sha: ${{ steps.select-policy.outputs.authenticated_main_sha }} main-snapshot: ${{ steps.select-policy.outputs.main_snapshot }} selected-base-sha: ${{ steps.select-policy.outputs.base_sha }} selected-head-sha: ${{ steps.select-policy.outputs.head_sha }} @@ -393,9 +485,11 @@ jobs: GIT_CONFIG_KEY_1: credential.https://github.com.helper GIT_CONFIG_VALUE_1: '!gh auth git-credential' run: | + trusted_main="$(git rev-parse HEAD)" + printf 'authenticated_main_sha=%s\n' "$trusted_main" >> "$GITHUB_OUTPUT" python3 -I scripts/castiron/custom_code_budget.py select-policy \ --repository "$REPOSITORY" --event-path "$GITHUB_EVENT_PATH" \ - --trusted-sha "$(git rev-parse HEAD)" \ + --trusted-sha "$trusted_main" \ --repo "$RUNNER_TEMP/castiron-policy-objects.git" \ --out "$RUNNER_TEMP/castiron-policy-selection.json" @@ -505,6 +599,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: SELECTED_POLICY_SHA: ${{ needs.compute.outputs.selected-policy-sha }} + AUTHENTICATED_MAIN_SHA: ${{ needs.compute.outputs.authenticated-main-sha }} MAIN_SNAPSHOT: ${{ needs.compute.outputs.main-snapshot }} SELECTED_BASE_SHA: ${{ needs.compute.outputs.selected-base-sha }} SELECTED_HEAD_SHA: ${{ needs.compute.outputs.selected-head-sha }} @@ -629,7 +724,7 @@ jobs: if (!(typeof value === 'string' || Number.isSafeInteger(value)) || !/^[1-9][0-9]*$/.test(String(value))) throw new Error('Invalid review identity'); const identity = String(value); - if (review.state === 'PENDING' || !review.author) continue; + if (['PENDING', 'COMMENTED'].includes(review.state) || !review.author) continue; const previous = latest.get(review.author.login); if (!previous || BigInt(identity) > BigInt(previous.identity)) latest.set(review.author.login, {...review, identity}); } @@ -680,6 +775,16 @@ jobs: const {data: target} = await github.rest.git.getRef({...context.repo, ref: 'heads/next'}); fresh = fresh && base === target.object.sha; } + // Main pushes invalidate next evaluations, even when their reviewed + // checker pin stays unchanged. Do not restore success from a run + // computed before that invalidation. This is the reporter checkout, + // not the independently reviewed (and possibly older) policy pin. + if (branch === 'next') { + const authenticatedMain = process.env.AUTHENTICATED_MAIN_SHA || ''; + const {data: currentMain} = await github.rest.git.getRef({...context.repo, ref: 'heads/main'}); + fresh = fresh && /^[0-9a-f]{40}$/.test(authenticatedMain) && + currentMain.object.sha === authenticatedMain; + } // Bootstrap may adopt only the still-current authenticated main tree. // This value comes from our selector, never from candidate artifacts. if (process.env.MAIN_SNAPSHOT) { diff --git a/.github/workflows/castiron-custom-code.yml b/.github/workflows/castiron-custom-code.yml index fc26e2e8f0..afe1536098 100644 --- a/.github/workflows/castiron-custom-code.yml +++ b/.github/workflows/castiron-custom-code.yml @@ -2,6 +2,8 @@ name: Castiron custom code on: + push: + branches: [main] pull_request: types: [opened, synchronize, reopened, ready_for_review, edited] pull_request_review: @@ -18,11 +20,11 @@ concurrency: cancel-in-progress: false env: - REPORTER_SHA256: c243015818640b1e7068605c5352fcb770d406202ee4e75316fe0ca85d06a1cc + REPORTER_SHA256: ddb15c9a162d26a6c9abf1b4fefcd80f2e685d9d255af729863258b7ecd08e2d jobs: queue-signal: - if: github.event_name == 'merge_group' || github.event_name == 'pull_request_review' + if: github.event_name == 'merge_group' || github.event_name == 'pull_request_review' || github.event_name == 'push' runs-on: ubuntu-latest timeout-minutes: 1 permissions: {} diff --git a/api_reference/openapi.transformed.yml b/api_reference/openapi.transformed.yml index 64ddf645f2..7ecd7eae4a 100644 --- a/api_reference/openapi.transformed.yml +++ b/api_reference/openapi.transformed.yml @@ -74734,7 +74734,8 @@ components: required: - mode title: Prompt cache breakpoint - description: Marks the exact end of a reusable prompt prefix. The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + description: Marks the exact end of a reusable prompt prefix. When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + x-stainless-go-constant-constructor: true ModerationMode: type: string enum: @@ -74964,7 +74965,8 @@ components: required: - mode title: Prompt cache breakpoint - description: Marks the exact end of a reusable prompt prefix. The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + description: Marks the exact end of a reusable prompt prefix. When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + x-stainless-go-constant-constructor: true InputTextContent: properties: type: @@ -74979,6 +74981,7 @@ components: description: The text input to the model. prompt_cache_breakpoint: $ref: '#/components/schemas/PromptCacheBreakpointConfig' + x-stainless-go-constant-constructor: true type: object required: - type @@ -75254,6 +75257,7 @@ components: description: The detail level of the image to be sent to the model. One of `high`, `low`, `auto`, or `original`. Defaults to `auto`. prompt_cache_breakpoint: $ref: '#/components/schemas/PromptCacheBreakpointConfig' + x-stainless-go-constant-constructor: true type: object required: - type @@ -75285,6 +75289,7 @@ components: description: The detail level of the screenshot image to be sent to the model. One of `high`, `low`, `auto`, or `original`. Defaults to `auto`. prompt_cache_breakpoint: $ref: '#/components/schemas/PromptCacheBreakpointConfig' + x-stainless-go-constant-constructor: true type: object required: - type @@ -75322,6 +75327,7 @@ components: The content of the file to be sent to the model. prompt_cache_breakpoint: $ref: '#/components/schemas/PromptCacheBreakpointConfig' + x-stainless-go-constant-constructor: true file_url: type: string format: uri @@ -77314,6 +77320,7 @@ components: anyOf: - $ref: '#/components/schemas/PromptCacheBreakpointParam' - type: 'null' + x-stainless-go-constant-constructor: true type: object required: - type @@ -77351,6 +77358,7 @@ components: anyOf: - $ref: '#/components/schemas/PromptCacheBreakpointParam' - type: 'null' + x-stainless-go-constant-constructor: true type: object required: - type @@ -77401,6 +77409,7 @@ components: anyOf: - $ref: '#/components/schemas/PromptCacheBreakpointParam' - type: 'null' + x-stainless-go-constant-constructor: true type: object required: - type @@ -99861,6 +99870,7 @@ components: The content of the file to be sent to the model. prompt_cache_breakpoint: $ref: '#/components/schemas/BetaPromptCacheBreakpointConfig' + x-stainless-go-constant-constructor: true file_url: type: string format: uri @@ -99898,7 +99908,8 @@ components: required: - mode title: Prompt cache breakpoint - description: Marks the exact end of a reusable prompt prefix. The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + description: Marks the exact end of a reusable prompt prefix. When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + x-stainless-go-constant-constructor: true BetaInputImageContent: properties: type: @@ -99924,6 +99935,7 @@ components: description: The detail level of the image to be sent to the model. One of `high`, `low`, `auto`, or `original`. Defaults to `auto`. prompt_cache_breakpoint: $ref: '#/components/schemas/BetaPromptCacheBreakpointConfig' + x-stainless-go-constant-constructor: true type: object required: - type @@ -99951,6 +99963,7 @@ components: description: The text input to the model. prompt_cache_breakpoint: $ref: '#/components/schemas/BetaPromptCacheBreakpointConfig' + x-stainless-go-constant-constructor: true type: object required: - type @@ -101731,6 +101744,7 @@ components: anyOf: - $ref: '#/components/schemas/BetaPromptCacheBreakpointParam' - type: 'null' + x-stainless-go-constant-constructor: true type: object required: - type @@ -101755,7 +101769,8 @@ components: required: - mode title: Prompt cache breakpoint - description: Marks the exact end of a reusable prompt prefix. The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + description: Marks the exact end of a reusable prompt prefix. When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + x-stainless-go-constant-constructor: true BetaDetailEnum: type: string enum: @@ -101780,6 +101795,7 @@ components: anyOf: - $ref: '#/components/schemas/BetaPromptCacheBreakpointParam' - type: 'null' + x-stainless-go-constant-constructor: true type: object required: - type @@ -101899,6 +101915,7 @@ components: anyOf: - $ref: '#/components/schemas/BetaPromptCacheBreakpointParam' - type: 'null' + x-stainless-go-constant-constructor: true type: object required: - type @@ -104224,6 +104241,7 @@ components: description: The detail level of the screenshot image to be sent to the model. One of `high`, `low`, `auto`, or `original`. Defaults to `auto`. prompt_cache_breakpoint: $ref: '#/components/schemas/BetaPromptCacheBreakpointConfig' + x-stainless-go-constant-constructor: true type: object required: - type diff --git a/scripts/castiron/custom_code_budget.py b/scripts/castiron/custom_code_budget.py index 01d21ee8cb..88b8085329 100755 --- a/scripts/castiron/custom_code_budget.py +++ b/scripts/castiron/custom_code_budget.py @@ -737,11 +737,17 @@ def prepare_main_snapshot( """Create and verify an adoption commit without altering a ref, index or metadata.""" base, authenticated_main = map(report.require_sha, (base, authenticated_main)) strict = {"schema_version": 2, "max_custom_patch_lines": 0, "generated_files": "strict"} - if read_policy(repo, authenticated_main) != strict or "next_policy" in read_policy(repo, base): + main_policy = read_policy(repo, authenticated_main) + if { + key: value for key, value in main_policy.items() if key != "ratchet" + } != strict or "next_policy" in read_policy(repo, base): raise ValueError("main snapshot preparation requires strict-zero main and unpinned next") if policy.merge_base(repo, base, authenticated_main) != base: raise ValueError("protected next must be an ancestor of authenticated main") - value = {**strict, "next_policy": {"repository": repository, "main_sha": authenticated_main}} + value = { + **main_policy, + "next_policy": {"repository": repository, "main_sha": authenticated_main}, + } blob = report.git( repo, "hash-object", @@ -814,10 +820,8 @@ def prepare_next_baseline(repo: Path, repository: str, branch: str) -> dict[str, if not report.REPOSITORY.fullmatch(repository) or not branch.startswith("codex/"): raise ValueError("expected a repository and a new codex/ review branch") report.git(repo, "check-ref-format", "--branch", branch) - if ( - report.git(repo, "remote", "get-url", "origin").decode().strip() - != f"https://github.com/{repository}.git" - ): + origin = report.git(repo, "remote", "get-url", "origin").decode().strip() + if origin not in report.github_origin_urls(repository): raise ValueError("preparation requires the public repository as origin") root = f"repos/{repository}" metadata = read_public_api(root) diff --git a/scripts/castiron/custom_code_policy.py b/scripts/castiron/custom_code_policy.py index 35538555ea..8d06a30575 100755 --- a/scripts/castiron/custom_code_policy.py +++ b/scripts/castiron/custom_code_policy.py @@ -98,8 +98,8 @@ def next_policy_sha(repo: Path, repository: str, base: str, authenticated_main: authenticated_main = report.require_sha(authenticated_main) if not report.REPOSITORY.fullmatch(repository): raise ValueError("invalid policy repository") - expected_origin = f"https://github.com/{repository}.git" - if report.git(repo, "remote", "get-url", "origin").decode().strip() != expected_origin: + origin = report.git(repo, "remote", "get-url", "origin").decode().strip() + if origin not in report.github_origin_urls(repository): raise ValueError("policy repository origin mismatch") fetched_main = report.git(repo, "rev-parse", "refs/remotes/origin/main").decode().strip() if fetched_main != authenticated_main: @@ -170,7 +170,9 @@ def has_human_approval(reviews: list[dict[str, Any]], author: str, head: str) -> """Versioned review predicate; publisher consumes these same trusted rules.""" latest: dict[str, dict[str, Any]] = {} for review in reviews: - if review["state"] != "PENDING" and review.get("user") is not None: + # Discussion does not replace an approval; blocking or dismissed + # reviews still supersede it, and edited attestations are checked below. + if review["state"] not in ("PENDING", "COMMENTED") and review.get("user") is not None: login = review["user"]["login"] if login not in latest or review["id"] > latest[login]["id"]: latest[login] = review @@ -312,10 +314,11 @@ def is_main_snapshot_bootstrap( if "next_policy" in read_policy(repo, base): return False strict = {"schema_version": 2, "max_custom_patch_lines": 0, "generated_files": "strict"} - if read_policy(repo, authenticated_main) != strict: + main_policy = read_policy(repo, authenticated_main) + if {key: value for key, value in main_policy.items() if key != "ratchet"} != strict: return False expected_policy = { - **strict, + **main_policy, "next_policy": {"repository": repository, "main_sha": authenticated_main}, } if read_policy(repo, head) != expected_policy: diff --git a/scripts/castiron/custom_code_report.py b/scripts/castiron/custom_code_report.py index d3fa56c36a..28b827905b 100755 --- a/scripts/castiron/custom_code_report.py +++ b/scripts/castiron/custom_code_report.py @@ -62,6 +62,17 @@ class ReportError(ValueError): """The report cannot establish a trustworthy comparison.""" +def github_origin_urls(repository: str) -> set[str]: + """Accepted URL spellings for one exact GitHub repository.""" + if not REPOSITORY.fullmatch(repository): + raise ReportError("invalid GitHub repository") + return { + f"https://github.com/{repository}.git", + f"https://github.com/{repository}", + f"git@github.com:{repository}.git", + } + + @dataclass(frozen=True) class Entry: mode: bytes @@ -487,14 +498,7 @@ def unique(pairs: list[tuple[str, Any]]) -> dict[str, Any]: if repository not in {"openai/openai-java", "openai/openai-cli"}: raise ReportError("first adoption is only supported for legacy Java/CLI") remote = git(repo, "remote", "get-url", "origin").decode().strip() - allowed = { - f"https://github.com/{repository}.git", - f"https://github.com/{repository}", - f"git@github.com:{repository}.git", - f"https://github.com/{repository}-internal.git", - f"https://github.com/{repository}-internal", - f"git@github.com:{repository}-internal.git", - } + allowed = github_origin_urls(repository) | github_origin_urls(f"{repository}-internal") if remote not in allowed: raise ReportError("adoption policy repository does not match SDK origin") source = require_sha(pin["main_sha"]) diff --git a/scripts/castiron/custom_code_test_support.py b/scripts/castiron/custom_code_test_support.py index add136e49d..812ec45acd 100644 --- a/scripts/castiron/custom_code_test_support.py +++ b/scripts/castiron/custom_code_test_support.py @@ -24,6 +24,9 @@ def setUp(self) -> None: # pyright: ignore[reportImplicitOverride] self.git("init", "-q", "-b", "main") self.git("config", "user.name", "Castiron test") self.git("config", "user.email", "castiron@example.test") + # Finish automatic maintenance before the temporary repository is removed. + self.git("config", "maintenance.autoDetach", "false") + self.git("config", "gc.autoDetach", "false") def git(self, *args: str) -> str: return subprocess.run( diff --git a/scripts/castiron/generated-files.json b/scripts/castiron/generated-files.json index ef047246a3..89ebbad90b 100644 --- a/scripts/castiron/generated-files.json +++ b/scripts/castiron/generated-files.json @@ -3,11 +3,11 @@ "files": { ".github/workflows/castiron-custom-code-comment.yml": { "mode": "100644", - "sha256": "b1057dcb3e913bc04e6b04451ce2c64c95c0c588718f7dfddffef3c38f151844" + "sha256": "ec43a2286a8f61efc2bfade540b4ccf572aac51fe0d2796121700f64f5bfce9b" }, ".github/workflows/castiron-custom-code.yml": { "mode": "100644", - "sha256": "ae54d4fd4828221782866f5d1e9fb080206d34ac73c63cd8ed2b6b8e83eac387" + "sha256": "b52dcb3feb424790aff1f6f7cff7f385bd921369257b8e59be2e3860ec1f4926" }, "api.md": { "mode": "100644", @@ -23,19 +23,19 @@ }, "scripts/castiron/custom_code_budget.py": { "mode": "100755", - "sha256": "dc7e39700868edfddd7152fbe03ae7347099a5623a46ec2dcc26650ea804ac06" + "sha256": "b85ae4eab2a84dfbcd1f197f87077f686bc4f3347dbf74cca3abb1f99ab50b9a" }, "scripts/castiron/custom_code_policy.py": { "mode": "100755", - "sha256": "ae04c9cfc445db50d9d3d346470ef08f60afc04f85c02e9dcb0691e4e16982ee" + "sha256": "890ecfc963b8a2461e7e801c9071735a975ed9876de89c80323da10960965295" }, "scripts/castiron/custom_code_report.py": { "mode": "100755", - "sha256": "c243015818640b1e7068605c5352fcb770d406202ee4e75316fe0ca85d06a1cc" + "sha256": "ddb15c9a162d26a6c9abf1b4fefcd80f2e685d9d255af729863258b7ecd08e2d" }, "scripts/castiron/custom_code_test_support.py": { "mode": "100644", - "sha256": "022cd73039e47e4b390ddad54a6e46b0924efa4e9e047e6f0639924752353b2c" + "sha256": "b90c31b6b7347a3bc254b9fec228d5ccad3025306c60465a027a239420276671" }, "scripts/castiron/fixtures/github_publisher.cjs": { "mode": "100644", @@ -51,11 +51,11 @@ }, "scripts/castiron/test_custom_code_github.py": { "mode": "100644", - "sha256": "8e5c46ce2820f5e8232673da6c75f75a6f289617c30203771fca1d981e793eba" + "sha256": "815e8db60dd81f69895696813f6b612856eb899803e3e9b85df22ec688073b87" }, "scripts/castiron/test_custom_code_policy.py": { "mode": "100644", - "sha256": "9493ac7d56b6291042eca3b77141e7226b845332c96e6e33aaf90a0687f346d5" + "sha256": "fd503013d604cf9659df95a3a47cecfc17da364397625d95ef6121aa790e6a05" }, "scripts/castiron/test_custom_code_policy_source.py": { "mode": "100644", @@ -63,7 +63,7 @@ }, "scripts/castiron/test_custom_code_publication.py": { "mode": "100644", - "sha256": "7a5a381cad6106a079e0e1eb9415bbb6393e7e7317d020b869ad1338201632de" + "sha256": "4533c8c852d590446180715adbd00a63c12af29c53316d3e91d129890ccadbc6" }, "scripts/castiron/test_custom_code_report.py": { "mode": "100644", @@ -71,15 +71,15 @@ }, "scripts/castiron/test_custom_code_review_revocation.py": { "mode": "100644", - "sha256": "c2542298548bd00d522ba448685e998897de82cfcc967f8e1b0297e5fbcec141" + "sha256": "d7f309e2324f48f62f9f7f90c2e82f31354e414a20e3ddb0da724b60adeff7a3" }, "scripts/castiron/test_custom_code_status_publication.py": { "mode": "100644", - "sha256": "c3479390737e088ccb3d31926ae2636f763fc6919b3f871d21f066fed6802bfd" + "sha256": "7398877d4ec209d0cd40e0d83f8e4482b6de5bef95f1711c9905d6a97a3791d3" }, "scripts/castiron/test_custom_code_strict.py": { "mode": "100644", - "sha256": "3b61e1bde4bfc80cf657746f482fe334a9e0c7517262d3eb23b940ef49a0c374" + "sha256": "20684147ec37f8233798a46aef8b618a2bbf2c8b507605a959d644fce163d73d" }, "src/openai/__init__.py": { "mode": "100644", @@ -2815,35 +2815,35 @@ }, "src/openai/types/beta/beta_response_input_file.py": { "mode": "100644", - "sha256": "678fff6dcb76fdd4b958c86e9e0a1009244283e3688f4d5c43184fd8103f7dbe" + "sha256": "cc6f72da609314c3a4fcd13f2a6f79a1666c32d1468f32c3d92f447db0f8ce24" }, "src/openai/types/beta/beta_response_input_file_content.py": { "mode": "100644", - "sha256": "a26937c4678dbf2e03f15317a9bbe2e9729ec21c67cb23ad1ca13421b5206c82" + "sha256": "b4d1e9f930d592f41c9eeaa825caa0c322308688d785b2a445a4f3eea7ed98cc" }, "src/openai/types/beta/beta_response_input_file_content_param.py": { "mode": "100644", - "sha256": "e9ee5b6fa0598dfc6923e4db2008dbd93e0ef0e12c7a3486fcce188ac606b26f" + "sha256": "ebc40fc3f15f84df97a900901debe5367203daaa23e8b90755a439755a2586a1" }, "src/openai/types/beta/beta_response_input_file_param.py": { "mode": "100644", - "sha256": "9e0ff392f9355d35455db9fc8d7b3c87cc0cefa5c241eefdb28a66f4668aaef7" + "sha256": "a6db8c9671a3ef0cdf0eb19883bc19a9c0f1f269568b256dff1089bc30177cd1" }, "src/openai/types/beta/beta_response_input_image.py": { "mode": "100644", - "sha256": "3792174e56f06280aa1159a76da5ba81ae7bc06e95a49538a49eb35a3153c17d" + "sha256": "4fb6c1290f75cf5cf2b1bf8264cfee7be38ad44d1e25b7016654b939f674a5bd" }, "src/openai/types/beta/beta_response_input_image_content.py": { "mode": "100644", - "sha256": "897f719ce596f4c34e1ef02235a6734ed94bda418c53ae21f035d3e5964b4cdc" + "sha256": "34dd073b133434db8b419c34a14ce8ab51948a3236a86a4c8ae7e061f710709c" }, "src/openai/types/beta/beta_response_input_image_content_param.py": { "mode": "100644", - "sha256": "29fd94be12976fbb72fea7db17008d4f1a06dc8e88bfe8c55dac4c4018eb167b" + "sha256": "1f49f0afec9d68e8ecc2da1ca6abc3ac7624966c31e7b2f85a8deb4bb37dc452" }, "src/openai/types/beta/beta_response_input_image_param.py": { "mode": "100644", - "sha256": "4862b3cc063af9d90e7dc67579b84ae21087bf46bd18faeb481d65ba23b29a9e" + "sha256": "c78ecdce3c708959d48ac87aba73addd62cc55a31e579f5be5eafa2082323c20" }, "src/openai/types/beta/beta_response_input_item.py": { "mode": "100644", @@ -2871,23 +2871,23 @@ }, "src/openai/types/beta/beta_response_input_text.py": { "mode": "100644", - "sha256": "da753d1f3e2618ad969342bb5c20ddf515aa7481f8f81bc351a2b674812b3aa1" + "sha256": "e214618246f1ecb60e2d3c69731095ec298e2fa104e46fb45cbab6746f7e7030" }, "src/openai/types/beta/beta_response_input_text_content.py": { "mode": "100644", - "sha256": "05fafd48cdbe85f06a942e6f3c5f211e5694bb3c4047dc7d6e53850907b5bacb" + "sha256": "81e335dd365648cecf5d12fe521ad419820b9205da077909571418a747ec6760" }, "src/openai/types/beta/beta_response_input_text_content_param.py": { "mode": "100644", - "sha256": "147fd5668879a2195d8bd570f33bac43db1f3959f5baa67172bb8e832b6d99c2" + "sha256": "e32456ef57caa7534b501594a137ac8b0adc957d3b7db64e1555446a2f03f8a3" }, "src/openai/types/beta/beta_response_input_text_param.py": { "mode": "100644", - "sha256": "4db28c3ef3960793c5a874009fa43f2d27afb4b10aff154ebf1160e4f7eef4e1" + "sha256": "ac8b882ef0d4c4c17d0e7c9ff90127551a69f474acd0948584d38f32d4772f9e" }, "src/openai/types/beta/beta_response_item.py": { "mode": "100644", - "sha256": "a36f81a50941233bd7f65e323c8fadefe24a309d2f2697ef103382d9d456ca3f" + "sha256": "9011df6b2a79271e30eef466909d5e18201caf603c19ec6133c7961079345c8d" }, "src/openai/types/beta/beta_response_local_environment.py": { "mode": "100644", @@ -2927,7 +2927,7 @@ }, "src/openai/types/beta/beta_response_output_item.py": { "mode": "100644", - "sha256": "f785f2680a96e7e2b262c8d653e02c84f59a96cbcb6721edcf1ac258d10e4095" + "sha256": "2f2ba65faffd931ceb62b95346e8a0b42212060fd08c2dd4f150495b4d660bc2" }, "src/openai/types/beta/beta_response_output_item_added_event.py": { "mode": "100644", @@ -3851,19 +3851,19 @@ }, "src/openai/types/chat/chat_completion_content_part_image.py": { "mode": "100644", - "sha256": "37c32f3ac96d0bd3b7bfb7c6fd5cb24ca8834f123962b8d3775c692f0c790b87" + "sha256": "60536a778ffc543a55bd252884fa28619738798efd6d87927d2aab5a4628fbc8" }, "src/openai/types/chat/chat_completion_content_part_image_param.py": { "mode": "100644", - "sha256": "a4d2f746c6d3939a4f450f51c275cfde4eba8c9f523a5ffa4a488c9f82a711a3" + "sha256": "daf536ae602fd6dba4cde122e6c17fc7e872e84f22c9d7ac7bceea3d2384fd0f" }, "src/openai/types/chat/chat_completion_content_part_input_audio_param.py": { "mode": "100644", - "sha256": "5d4db7a70e814268c78d580a43adf765ec31da8fde1e872592837f9baf203280" + "sha256": "d1cb56d79cee6f0ad0e6532e148d180d4d452e4ccede20e1cc84117bae0a7df7" }, "src/openai/types/chat/chat_completion_content_part_param.py": { "mode": "100644", - "sha256": "4334661b0c4b913af698bd17a2004ad8cdf008549222f10e23f87cb84e89dd30" + "sha256": "e992307987f2c5dbb76304efd1deba79f96ac9588ba92d40188c7951136f97c7" }, "src/openai/types/chat/chat_completion_content_part_refusal_param.py": { "mode": "100644", @@ -3871,11 +3871,11 @@ }, "src/openai/types/chat/chat_completion_content_part_text.py": { "mode": "100644", - "sha256": "0e0cac98dd112de540a08bdb288ed8b7ac8a0a3908bb738bc97ec5a5bcea484f" + "sha256": "e6ee08604f82c5592ab33ebf10467464cc83deb41946707412f496df9593fdb8" }, "src/openai/types/chat/chat_completion_content_part_text_param.py": { "mode": "100644", - "sha256": "05dfba4e59c1792d84107bf31a58d4670205ca2757415fab9da081f0dadb286b" + "sha256": "f76ceaec2e0a7e02949b3a825f9262069b870d6d0fcead05582961d6834a4392" }, "src/openai/types/chat/chat_completion_custom_tool_param.py": { "mode": "100644", @@ -4099,7 +4099,7 @@ }, "src/openai/types/conversations/computer_screenshot_content.py": { "mode": "100644", - "sha256": "e2130cbb329d466c958e8f3c8b79a2c98aa0843a0bd430d4b9968a01705ea67d" + "sha256": "b7b8505f2bb0af2928c36e2e66653cf870632cef2ef3205fbccb735d8ca0c345" }, "src/openai/types/conversations/conversation.py": { "mode": "100644", @@ -6439,35 +6439,35 @@ }, "src/openai/types/responses/response_input_file.py": { "mode": "100644", - "sha256": "08367864bff957fa4a8b020acee507981a9dc2280efe19b7fb7eb2287959f6a4" + "sha256": "56dfe0226bacd7745e5f4447a9d7bf57dc6d06d19901be84b8493532e9a85f58" }, "src/openai/types/responses/response_input_file_content.py": { "mode": "100644", - "sha256": "a3bd4748c31fa004957efbd251821f670179f33265c4de609934c336fd1adb5f" + "sha256": "cc30f4f1095335e899710cb6e87662261a45f34fb4f041bb262a4163c33a7cae" }, "src/openai/types/responses/response_input_file_content_param.py": { "mode": "100644", - "sha256": "1b3cb6af87357124adfc1e19ad82e19a87a54058c7726414e9c26b7a3955690f" + "sha256": "a67b4d1915d0192a0fd101c8a496e29f9fd7316a973a4ac69c529eb43514b758" }, "src/openai/types/responses/response_input_file_param.py": { "mode": "100644", - "sha256": "dc7af160ae8ca96c77aebf92f01fb3e5db77de39d9deb767d0f69a792837a73f" + "sha256": "20eab505e6a62347e96f4783e01a3ca91c1000bb07ff357687a9b66b313466d5" }, "src/openai/types/responses/response_input_image.py": { "mode": "100644", - "sha256": "4852c46a52e5128aed6ddb96e2cbda86b8ca5b9235f632ee60668f0f67908df6" + "sha256": "2998b697b245bd5b74b9cd7da3a386438978333cd0fcf226b367787ea094ae29" }, "src/openai/types/responses/response_input_image_content.py": { "mode": "100644", - "sha256": "79d7a845bae7ab4f7d33d2b5aafcc175cc36f05d0fa97613c80dc6c65a7f351c" + "sha256": "332662907758e795af9823de9c4b50d71117387654c4751a779992f9493e0edd" }, "src/openai/types/responses/response_input_image_content_param.py": { "mode": "100644", - "sha256": "3c9402108cf4ef22f7550b4cc023dc9f03ad36841b966b28534d050ce71654e0" + "sha256": "6878cc55a90fbee76155260a7264059ef45f02f9aa74fc643a4c93f20156336b" }, "src/openai/types/responses/response_input_image_param.py": { "mode": "100644", - "sha256": "db2fd3ca33fad4186fadd2c636f0509aa0c768f98d55590bf017a44fdaeaf908" + "sha256": "a6110b76f8a2f47285b6848304340189f58728561c531764c1493d8d016ba637" }, "src/openai/types/responses/response_input_item.py": { "mode": "100644", @@ -6495,19 +6495,19 @@ }, "src/openai/types/responses/response_input_text.py": { "mode": "100644", - "sha256": "5e8250a26a4fa696b5dd63f4e990ab89f3370d2c88f1df898e386eea98070434" + "sha256": "fff0cfbde004cb1e530ec18fa2ab45f0bdb53cf12f17d94aeae77c990af65e9b" }, "src/openai/types/responses/response_input_text_content.py": { "mode": "100644", - "sha256": "45acd1cafcdeef42d581af4156c7577f69629fe9393bbaf4d2340ecce516d682" + "sha256": "6c3dbf6d29eb32d57f38f63bb592bf5225d859a1711312948a2b0173600ecd4e" }, "src/openai/types/responses/response_input_text_content_param.py": { "mode": "100644", - "sha256": "852bb7535307b24de02778f46b20c8a2a9727ce165c3a171ed42f03ac94a9072" + "sha256": "cecf938a53f48c491eb808ffd18205cfefbe13940947dc0b01ee1ed4d20e704c" }, "src/openai/types/responses/response_input_text_param.py": { "mode": "100644", - "sha256": "67d2d5fb278f9fe1f8ed0c8f4201477f23de5a6c681465f81d4471d5c752fc8d" + "sha256": "e9619446166e7312ddbe46246dc054d781486a40cbef804bf4103ff2bb2a030c" }, "src/openai/types/responses/response_item.py": { "mode": "100644", diff --git a/scripts/castiron/test_custom_code_github.py b/scripts/castiron/test_custom_code_github.py index 4c71402e9d..8cf302c659 100644 --- a/scripts/castiron/test_custom_code_github.py +++ b/scripts/castiron/test_custom_code_github.py @@ -110,7 +110,10 @@ def test_trusted_report_recomputes_pr_output_in_a_bare_repository(self) -> None: base = self.commit() legitimate, _ = report.build_report(self.repo, base, base, require_head_hash=True) self.write("generated.py", "generated\n# custom\n") - self.write("scripts/castiron/custom_code_report.py", "raise RuntimeError('PR code ran')\n") + self.write( + "scripts/castiron/custom_code_report.py", + "raise RuntimeError('PR code ran')\n", + ) self.write("report.json", json.dumps(legitimate)) head = self.commit() self.write( diff --git a/scripts/castiron/test_custom_code_policy.py b/scripts/castiron/test_custom_code_policy.py index d71aef3e12..c68bb7072e 100644 --- a/scripts/castiron/test_custom_code_policy.py +++ b/scripts/castiron/test_custom_code_policy.py @@ -426,6 +426,33 @@ def test_selector_outputs_transport_review_receipt_as_json(self) -> None: self.assertFalse(json.loads(values["bootstrap"])) self.assertEqual(json.loads(out.read_text())["schema_version"], 1) + def test_follow_up_comments_preserve_policy_approval(self) -> None: + approval = self.approved() + for state in ("COMMENTED", "PENDING"): + follow_up = self.approved(id=11, state=state, body="A non-blocking follow-up") + for reviews in ([approval, follow_up], [follow_up, approval]): + with ( + self.subTest(state=state, reviews=reviews), + mock.patch.object(selector.report, "api", side_effect=self.review_api(reviews)), + ): + self.assertEqual(selector.require_human_policy_review(REPO, self.head), 3) + + def test_comments_do_not_restore_revoked_policy_approval(self) -> None: + comment = self.approved(id=12, state="COMMENTED", body="A follow-up comment") + cases = [ + [self.approved(), self.approved(id=11, state="CHANGES_REQUESTED"), comment], + [self.approved(state="DISMISSED"), comment], + [self.approved(body="Attestation removed"), comment], + [comment], + ] + for reviews in cases: + with ( + self.subTest(reviews=reviews), + mock.patch.object(selector.report, "api", side_effect=self.review_api(reviews)), + self.assertRaisesRegex(ValueError, "human"), + ): + selector.require_human_policy_review(REPO, self.head) + def test_forged_bot_stale_dismissed_untrusted_reviews_rejected(self) -> None: self.git("checkout", "-q", "--detach", self.base) self.write_policy( @@ -554,6 +581,16 @@ def test_wrong_origin_rejected(self) -> None: with self.assertRaisesRegex(ValueError, "origin"): self.select() + def test_same_repository_github_url_spellings_are_accepted(self) -> None: + for origin in ( + f"https://github.com/{REPO}.git", + f"https://github.com/{REPO}", + f"git@github.com:{REPO}.git", + ): + with self.subTest(origin=origin): + self.git("remote", "set-url", "origin", origin) + self.assertEqual(self.select()["policy_sha"], self.main) + def test_policy_update_uses_old_base_checker_and_requires_human(self) -> None: self.git("checkout", "-q", "--detach", self.main) self.write_policy({**self.policy, "max_custom_patch_lines": 5000}) diff --git a/scripts/castiron/test_custom_code_publication.py b/scripts/castiron/test_custom_code_publication.py index b56cce0d9b..205fab5077 100644 --- a/scripts/castiron/test_custom_code_publication.py +++ b/scripts/castiron/test_custom_code_publication.py @@ -11,7 +11,6 @@ from typing import Any from unittest import mock -import custom_code_policy as policy import custom_code_report as report from custom_code_test_support import ( WORKFLOW, @@ -597,576 +596,5 @@ def test_successful_comment_rejects_another_source_before_and_after_pagination( self.assertTrue(all(call.args[0] == "GET" for call in api.call_args_list)) -@unittest.skipUnless(shutil.which("node"), "Node is needed to execute the status-publisher fixture") -class StatusPublisherTests(unittest.TestCase): - def publish( - self, - *, - event_name: str = "pull_request", - head_changed: bool = False, - base_changed: bool = False, - no_result: bool = False, - failed_budget: bool = False, - fallback_pulls: list[dict[str, int]] | None = None, - run_overrides: dict[str, Any] | None = None, - previous_statuses: list[dict[str, Any]] | None = None, - branch: str = "main", - evaluated_branch: str | None = None, - policy_changed: bool = False, - policy_sha: str | None = None, - selected_policy_matches: bool = True, - main_snapshot: str = "", - queue_absent: bool = False, - queue_pages: list[dict[str, Any]] | None = None, - retarget_during_publication: bool = False, - same_head_next_pr: bool = False, - policy_reviews: list[dict[str, Any]] | None = None, - reviews: list[dict[str, Any]] | None = None, - review_nodes: list[dict[str, Any]] | None = None, - review_pages: list[dict[str, Any]] | None = None, - policy_pull: dict[str, Any] | None = None, - missing_review_contract: bool = False, - review_contract: dict[str, Any] | None = None, - ) -> list[dict[str, Any]]: - base, head = "a" * 40, "b" * 40 - run = source_run(head, event_name) - if event_name == "merge_group": - run["head_branch"] = f"gh-readonly-queue/{branch}/pr-7-example" - payload: dict[str, Any] = { - "reviews": reviews or [], - "review_nodes": review_nodes - if review_nodes is not None - else [ - { - "fullDatabaseId": str(review["id"]), - "state": review["state"], - "body": review.get("body"), - "commit": {"oid": review.get("commit_id")}, - "author": { - "login": review["user"]["login"], - "__typename": review["user"]["type"], - }, - "authorAssociation": review.get("author_association"), - } - for review in reviews or [] - ], - "review_pages": review_pages, - "main_sha": "d" * 40 - if policy_changed - else (policy_sha or ("c" * 40 if branch == "main" and base_changed else base)), - "queue_absent": queue_absent, - "queue_pages": queue_pages, - "fallback_pulls": fallback_pulls, - "previous_statuses": previous_statuses or [], - "context": { - "eventName": "workflow_run", - "repo": {"owner": "openai", "repo": "example"}, - "serverUrl": "https://github.com", - "runId": 123, - "payload": { - "workflow_run": run, - }, - }, - "run": {**run, **(run_overrides or {})}, - "current": { - "number": 3, - "user": {"login": "author"}, - "state": "open", - "head": source_head("c" * 40 if head_changed else head), - "base": { - "sha": "c" * 40 if base_changed else base, - "ref": branch, - "repo": {"full_name": "openai/example"}, - }, - }, - "env": { - "POLICY_REVIEWS": json.dumps(policy_reviews or []), - "HUMAN_REVIEW_RULES": "" - if missing_review_contract - else json.dumps( - policy.HUMAN_REVIEW_RULES if review_contract is None else review_contract - ), - "MAIN_SNAPSHOT": main_snapshot, - "SELECTED_BRANCH": branch, - "SELECTED_BOOTSTRAP": "false", - "SELECTED_BASE_SHA": base, - "SELECTED_HEAD_SHA": head, - "SELECTED_POLICY_SHA": (policy_sha or base) - if selected_policy_matches - else "f" * 40, - "TARGET_BRANCH": "" if no_result else (evaluated_branch or branch), - "POLICY_SHA": "" if no_result else (policy_sha or base), - "BASE_SHA": "" if no_result else base, - "HEAD_SHA": "" if no_result else head, - "ISOLATION_RESULT": "success", - "BUDGET_RESULT": "failure" if failed_budget else "success", - "PUBLISH_ATTEMPT": "1", - }, - } - if policy_pull: - payload["pulls_by_number"] = {policy_pull["number"]: policy_pull} - if same_head_next_pr: - current = payload["current"] - payload["pulls_by_number"] = { - 3: current, - 4: {**current, "number": 4, "base": {**current["base"], "ref": "next"}}, - } - if retarget_during_publication: - current = payload["current"] - payload["current_sequence"] = [ - current, - {**current, "base": {**current["base"], "ref": "other"}}, - ] - return run_publisher("Publish exact-head statuses after checking freshness", payload) - - def test_live_human_review_predicate_matches_selector(self) -> None: - head = "b" * 40 - approved = { - "id": 10, - "state": "APPROVED", - "commit_id": head, - "user": {"login": "human", "type": "User"}, - "author_association": "MEMBER", - "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + head, - } - cases = [ - [approved], - [], - [{**approved, "state": "DISMISSED"}], - [approved, {**approved, "id": 11, "state": "CHANGES_REQUESTED"}], - [approved, {**approved, "id": 11, "state": "COMMENTED"}], - [{**approved, "commit_id": "f" * 40}], - [{**approved, "user": {"login": "author", "type": "User"}}], - [{**approved, "user": {"login": "bot", "type": "Bot"}}], - [{**approved, "author_association": "NONE"}], - ] - for body in [ - "", - "Looks good to me", - policy.HUMAN_REVIEW_RULES["attestation_prefix"] + "f" * 40, - "> " + policy.HUMAN_REVIEW_RULES["attestation_prefix"] + head, - "", - "Automated source review.", - "Source-only review; I did not execute candidate code or workflows.", - "This does not substitute for human approval.", - "This is not a human approving review.", - ]: - cases.append([{**approved, "body": body}]) - for reviews in cases: - with self.subTest(reviews=reviews): - expected = ( - "success" if policy.has_human_approval(reviews, "author", head) else "failure" - ) - writes = self.publish( - branch="next", policy_reviews=[{"number": 3, "head_sha": head}], reviews=reviews - ) - self.assertEqual([w["state"] for w in writes], [expected, expected]) - - def test_final_publisher_preserves_review_identity_precision(self) -> None: - head = "b" * 40 - approval = { - "id": 9007199254740992, - "state": "APPROVED", - "commit_id": head, - "user": {"login": "human", "type": "User"}, - "author_association": "MEMBER", - "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + head, - } - replacement = {**approval, "id": 9007199254740993, "state": "COMMENTED"} - for reviews in ([approval, replacement], [replacement, approval]): - with self.subTest(reviews=reviews): - writes = self.publish( - branch="next", - policy_reviews=[{"number": 3, "head_sha": head}], - reviews=reviews, - ) - self.assertEqual([w["state"] for w in writes], ["failure"] * 2) - writes = self.publish( - branch="next", - policy_reviews=[{"number": 3, "head_sha": head}], - reviews=[{**replacement, "state": "APPROVED"}, {**approval, "state": "DISMISSED"}], - ) - self.assertEqual([w["state"] for w in writes], ["success"] * 2) - - def test_final_publisher_review_pages_and_malformed_identity(self) -> None: - head = "b" * 40 - approval = { - "fullDatabaseId": "9007199254740992", - "state": "APPROVED", - "commit": {"oid": head}, - "author": {"login": "human", "__typename": "User"}, - "authorAssociation": "MEMBER", - "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + head, - } - - def publish(**kwargs: Any) -> list[str]: - return [ - w["state"] - for w in self.publish( - branch="next", - policy_reviews=[{"number": 3, "head_sha": head}], - **kwargs, - ) - ] - - self.assertEqual(publish(review_nodes=[approval]), ["success"] * 2) - for identity in (None, "", "1.5", "-1", "0", 9007199254740992): - with self.subTest(identity=identity): - self.assertEqual( - publish(review_nodes=[{**approval, "fullDatabaseId": identity}]), - ["failure"] * 2, - ) - pages: list[dict[str, Any]] = [ - { - "expected_cursor": None, - "nodes": [approval], - "pageInfo": {"hasNextPage": True, "endCursor": "page2"}, - }, - { - "expected_cursor": "page2", - "nodes": [{**approval, "fullDatabaseId": "9007199254740993", "state": "DISMISSED"}], - "pageInfo": {"hasNextPage": False, "endCursor": None}, - }, - ] - self.assertEqual(publish(review_pages=pages), ["failure"] * 2) - self.assertEqual( - publish( - review_pages=[{**pages[0], "pageInfo": {"hasNextPage": True, "endCursor": None}}] - ), - ["failure"] * 2, - ) - - def test_queue_policy_approval_dismissal_after_selection_fails(self) -> None: - candidate = "e" * 40 - pull: dict[str, Any] = { - "number": 7, - "user": {"login": "author"}, - "state": "open", - "head": source_head(candidate), - "base": {"ref": "next", "repo": {"full_name": "openai/example"}}, - } - approved = { - "id": 1, - "state": "APPROVED", - "commit_id": candidate, - "user": {"login": "human", "type": "User"}, - "author_association": "MEMBER", - "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + candidate, - } - for state in ("APPROVED", "DISMISSED", "CHANGES_REQUESTED"): - with self.subTest(state=state): - writes = self.publish( - event_name="merge_group", - branch="next", - policy_pull=pull, - policy_reviews=[{"number": 7, "head_sha": candidate}], - reviews=[{**approved, "state": state}], - ) - self.assertEqual( - [w["state"] for w in writes], - ["success" if state == "APPROVED" else "failure"] * 2, - ) - # Ordinary automated approvals may keep the queue live, but cannot replace - # the independently required human policy approval. - writes = self.publish( - event_name="merge_group", - branch="next", - policy_pull=pull, - policy_reviews=[{"number": 7, "head_sha": candidate}], - reviews=[ - {**approved, "state": "DISMISSED"}, - { - **approved, - "id": 2, - "user": {"login": "automated", "type": "User"}, - "body": "", - }, - ], - ) - self.assertEqual([w["state"] for w in writes], ["failure", "failure"]) - - def test_queue_policy_constituent_changes_fail_closed(self) -> None: - candidate = "e" * 40 - pull: dict[str, Any] = { - "number": 7, - "user": {"login": "author"}, - "state": "open", - "head": source_head(candidate), - "base": {"ref": "next", "repo": {"full_name": "openai/example"}}, - } - approved = { - "id": 1, - "state": "APPROVED", - "commit_id": candidate, - "user": {"login": "human", "type": "User"}, - "author_association": "MEMBER", - "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + candidate, - } - for changed in [ - {**pull, "state": "closed"}, - {**pull, "head": {"sha": "f" * 40}}, - {**pull, "base": {**pull["base"], "ref": "main"}}, - ]: - with self.subTest(changed=changed): - writes = self.publish( - event_name="merge_group", - branch="next", - policy_pull=changed, - policy_reviews=[{"number": 7, "head_sha": candidate}], - reviews=[approved], - ) - self.assertEqual([w["state"] for w in writes], ["failure", "failure"]) - - def test_missing_policy_review_contract_fails_closed(self) -> None: - self.assertEqual( - [w["state"] for w in self.publish(branch="next", missing_review_contract=True)], - ["failure", "failure"], - ) - - def test_legacy_review_contract_fails_closed_only_on_next(self) -> None: - legacy = { - key: value - for key, value in policy.HUMAN_REVIEW_RULES.items() - if key != "attestation_prefix" - } - legacy["version"] = 1 - for branch, expected in (("next", "failure"), ("main", "success")): - with self.subTest(branch=branch): - self.assertEqual( - [w["state"] for w in self.publish(branch=branch, review_contract=legacy)], - [expected, expected], - ) - - def test_statuses_attach_to_candidate_not_main(self) -> None: - for event in ("pull_request", "merge_group"): - with self.subTest(event=event): - results = self.publish(event_name=event) - self.assertEqual(len(results), 2) - self.assertTrue( - all(r["sha"] == "b" * 40 and r["state"] == "success" for r in results) - ) - - def test_next_cannot_overwrite_main_results(self) -> None: - for main_event in ("pull_request", "merge_group"): - main = self.publish(event_name=main_event, failed_budget=True) - self.assertEqual([r["state"] for r in main], ["success", "failure"]) - for event in ("pull_request", "merge_group"): - with self.subTest(main_event=main_event, event=event): - # Both evaluations use the same commit. A next success - # must not replace the main target's required failure. - result = self.publish(event_name=event, branch="next", previous_statuses=main) - self.assertEqual([r["state"] for r in result], ["success", "success"]) - self.assertTrue( - {r["context"] for r in result}.isdisjoint(r["context"] for r in main) - ) - - def test_same_head_next_pr_does_not_block_required_main_statuses(self) -> None: - for numbers in ([3, 4], [4, 3]): - with self.subTest(numbers=numbers): - results = self.publish( - run_overrides={"pull_requests": []}, - fallback_pulls=[{"number": number} for number in numbers], - same_head_next_pr=True, - failed_budget=True, - ) - self.assertEqual([r["state"] for r in results], ["success", "failure"]) - self.assertTrue(all(r["sha"] == "b" * 40 for r in results)) - next_results = self.publish( - branch="next", run_overrides={"pull_requests": [{"number": 4}]}, same_head_next_pr=True - ) - self.assertEqual([r["state"] for r in next_results], ["success", "success"]) - self.assertTrue(all(r["context"].startswith("Castiron next /") for r in next_results)) - - def test_next_queue_approved_pin_survives_main_advancing(self) -> None: - for changed in (False, True): - with self.subTest(policy_changed=changed): - results = self.publish( - event_name="merge_group", branch="next", policy_changed=changed - ) - self.assertEqual([r["state"] for r in results], ["success", "success"]) - - def test_next_pin_receipt_and_target_freshness(self) -> None: - for event in ("pull_request", "merge_group"): - cases: tuple[dict[str, Any], ...] = ( - {"selected_policy_matches": False}, - {"no_result": True}, - {"base_changed": True}, - ) - for kwargs in cases: - with self.subTest(event=event, kwargs=kwargs): - results = self.publish(event_name=event, branch="next", **kwargs) - self.assertEqual([r["state"] for r in results], ["failure", "failure"]) - results = self.publish(event_name=event, branch="next", policy_sha="e" * 40) - self.assertEqual([r["state"] for r in results], ["success", "success"]) - results = self.publish(event_name="merge_group", branch="next", queue_absent=True) - self.assertEqual([r["state"] for r in results], ["failure", "failure"]) - - def test_main_snapshot_publication_rechecks_live_main(self) -> None: - for event in ("pull_request", "merge_group"): - for moved in (False, True): - with self.subTest(event=event, moved=moved): - writes = self.publish( - event_name=event, - branch="next", - policy_sha="e" * 40, - main_snapshot="e" * 40, - policy_changed=moved, - ) - self.assertEqual( - [w["state"] for w in writes], - ["failure", "failure"] if moved else ["success", "success"], - ) - - def test_main_statuses_bind_target_policy_and_queue_membership(self) -> None: - for event in ("pull_request", "merge_group"): - with self.subTest(event=event): - stale = self.publish(event_name=event, evaluated_branch="next") - self.assertEqual([r["state"] for r in stale], ["failure", "failure"]) - for policy_changed, queue_absent in ((True, False), (False, True)): - with self.subTest(policy_changed=policy_changed, queue_absent=queue_absent): - result = self.publish( - event_name="merge_group", - policy_changed=policy_changed, - queue_absent=queue_absent, - ) - self.assertEqual([r["state"] for r in result], ["failure", "failure"]) - self.assertEqual(self.publish(retarget_during_publication=True), []) - - def test_next_fork_and_unsupported_targets(self) -> None: - result = self.publish( - branch="next", run_overrides={"pull_requests": []}, fallback_pulls=[{"number": 3}] - ) - self.assertEqual([r["state"] for r in result], ["success", "success"]) - for event in ("pull_request", "merge_group"): - for branch in ("Next", "Main", "release", "next-other"): - with self.subTest(event=event, branch=branch): - self.assertEqual(self.publish(event_name=event, branch=branch), []) - - def test_main_queue_membership_paginates(self) -> None: - pages: list[dict[str, Any]] = [ - { - "expected_cursor": None, - "nodes": [], - "pageInfo": {"hasNextPage": True, "endCursor": "page2"}, - }, - { - "expected_cursor": "page2", - "nodes": [{"headCommit": {"oid": "b" * 40}}], - "pageInfo": {"hasNextPage": False, "endCursor": None}, - }, - ] - result = self.publish(event_name="merge_group", queue_pages=pages) - self.assertEqual([r["state"] for r in result], ["success", "success"]) - - def test_fork_statuses_with_no_commit_association(self) -> None: - options: dict[str, Any] = { - "run_overrides": {"pull_requests": []}, - "fallback_pulls": [{"number": 3}], - } - results = self.publish(**options) - self.assertEqual(len(results), 2) - self.assertTrue(all(r["sha"] == "b" * 40 and r["state"] == "success" for r in results)) - self.assertEqual(self.publish(**options, head_changed=True), []) - self.assertTrue( - all(r["state"] == "failure" for r in self.publish(**options, no_result=True)) - ) - self.assertEqual(self.publish(run_overrides={"pull_requests": []}, fallback_pulls=[]), []) - self.assertEqual( - self.publish( - run_overrides={"pull_requests": []}, fallback_pulls=[{"number": 3}, {"number": 4}] - ), - [], - ) - - def test_stale_pr_head_is_not_published(self) -> None: - self.assertEqual(self.publish(head_changed=True), []) - - def test_pr_snapshot_survives_main_advancing(self) -> None: - results = self.publish(base_changed=True) - self.assertEqual(len(results), 2) - self.assertTrue(all(r["state"] == "success" for r in results)) - self.assertTrue(all("a" * 12 in r["description"] for r in results)) - - def test_queue_still_rejects_main_advancing(self) -> None: - results = self.publish(event_name="merge_group", base_changed=True) - self.assertEqual(len(results), 2) - self.assertTrue(all(r["state"] == "failure" for r in results)) - - def test_missing_evaluation_publishes_actionable_failures(self) -> None: - for event in ("pull_request", "merge_group"): - results = self.publish(event_name=event, no_result=True, base_changed=True) - self.assertEqual(len(results), 2) - for result in results: - self.assertEqual(result["state"], "failure") - self.assertIn("inspect the trusted run", result["description"]) - self.assertTrue(result["target_url"].endswith("/actions/runs/123")) - - def test_main_rejects_mismatched_selector_receipt(self) -> None: - results = self.publish(selected_policy_matches=False) - self.assertTrue(results) - self.assertTrue(all(result["state"] != "success" for result in results)) - - def test_lookalike_workflow_cannot_publish(self) -> None: - self.assertEqual( - self.publish( - run_overrides={ - "path": ".github/workflows/castiron-custom-code.yml@evil.yml", - "workflow_id": 789, - } - ), - [], - ) - - def test_superseded_or_wrong_source_run_cannot_publish(self) -> None: - for overrides in ( - {"run_attempt": 2}, - {"head_sha": "c" * 40}, - {"event": "push"}, - {"path": "other.yml"}, - ): - with self.subTest(overrides=overrides): - self.assertEqual(self.publish(run_overrides=overrides), []) - - def test_independent_check_failures_are_preserved(self) -> None: - results = self.publish(failed_budget=True) - self.assertEqual([r["state"] for r in results], ["success", "failure"]) - - def test_older_evaluation_cannot_overwrite_newer_failure(self) -> None: - for order in ("124:1:125:1", "123:2:125:1", "123:1:125:1", "123:1:123:2"): - with self.subTest(order=order): - previous = { - "context": "Castiron / custom-code budget", - "creator": {"login": "github-actions[bot]"}, - "state": "failure", - "description": f"Failed against newer main. [evaluation {order}]", - "target_url": f"https://github.com/openai/example/actions/runs/{order.split(':')[2]}", - } - self.assertEqual(self.publish(previous_statuses=[previous]), []) - - def test_publication_guard_allows_current_retry_and_ignores_unrelated_statuses(self) -> None: - previous = { - "context": "Castiron / custom-code budget", - "creator": {"login": "github-actions[bot]"}, - "description": "Failed. [evaluation 123:1:123:1]", - "target_url": "https://github.com/openai/example/actions/runs/123", - } - for overrides in ( - {}, # A partial publication can retry the same evaluation. - { - "description": "Failed. [evaluation 122:9:999:9]", - "target_url": "https://github.com/openai/example/actions/runs/999", - }, - {"description": "Legacy status without evaluation marker"}, - {"context": "Other check", "description": "[evaluation 999:9:999:9]"}, - {"creator": {"login": "someone"}, "description": "[evaluation 999:9:999:9]"}, - {"description": "[evaluation 999:9:999:9]", "target_url": "https://example.com/999"}, - ): - with self.subTest(overrides=overrides): - results = self.publish(previous_statuses=[{**previous, **overrides}]) - self.assertEqual(len(results), 2) - self.assertTrue(all(r["state"] == "success" for r in results)) - self.assertTrue(all(len(r["description"]) <= 140 for r in results)) - - if __name__ == "__main__": unittest.main() diff --git a/scripts/castiron/test_custom_code_review_revocation.py b/scripts/castiron/test_custom_code_review_revocation.py index 90b39773f8..f7285795a8 100644 --- a/scripts/castiron/test_custom_code_review_revocation.py +++ b/scripts/castiron/test_custom_code_review_revocation.py @@ -209,6 +209,37 @@ def test_unrelated_comment_cannot_invalidate_valid_approval_or_request_recovery( payload["forbid_queue"] = True self.assertEqual(self.invoke(payload), []) + def test_later_comment_preserves_green_pr_and_queue_statuses(self) -> None: + for state in ("COMMENTED", "PENDING"): + for paginated in (False, True): + with self.subTest(state=state, paginated=paginated): + payload = self.recovery() + approval = self.approval() + comment = { + **approval, + "fullDatabaseId": "102", + "state": state, + "body": "Follow-up discussion", + } + payload["review_nodes"] = [approval, comment] + if paginated: + payload.pop("review_nodes") + payload["review_pages"] = [ + { + "expected_cursor": None, + "nodes": [comment], + "pageInfo": {"hasNextPage": True, "endCursor": "more"}, + }, + { + "expected_cursor": "more", + "nodes": [approval], + "pageInfo": {"hasNextPage": False}, + }, + ] + for status in payload["previous_statuses"]: + status["state"] = "success" + self.assertEqual(self.invoke(payload), []) + def test_ineligible_review_cannot_request_recovery(self) -> None: for overrides in ( {"state": "COMMENTED"}, @@ -280,7 +311,7 @@ def test_full_review_id_handles_current_and_beyond_safe_integer_ids(self) -> Non **self.approval(), "databaseId": None, "fullDatabaseId": "9007199254740993", - "state": "COMMENTED", + "state": "CHANGES_REQUESTED", }, ] self.assertEqual(len(self.invoke(payload)), 6) @@ -350,11 +381,13 @@ def test_untrusted_marker_cannot_suppress_recovery(self) -> None: payload["runs_by_id"]["21"][field] = value self.assertEqual(len(self.invoke(payload)), 6) - def test_dismissal_edit_away_and_later_comment_revoke_even_for_other_signal_actor(self) -> None: + def test_dismissal_edit_away_and_blocking_review_revoke_even_for_other_signal_actor( + self, + ) -> None: for changed in ( {"state": "DISMISSED"}, {"body": ""}, - {"state": "COMMENTED", "fullDatabaseId": "102"}, + {"state": "CHANGES_REQUESTED", "fullDatabaseId": "102"}, ): payload = self.payload() payload["review_nodes"] = [{**self.approval(), **changed}] @@ -363,7 +396,9 @@ def test_dismissal_edit_away_and_later_comment_revoke_even_for_other_signal_acto payload["review_pages"] = [ { "expected_cursor": None, - "nodes": [{**self.approval(), "fullDatabaseId": "102", "state": "COMMENTED"}], + "nodes": [ + {**self.approval(), "fullDatabaseId": "102", "state": "CHANGES_REQUESTED"} + ], "pageInfo": {"hasNextPage": True, "endCursor": "more"}, }, { @@ -633,11 +668,115 @@ def test_no_success_or_foreign_run_can_be_published(self) -> None: ) self.assertTrue(all(item.get("state") != "success" for item in result)) + def main_advance_payload(self) -> dict[str, Any]: + payload = self.payload() + main = "d" * 40 + run = { + **source_run(main, "push"), + "head_branch": "main", + "head_repository": {"full_name": "openai/example"}, + } + payload["run"] = run + payload["main_sha"] = main + payload["context"]["payload"]["workflow_run"] = dict(run) + payload["runs_by_sha"]["c" * 40][0]["head_branch"] = "gh-readonly-queue/next/pr-3-example" + return payload + + def invoke_main_advance(self, payload: dict[str, Any]) -> list[dict[str, Any]]: + return run_publisher("Re-evaluate live next queue after main advances", payload) + + def test_main_advance_revokes_success_before_normal_queue_rerun(self) -> None: + result = self.invoke_main_advance(self.main_advance_payload()) + self.assertEqual([item.get("state") for item in result[:2]], ["pending", "pending"]) + self.assertEqual({item["sha"] for item in result[:2]}, {"c" * 40}) + self.assertTrue(all(item["context"].startswith("Castiron next /") for item in result[:2])) + self.assertEqual([item["run_id"] for item in result[2:]], [14]) + self.assertNotIn("success", [item.get("state") for item in result]) + + def test_old_main_push_does_not_invalidate_newer_qualified_candidates(self) -> None: + payload = self.main_advance_payload() + payload["main_sha"] = "e" * 40 + self.assertEqual(self.invoke_main_advance(payload), []) + + def test_main_advance_rejects_foreign_or_non_main_signals(self) -> None: + for field, value in ( + ("head_repository", {"full_name": "other/repo"}), + ("repository", {"full_name": "other/repo"}), + ("head_branch", "next"), + ("event", "pull_request"), + ): + with self.subTest(field=field): + payload = self.main_advance_payload() + payload["run"][field] = value + with self.assertRaises(subprocess.CalledProcessError): + self.invoke_main_advance(payload) + payload = self.main_advance_payload() + payload["run"]["workflow_id"] = 999 + self.assertEqual(self.invoke_main_advance(payload), []) + + def test_main_advance_invalidates_every_queue_page_before_dispatch(self) -> None: + payload = self.main_advance_payload() + payload["queue_pages"] = [ + { + "expected_cursor": None, + "nodes": [{"headCommit": None}, {"headCommit": {"oid": "c" * 40}}], + "pageInfo": {"hasNextPage": True, "endCursor": "second"}, + }, + { + "expected_cursor": "second", + "nodes": [{"headCommit": {"oid": "e" * 40}}, {"headCommit": None}], + "pageInfo": {"hasNextPage": False, "endCursor": None}, + }, + ] + result = self.invoke_main_advance(payload) + self.assertEqual([item.get("state") for item in result[:4]], ["pending"] * 4) + self.assertEqual({item["sha"] for item in result[:4]}, {"c" * 40, "e" * 40}) + self.assertEqual([item["run_id"] for item in result[4:]], [14]) + + def test_main_advance_still_rejects_malformed_non_null_heads(self) -> None: + for head in ({}, {"oid": None}, {"oid": "g" * 40}): + with self.subTest(head=head): + payload = self.main_advance_payload() + payload["queue_nodes"][0]["headCommit"] = head + with self.assertRaises(subprocess.CalledProcessError): + self.invoke_main_advance(payload) + + def test_main_advance_does_not_rerun_wrong_queue_or_foreign_workflows(self) -> None: + for field, value in ( + ("repository", {"full_name": "other/repo"}), + ("workflow_id", 999), + ("head_branch", "gh-readonly-queue/main/pr-3-example"), + ): + with self.subTest(field=field): + payload = self.main_advance_payload() + payload["runs_by_sha"]["c" * 40][0][field] = value + result = self.invoke_main_advance(payload) + self.assertEqual([item.get("state") for item in result], ["pending", "pending"]) + + def test_main_advance_leaves_newer_running_evaluation_to_publish(self) -> None: + payload = self.main_advance_payload() + runs = payload["runs_by_sha"]["c" * 40] + runs.append({**runs[0], "id": 15, "status": "in_progress"}) + result = self.invoke_main_advance(payload) + self.assertEqual([item.get("state") for item in result], ["pending", "pending"]) + + def test_main_advance_with_empty_queue_has_no_writes(self) -> None: + payload = self.main_advance_payload() + payload["queue_nodes"] = [] + payload["queue_absent"] = True + self.assertEqual(self.invoke_main_advance(payload), []) + def test_signal_is_unprivileged_and_writers_serialize(self) -> None: producer = (WORKFLOW.parent / "castiron-custom-code.yml").read_text() self.assertIn("pull_request_review:\n types: [submitted, edited, dismissed]", producer) signal = producer.split(" queue-signal:\n", 1)[1].split(" candidate-tests:\n", 1)[0] self.assertIn("permissions: {}", signal) + self.assertIn("push:\n branches: [main]", producer) + main_change = ( + WORKFLOW.read_text().split(" main-change:\n", 1)[1].split(" compute:\n", 1)[0] + ) + self.assertNotIn("checkout@", main_change) + self.assertNotIn("download-artifact", main_change) self.assertNotIn("checkout@", signal) handler = WORKFLOW.read_text() revoke = handler.split(" review-change:\n", 1)[1].split(" compute:\n", 1)[0] @@ -646,7 +785,7 @@ def test_signal_is_unprivileged_and_writers_serialize(self) -> None: self.assertIn("path.join(root, 'custom_code_policy.py'), 'review-required'", revoke) self.assertNotIn("download-artifact", revoke) self.assertEqual( - handler.count("group: castiron-budget-writes-${{ github.repository_id }}"), 2 + handler.count("group: castiron-budget-writes-${{ github.repository_id }}"), 3 ) def test_direct_head_review_discovery_rejects_shared_commit_from_other_sources( diff --git a/scripts/castiron/test_custom_code_status_publication.py b/scripts/castiron/test_custom_code_status_publication.py index 7fe6edf2f5..0aca402217 100644 --- a/scripts/castiron/test_custom_code_status_publication.py +++ b/scripts/castiron/test_custom_code_status_publication.py @@ -29,7 +29,9 @@ def publish( branch: str = "main", evaluated_branch: str | None = None, policy_changed: bool = False, + authenticated_main_sha: str | None = None, bootstrap: str = "false", + main_snapshot: str = "", policy_sha: str | None = None, selected_policy_matches: bool = True, queue_absent: bool = False, @@ -106,6 +108,7 @@ def publish( ), "SELECTED_BRANCH": branch, "SELECTED_BOOTSTRAP": bootstrap, + "MAIN_SNAPSHOT": main_snapshot, "SELECTED_BASE_SHA": base, "SELECTED_HEAD_SHA": head, "SELECTED_POLICY_SHA": (policy_sha or base) @@ -120,6 +123,9 @@ def publish( "PUBLISH_ATTEMPT": "1", }, } + payload["env"]["AUTHENTICATED_MAIN_SHA"] = ( + payload["main_sha"] if authenticated_main_sha is None else authenticated_main_sha + ) if policy_pull: payload["pulls_by_number"] = {policy_pull["number"]: policy_pull} if same_head_next_pr: @@ -187,6 +193,49 @@ def test_live_human_review_predicate_matches_selector(self) -> None: ) self.assertEqual([w["state"] for w in writes], [expected, expected]) + def test_final_publisher_preserves_approval_across_comments(self) -> None: + head = "b" * 40 + approval = { + "id": 10, + "state": "APPROVED", + "commit_id": head, + "user": {"login": "human", "type": "User"}, + "author_association": "MEMBER", + "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + head, + } + comment = {**approval, "id": 11, "state": "COMMENTED", "body": "A follow-up comment"} + cases = [ + ([approval, comment], "success"), + ([comment, approval], "success"), + ([comment], "failure"), + ([{**approval, "state": "DISMISSED"}, comment], "failure"), + ([{**approval, "body": "Attestation removed"}, comment], "failure"), + ( + [ + approval, + {**approval, "id": 12, "state": "CHANGES_REQUESTED"}, + {**comment, "id": 13}, + ], + "failure", + ), + ( + [ + {**approval, "state": "CHANGES_REQUESTED"}, + {**approval, "id": 12}, + {**comment, "id": 13}, + ], + "success", + ), + ] + for reviews, expected in cases: + with self.subTest(reviews=reviews): + writes = self.publish( + branch="next", + policy_reviews=[{"number": 3, "head_sha": head}], + reviews=reviews, + ) + self.assertEqual([w["state"] for w in writes], [expected] * 2) + def test_final_publisher_preserves_review_identity_precision(self) -> None: head = "b" * 40 approval = { @@ -197,7 +246,7 @@ def test_final_publisher_preserves_review_identity_precision(self) -> None: "author_association": "MEMBER", "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + head, } - replacement = {**approval, "id": 9007199254740993, "state": "COMMENTED"} + replacement = {**approval, "id": 9007199254740993, "state": "CHANGES_REQUESTED"} for reviews in ([approval, replacement], [replacement, approval]): with self.subTest(reviews=reviews): writes = self.publish( @@ -261,6 +310,41 @@ def publish(**kwargs: Any) -> list[str]: ["failure"] * 2, ) + def test_next_publication_binds_authenticated_main(self) -> None: + # The reviewed checker pin may remain old. The evaluation's main snapshot + # must still be current so an old publisher cannot undo invalidation. + for event in ("pull_request", "merge_group"): + for authenticated_main, expected in ( + ("a" * 40, "failure"), + ("d" * 40, "success"), + ("", "failure"), + ("invalid", "failure"), + ): + with self.subTest(event=event, authenticated_main=authenticated_main): + writes = self.publish( + event_name=event, + branch="next", + policy_changed=True, + authenticated_main_sha=authenticated_main, + ) + self.assertEqual([item["state"] for item in writes], [expected] * 2) + + def test_main_snapshot_publication_rechecks_live_main(self) -> None: + for event in ("pull_request", "merge_group"): + for moved in (False, True): + with self.subTest(event=event, moved=moved): + writes = self.publish( + event_name=event, + branch="next", + policy_sha="e" * 40, + main_snapshot="e" * 40, + policy_changed=moved, + ) + self.assertEqual( + [w["state"] for w in writes], + ["failure", "failure"] if moved else ["success", "success"], + ) + def test_bootstrap_rechecks_authenticated_main(self) -> None: for event in ("pull_request", "merge_group"): for bootstrap, changed, expected in ( @@ -332,84 +416,88 @@ def test_freshness_is_rechecked_after_review_pagination(self) -> None: def test_queue_policy_approval_dismissal_after_selection_fails(self) -> None: candidate = "e" * 40 - pull: dict[str, Any] = { - "number": 7, - "user": {"login": "author"}, - "state": "open", - "head": {"sha": candidate}, - "base": {"ref": "next", "repo": {"full_name": "openai/example"}}, - } - approved = { - "id": 1, - "state": "APPROVED", - "commit_id": candidate, - "user": {"login": "human", "type": "User"}, - "author_association": "MEMBER", - "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + candidate, - } - for state in ("APPROVED", "DISMISSED", "CHANGES_REQUESTED"): - with self.subTest(state=state): + for queue_head in (source_head(candidate), {"sha": candidate}): + with self.subTest(queue_head=queue_head): + pull: dict[str, Any] = { + "number": 7, + "user": {"login": "author"}, + "state": "open", + "head": queue_head, + "base": {"ref": "next", "repo": {"full_name": "openai/example"}}, + } + approved = { + "id": 1, + "state": "APPROVED", + "commit_id": candidate, + "user": {"login": "human", "type": "User"}, + "author_association": "MEMBER", + "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + candidate, + } + for state in ("APPROVED", "DISMISSED", "CHANGES_REQUESTED"): + with self.subTest(state=state): + writes = self.publish( + event_name="merge_group", + branch="next", + policy_pull=pull, + policy_reviews=[{"number": 7, "head_sha": candidate}], + reviews=[{**approved, "state": state}], + ) + self.assertEqual( + [w["state"] for w in writes], + ["success" if state == "APPROVED" else "failure"] * 2, + ) + # Ordinary automated approvals may keep the queue live, but cannot replace + # the independently required human policy approval. writes = self.publish( event_name="merge_group", branch="next", policy_pull=pull, policy_reviews=[{"number": 7, "head_sha": candidate}], - reviews=[{**approved, "state": state}], - ) - self.assertEqual( - [w["state"] for w in writes], - ["success" if state == "APPROVED" else "failure"] * 2, + reviews=[ + {**approved, "state": "DISMISSED"}, + { + **approved, + "id": 2, + "user": {"login": "automated", "type": "User"}, + "body": "", + }, + ], ) - # Ordinary automated approvals may keep the queue live, but cannot replace - # the independently required human policy approval. - writes = self.publish( - event_name="merge_group", - branch="next", - policy_pull=pull, - policy_reviews=[{"number": 7, "head_sha": candidate}], - reviews=[ - {**approved, "state": "DISMISSED"}, - { - **approved, - "id": 2, - "user": {"login": "automated", "type": "User"}, - "body": "", - }, - ], - ) - self.assertEqual([w["state"] for w in writes], ["failure", "failure"]) + self.assertEqual([w["state"] for w in writes], ["failure", "failure"]) def test_queue_policy_constituent_changes_fail_closed(self) -> None: candidate = "e" * 40 - pull: dict[str, Any] = { - "number": 7, - "user": {"login": "author"}, - "state": "open", - "head": {"sha": candidate}, - "base": {"ref": "next", "repo": {"full_name": "openai/example"}}, - } - approved = { - "id": 1, - "state": "APPROVED", - "commit_id": candidate, - "user": {"login": "human", "type": "User"}, - "author_association": "MEMBER", - "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + candidate, - } - for changed in [ - {**pull, "state": "closed"}, - {**pull, "head": {"sha": "f" * 40}}, - {**pull, "base": {**pull["base"], "ref": "main"}}, - ]: - with self.subTest(changed=changed): - writes = self.publish( - event_name="merge_group", - branch="next", - policy_pull=changed, - policy_reviews=[{"number": 7, "head_sha": candidate}], - reviews=[approved], - ) - self.assertEqual([w["state"] for w in writes], ["failure", "failure"]) + for queue_head in (source_head(candidate), {"sha": candidate}): + with self.subTest(queue_head=queue_head): + pull: dict[str, Any] = { + "number": 7, + "user": {"login": "author"}, + "state": "open", + "head": queue_head, + "base": {"ref": "next", "repo": {"full_name": "openai/example"}}, + } + approved = { + "id": 1, + "state": "APPROVED", + "commit_id": candidate, + "user": {"login": "human", "type": "User"}, + "author_association": "MEMBER", + "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + candidate, + } + for changed in [ + {**pull, "state": "closed"}, + {**pull, "head": {"sha": "f" * 40}}, + {**pull, "base": {**pull["base"], "ref": "main"}}, + ]: + with self.subTest(changed=changed): + writes = self.publish( + event_name="merge_group", + branch="next", + policy_pull=changed, + policy_reviews=[{"number": 7, "head_sha": candidate}], + reviews=[approved], + ) + self.assertEqual([w["state"] for w in writes], ["failure", "failure"]) def test_missing_policy_review_contract_fails_closed(self) -> None: self.assertEqual( diff --git a/scripts/castiron/test_custom_code_strict.py b/scripts/castiron/test_custom_code_strict.py index 41acb56f9d..b258213e63 100644 --- a/scripts/castiron/test_custom_code_strict.py +++ b/scripts/castiron/test_custom_code_strict.py @@ -18,6 +18,14 @@ class StrictTests(GitTestCase): workflow = ".github/workflows/castiron-custom-code.yml" + tooling_dir = "scripts/castiron" + checker_path = f"{tooling_dir}/custom_code_budget.py" + manifest_path = f"{tooling_dir}/generated-files.json" + alternate_manifest_path = ( + "scripts/castiron/generated-files.json" + if tooling_dir == "generated/castiron" + else "generated/castiron/generated-files.json" + ) def setUp(self) -> None: # pyright: ignore[reportImplicitOverride] super().setUp() @@ -38,7 +46,7 @@ def setUp(self) -> None: # pyright: ignore[reportImplicitOverride] def inventory(self, paths: frozenset[bytes]) -> None: # Checkpoint code is data. A verifier must never execute this statement. self.write( - "scripts/castiron/custom_code_budget.py", + self.checker_path, "raise AssertionError('checkpoint code must not execute')\n" f"GENERATED_GITHUB_PATHS = frozenset({set(paths)!r})\n", ) @@ -52,11 +60,11 @@ def manifest(self, extra: tuple[str, ...] = ()) -> None: for p in ( "generated.py", *(path.decode() for path in budget.GENERATED_GITHUB_PATHS), - "scripts/castiron/custom_code_budget.py", + self.checker_path, *extra, ) } - self.write(budget.MANIFEST.decode(), json.dumps({"schema_version": 1, "files": files})) + self.write(self.manifest_path, json.dumps({"schema_version": 1, "files": files})) def evaluate(self) -> dict[str, Any]: return budget.evaluate(self.repo, self.base, self.commit(), public=False)[0] @@ -87,8 +95,33 @@ def test_strict_main_checks_migration_target_and_candidate(self) -> None: self.assertIn(self.workflow, result["strict_violations"][0]) def test_exact_main_snapshot_bootstrap_verifies_real_checkpoints(self) -> None: + self.assert_exact_main_snapshot_bootstrap() + + def test_exact_main_snapshot_bootstrap_preserves_go_ratchet(self) -> None: + self.assert_exact_main_snapshot_bootstrap(ratchet="go") + + def test_exact_main_snapshot_bootstrap_accepts_ssh_origin(self) -> None: + self.assert_exact_main_snapshot_bootstrap(origin="git@github.com:openai/example.git") + + def test_next_preparation_rejects_wrong_origin_before_network(self) -> None: + self.git("remote", "add", "origin", "https://github.com/attacker/example.git") + for origin in ( + "https://github.com/attacker/example.git", + "git@github.com:attacker/example.git", + "https://github.com/openai/example-internal.git", + "https://github.com.example.org/openai/example.git", + ): + with self.subTest(origin=origin), mock.patch.object(budget, "read_public_api") as api: + self.git("remote", "set-url", "origin", origin) + with self.assertRaisesRegex(ValueError, "public repository as origin"): + budget.prepare_next_baseline(self.repo, "openai/example", "codex/prepare") + api.assert_not_called() + + def assert_exact_main_snapshot_bootstrap( + self, ratchet: str | None = None, *, origin: str | None = None + ) -> None: repository = "openai/example" - self.git("remote", "add", "origin", f"https://github.com/{repository}.git") + self.git("remote", "add", "origin", origin or f"https://github.com/{repository}.git") def public_snapshot(generated: str, parent: str | None) -> str: stats = report.read_stats(self.repo, self.git("rev-parse", "HEAD")) @@ -108,7 +141,7 @@ def public_snapshot(generated: str, parent: str | None) -> str: self.write("generated.py", "legacy custom edit\n") legacy = self.commit("legacy unpinned next") self.write("generated.py", "generated\n") - checker = self.repo / "scripts/castiron/custom_code_budget.py" + checker = self.repo / self.checker_path checker.write_text(checker.read_text() + "NEXT_POLICY_VERSION = 1\n") self.manifest() # Generated checkpoints exclude integration metadata and repository policy. @@ -119,6 +152,8 @@ def public_snapshot(generated: str, parent: str | None) -> str: self.git("update-ref", "refs/remotes/origin/codegen/test", current_public) stats_path.write_text(stats_path.read_text() + f"public_codegen_sha: {current_public}\n") strict = {"schema_version": 2, "max_custom_patch_lines": 0, "generated_files": "strict"} + if ratchet is not None: + strict["ratchet"] = ratchet self.write(budget.POLICY, json.dumps(strict)) main = self.commit("verified strict main") self.git("update-ref", "refs/remotes/origin/main", main) @@ -156,6 +191,10 @@ def public_api(path: str) -> dict[str, Any]: ): prepared = budget.prepare_next_baseline(self.repo, repository, "codex/adopt-main") candidate = prepared["head_sha"] + self.assertEqual( + budget.read_policy(self.repo, candidate), + {**strict, "next_policy": {"repository": repository, "main_sha": main}}, + ) self.assertEqual(self.git("rev-parse", "refs/heads/codex/adopt-main"), candidate) self.assertEqual(self.git("write-tree"), original_index) self.assertEqual(self.git("rev-parse", "HEAD"), main) @@ -232,11 +271,11 @@ def transport(repo: Path, *args: str, **kwargs: Any) -> bytes: with self.subTest(object_store=mode): queue = mode in ("queue", "moved-main") revision = wrapper if queue else candidate - api = GitHubAPI(main, revision, "merge_group" if queue else "pull_request") - api.next = legacy - api.pull["base"]["ref"] = "next" + hosted_api = GitHubAPI(main, revision, "merge_group" if queue else "pull_request") + hosted_api.next = legacy + hosted_api.pull["base"]["ref"] = "next" if queue: - api.run["head_branch"] = "gh-readonly-queue/next/pr-3-test" + hosted_api.run["head_branch"] = "gh-readonly-queue/next/pr-3-test" objects = self.repo / f"bootstrap-{mode}.git" out = self.repo / f"bootstrap-{mode}-report" receipt = { @@ -251,8 +290,8 @@ def transport(repo: Path, *args: str, **kwargs: Any) -> bytes: with ( mock.patch.object(report, "git", side_effect=transport), mock.patch.object(budget.report, "git", side_effect=transport), - mock.patch.object(report, "api", side_effect=api), - mock.patch.object(budget.report, "api", side_effect=api), + mock.patch.object(report, "api", side_effect=hosted_api), + mock.patch.object(budget.report, "api", side_effect=hosted_api), mock.patch.object( budget, "queued_entries", return_value=[(candidate, wrapper)] ), @@ -270,7 +309,10 @@ def transport(repo: Path, *args: str, **kwargs: Any) -> bytes: budget.github_evaluate( objects, repository, - {"repository": {"full_name": repository}, "workflow_run": api.run}, + { + "repository": {"full_name": repository}, + "workflow_run": hosted_api.run, + }, main, selection=receipt, ) @@ -278,7 +320,10 @@ def transport(repo: Path, *args: str, **kwargs: Any) -> bytes: result, _ = budget.github_evaluate( objects, repository, - {"repository": {"full_name": repository}, "workflow_run": api.run}, + { + "repository": {"full_name": repository}, + "workflow_run": hosted_api.run, + }, main, out if mode == "reused-pr" else None, selection=receipt, @@ -413,7 +458,7 @@ def test_candidate_cannot_rewrite_its_manifest_or_disable_strict(self) -> None: def test_missing_manifest_cannot_activate(self) -> None: self.git("checkout", "-q", self.generated) - (self.repo / budget.MANIFEST.decode()).unlink() + (self.repo / self.manifest_path).unlink() checkpoint = self.commit(self.git("show", "-s", "--format=%B", self.generated)) self.git("update-ref", "refs/remotes/origin/codegen/test", checkpoint) self.git("checkout", "-q", "main") @@ -435,12 +480,10 @@ def test_legitimate_new_generated_checkpoint_passes(self) -> None: checkpoint = self.commit(self.git("show", "-s", "--format=%B", self.generated)) self.git("update-ref", "refs/remotes/origin/codegen/test", checkpoint) self.git("checkout", "-q", "main") - for path in ("generated.py", self.workflow, budget.MANIFEST.decode()): + for path in ("generated.py", self.workflow, self.manifest_path): self.write(path, self.git("show", f"{checkpoint}:{path}") + "\n") # Preserve exact manifest bytes (the fixture writes JSON without a newline). - self.write( - budget.MANIFEST.decode(), self.git("show", f"{checkpoint}:{budget.MANIFEST.decode()}") - ) + self.write(self.manifest_path, self.git("show", f"{checkpoint}:{self.manifest_path}")) stats = (self.repo / ".castiron.stats.yml").read_text() self.write( ".castiron.stats.yml", @@ -474,9 +517,9 @@ def test_activation_is_policy_only_and_checks_workflows(self) -> None: def test_manifest_must_cover_snapshot_and_match_its_bytes(self) -> None: self.git("checkout", "-q", self.generated) - manifest = json.loads((self.repo / budget.MANIFEST.decode()).read_text()) + manifest = json.loads((self.repo / self.manifest_path).read_text()) manifest["files"].pop("generated.py") - self.write(budget.MANIFEST.decode(), json.dumps(manifest)) + self.write(self.manifest_path, json.dumps(manifest)) with self.assertRaisesRegex(ValueError, "cover.*exactly"): budget.generated_manifest(self.repo, self.commit()) self.manifest() @@ -486,10 +529,10 @@ def test_manifest_must_cover_snapshot_and_match_its_bytes(self) -> None: def test_relocated_tooling_preserves_ownership_and_retirement_checks(self) -> None: self.git("checkout", "--detach", "-q", self.generated) - old_manifest = budget.MANIFEST.decode() - new_manifest = "generated/castiron/generated-files.json" - old_checker = "scripts/castiron/custom_code_budget.py" - new_checker = "generated/castiron/custom_code_budget.py" + old_manifest = self.manifest_path + new_manifest = self.alternate_manifest_path + old_checker = self.checker_path + new_checker = str(Path(new_manifest).with_name("custom_code_budget.py")) manifest = json.loads((self.repo / old_manifest).read_text()) manifest["files"][new_checker] = manifest["files"].pop(old_checker) checker = (self.repo / old_checker).read_text() @@ -524,8 +567,8 @@ def test_relocated_tooling_preserves_ownership_and_retirement_checks(self) -> No def test_checkpoint_rejects_ambiguous_manifest_locations(self) -> None: self.git("checkout", "--detach", "-q", self.generated) self.write( - "generated/castiron/generated-files.json", - (self.repo / budget.MANIFEST.decode()).read_text(), + self.alternate_manifest_path, + (self.repo / self.manifest_path).read_text(), ) with self.assertRaisesRegex(ValueError, "ambiguous"): budget.generated_manifest(self.repo, self.commit()) @@ -533,10 +576,10 @@ def test_checkpoint_rejects_ambiguous_manifest_locations(self) -> None: def test_relocated_manifest_requires_colocated_checker_inventory(self) -> None: self.git("checkout", "--detach", "-q", self.generated) self.write( - "generated/castiron/generated-files.json", - (self.repo / budget.MANIFEST.decode()).read_text(), + self.alternate_manifest_path, + (self.repo / self.manifest_path).read_text(), ) - (self.repo / budget.MANIFEST.decode()).unlink() + (self.repo / self.manifest_path).unlink() with self.assertRaisesRegex(ValueError, "missing its generated GitHub inventory"): budget.generated_manifest(self.repo, self.commit()) @@ -569,7 +612,7 @@ def test_private_manifest_checks_excluded_workflows_and_actions(self) -> None: self.write(path, "generated workflow or action\n") self.inventory(budget.GENERATED_GITHUB_PATHS) self.manifest((path,)) - manifest = json.loads((self.repo / budget.MANIFEST.decode()).read_text()) + manifest = json.loads((self.repo / self.manifest_path).read_text()) manifest["files"][path] = { "mode": "100644", "sha256": hashlib.sha256((self.repo / path).read_bytes()).hexdigest(), @@ -580,7 +623,7 @@ def test_private_manifest_checks_excluded_workflows_and_actions(self) -> None: self.write(path, "unrecorded change\n") else: (self.repo / path).chmod(0o755) - self.write(budget.MANIFEST.decode(), json.dumps(manifest)) + self.write(self.manifest_path, json.dumps(manifest)) error = ( "omits generated GitHub inventory path" if mutation == "omit" @@ -627,14 +670,14 @@ def test_manifest_cannot_claim_sdk_owned_workflows_or_actions(self) -> None: with self.subTest(path=path, keep_blob=keep_blob): self.git("checkout", "--detach", "-q", self.generated) self.write(path, "SDK-owned configuration\n") - manifest = json.loads((self.repo / budget.MANIFEST.decode()).read_text()) + manifest = json.loads((self.repo / self.manifest_path).read_text()) manifest["files"][path] = { "mode": "100644", "sha256": hashlib.sha256((self.repo / path).read_bytes()).hexdigest(), } if not keep_blob: (self.repo / path).unlink() - self.write(budget.MANIFEST.decode(), json.dumps(manifest)) + self.write(self.manifest_path, json.dumps(manifest)) with self.assertRaisesRegex(ValueError, "SDK-owned GitHub path"): budget.generated_manifest(self.repo, self.commit()) @@ -646,9 +689,9 @@ def test_public_snapshot_requires_every_inventory_identity(self) -> None: self.inventory(budget.GENERATED_GITHUB_PATHS | {path.encode()}) self.manifest((path,)) (self.repo / path).unlink() - manifest = json.loads((self.repo / budget.MANIFEST.decode()).read_text()) + manifest = json.loads((self.repo / self.manifest_path).read_text()) del manifest["files"][path] - self.write(budget.MANIFEST.decode(), json.dumps(manifest)) + self.write(self.manifest_path, json.dumps(manifest)) with self.assertRaisesRegex(ValueError, "omits generated GitHub inventory path"): budget.generated_manifest( self.repo, self.commit("incomplete public checkpoint") @@ -669,9 +712,9 @@ def test_public_snapshot_keeps_strict_workflow_identity_without_private_blob(sel def test_generated_deletion_requires_removing_retired_file(self) -> None: self.git("checkout", "-q", self.generated) (self.repo / "generated.py").unlink() - manifest = json.loads((self.repo / budget.MANIFEST.decode()).read_text()) + manifest = json.loads((self.repo / self.manifest_path).read_text()) del manifest["files"]["generated.py"] - self.write(budget.MANIFEST.decode(), json.dumps(manifest)) + self.write(self.manifest_path, json.dumps(manifest)) new = self.commit() self.assertEqual(budget.verify_generated(self.repo, new, new, previous=self.generated), []) self.write("generated.py", "retained handwritten replacement\n") @@ -683,13 +726,13 @@ def test_generated_file_can_become_a_directory(self) -> None: self.git("checkout", "-q", self.generated) (self.repo / "generated.py").unlink() self.write("generated.py/nested.py", "new generated file\n") - manifest = json.loads((self.repo / budget.MANIFEST.decode()).read_text()) + manifest = json.loads((self.repo / self.manifest_path).read_text()) del manifest["files"]["generated.py"] manifest["files"]["generated.py/nested.py"] = { "mode": "100644", "sha256": hashlib.sha256(b"new generated file\n").hexdigest(), } - self.write(budget.MANIFEST.decode(), json.dumps(manifest)) + self.write(self.manifest_path, json.dumps(manifest)) new = self.commit() for worktree in (False, True): with self.subTest(worktree=worktree): diff --git a/src/openai/types/beta/beta_response_input_file.py b/src/openai/types/beta/beta_response_input_file.py index 55084bb2a4..55e8c5eed1 100644 --- a/src/openai/types/beta/beta_response_input_file.py +++ b/src/openai/types/beta/beta_response_input_file.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -51,6 +51,6 @@ class BetaResponseInputFile(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_input_file_content.py b/src/openai/types/beta/beta_response_input_file_content.py index 3e816bca7a..1be461b384 100644 --- a/src/openai/types/beta/beta_response_input_file_content.py +++ b/src/openai/types/beta/beta_response_input_file_content.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -51,6 +51,6 @@ class BetaResponseInputFileContent(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_input_file_content_param.py b/src/openai/types/beta/beta_response_input_file_content_param.py index 33b8c7c5b1..1fd4c06079 100644 --- a/src/openai/types/beta/beta_response_input_file_content_param.py +++ b/src/openai/types/beta/beta_response_input_file_content_param.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -51,6 +51,6 @@ class BetaResponseInputFileContentParam(TypedDict, total=False): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_input_file_param.py b/src/openai/types/beta/beta_response_input_file_param.py index 034f26d03f..e4793afed8 100644 --- a/src/openai/types/beta/beta_response_input_file_param.py +++ b/src/openai/types/beta/beta_response_input_file_param.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -51,6 +51,6 @@ class BetaResponseInputFileParam(TypedDict, total=False): prompt_cache_breakpoint: PromptCacheBreakpoint """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_input_image.py b/src/openai/types/beta/beta_response_input_image.py index 70b6f32b65..bc955994b2 100644 --- a/src/openai/types/beta/beta_response_input_image.py +++ b/src/openai/types/beta/beta_response_input_image.py @@ -12,7 +12,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -49,6 +49,6 @@ class BetaResponseInputImage(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_input_image_content.py b/src/openai/types/beta/beta_response_input_image_content.py index ae4631afe6..3eeb23fc37 100644 --- a/src/openai/types/beta/beta_response_input_image_content.py +++ b/src/openai/types/beta/beta_response_input_image_content.py @@ -12,7 +12,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -49,6 +49,6 @@ class BetaResponseInputImageContent(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_input_image_content_param.py b/src/openai/types/beta/beta_response_input_image_content_param.py index bd0a5c78f3..8aa6b9c380 100644 --- a/src/openai/types/beta/beta_response_input_image_content_param.py +++ b/src/openai/types/beta/beta_response_input_image_content_param.py @@ -13,7 +13,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -50,6 +50,6 @@ class BetaResponseInputImageContentParam(TypedDict, total=False): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_input_image_param.py b/src/openai/types/beta/beta_response_input_image_param.py index 97ba647010..4962bd6688 100644 --- a/src/openai/types/beta/beta_response_input_image_param.py +++ b/src/openai/types/beta/beta_response_input_image_param.py @@ -13,7 +13,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -50,6 +50,6 @@ class BetaResponseInputImageParam(TypedDict, total=False): prompt_cache_breakpoint: PromptCacheBreakpoint """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_input_text.py b/src/openai/types/beta/beta_response_input_text.py index 8c97d83d41..1bb793a313 100644 --- a/src/openai/types/beta/beta_response_input_text.py +++ b/src/openai/types/beta/beta_response_input_text.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -33,6 +33,6 @@ class BetaResponseInputText(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_input_text_content.py b/src/openai/types/beta/beta_response_input_text_content.py index d29d388a44..6200d933d0 100644 --- a/src/openai/types/beta/beta_response_input_text_content.py +++ b/src/openai/types/beta/beta_response_input_text_content.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -33,6 +33,6 @@ class BetaResponseInputTextContent(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_input_text_content_param.py b/src/openai/types/beta/beta_response_input_text_content_param.py index dad7072a9a..8fa65d724b 100644 --- a/src/openai/types/beta/beta_response_input_text_content_param.py +++ b/src/openai/types/beta/beta_response_input_text_content_param.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -33,6 +33,6 @@ class BetaResponseInputTextContentParam(TypedDict, total=False): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_input_text_param.py b/src/openai/types/beta/beta_response_input_text_param.py index 071ca2268e..97598340b3 100644 --- a/src/openai/types/beta/beta_response_input_text_param.py +++ b/src/openai/types/beta/beta_response_input_text_param.py @@ -10,7 +10,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -32,6 +32,6 @@ class BetaResponseInputTextParam(TypedDict, total=False): prompt_cache_breakpoint: PromptCacheBreakpoint """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_item.py b/src/openai/types/beta/beta_response_item.py index cbdf9a622f..b3778f35f1 100644 --- a/src/openai/types/beta/beta_response_item.py +++ b/src/openai/types/beta/beta_response_item.py @@ -107,7 +107,7 @@ class AgentMessageContentReasoningText(BaseModel): class AgentMessageContentComputerScreenshotPromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -141,8 +141,8 @@ class AgentMessageContentComputerScreenshot(BaseModel): prompt_cache_breakpoint: Optional[AgentMessageContentComputerScreenshotPromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/beta/beta_response_output_item.py b/src/openai/types/beta/beta_response_output_item.py index d1d7211481..84f5d756fd 100644 --- a/src/openai/types/beta/beta_response_output_item.py +++ b/src/openai/types/beta/beta_response_output_item.py @@ -105,7 +105,7 @@ class AgentMessageContentReasoningText(BaseModel): class AgentMessageContentComputerScreenshotPromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -139,8 +139,8 @@ class AgentMessageContentComputerScreenshot(BaseModel): prompt_cache_breakpoint: Optional[AgentMessageContentComputerScreenshotPromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/chat/chat_completion_content_part_image.py b/src/openai/types/chat/chat_completion_content_part_image.py index 82e553d845..d91c82db38 100644 --- a/src/openai/types/chat/chat_completion_content_part_image.py +++ b/src/openai/types/chat/chat_completion_content_part_image.py @@ -23,7 +23,7 @@ class ImageURL(BaseModel): class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -46,6 +46,6 @@ class ChatCompletionContentPartImage(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/chat/chat_completion_content_part_image_param.py b/src/openai/types/chat/chat_completion_content_part_image_param.py index a8e8e7276b..b9a3faaef3 100644 --- a/src/openai/types/chat/chat_completion_content_part_image_param.py +++ b/src/openai/types/chat/chat_completion_content_part_image_param.py @@ -22,7 +22,7 @@ class ImageURL(TypedDict, total=False): class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -45,6 +45,6 @@ class ChatCompletionContentPartImageParam(TypedDict, total=False): prompt_cache_breakpoint: PromptCacheBreakpoint """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/chat/chat_completion_content_part_input_audio_param.py b/src/openai/types/chat/chat_completion_content_part_input_audio_param.py index 0f3076e1f6..039d5081aa 100644 --- a/src/openai/types/chat/chat_completion_content_part_input_audio_param.py +++ b/src/openai/types/chat/chat_completion_content_part_input_audio_param.py @@ -18,7 +18,7 @@ class InputAudio(TypedDict, total=False): class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -41,6 +41,6 @@ class ChatCompletionContentPartInputAudioParam(TypedDict, total=False): prompt_cache_breakpoint: PromptCacheBreakpoint """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/chat/chat_completion_content_part_param.py b/src/openai/types/chat/chat_completion_content_part_param.py index 192794e88c..cc2f04c0a9 100644 --- a/src/openai/types/chat/chat_completion_content_part_param.py +++ b/src/openai/types/chat/chat_completion_content_part_param.py @@ -29,7 +29,7 @@ class FileFile(TypedDict, total=False): class FilePromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -52,8 +52,8 @@ class File(TypedDict, total=False): prompt_cache_breakpoint: FilePromptCacheBreakpoint """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/chat/chat_completion_content_part_text.py b/src/openai/types/chat/chat_completion_content_part_text.py index ffd2ffecfc..679b3579b0 100644 --- a/src/openai/types/chat/chat_completion_content_part_text.py +++ b/src/openai/types/chat/chat_completion_content_part_text.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -33,6 +33,6 @@ class ChatCompletionContentPartText(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/chat/chat_completion_content_part_text_param.py b/src/openai/types/chat/chat_completion_content_part_text_param.py index 2c3e8321fb..a17aee97d4 100644 --- a/src/openai/types/chat/chat_completion_content_part_text_param.py +++ b/src/openai/types/chat/chat_completion_content_part_text_param.py @@ -10,7 +10,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -32,6 +32,6 @@ class ChatCompletionContentPartTextParam(TypedDict, total=False): prompt_cache_breakpoint: PromptCacheBreakpoint """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/conversations/computer_screenshot_content.py b/src/openai/types/conversations/computer_screenshot_content.py index 75afcec376..b01288b1ea 100644 --- a/src/openai/types/conversations/computer_screenshot_content.py +++ b/src/openai/types/conversations/computer_screenshot_content.py @@ -12,7 +12,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -46,6 +46,6 @@ class ComputerScreenshotContent(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/responses/response_input_file.py b/src/openai/types/responses/response_input_file.py index 549419339e..2849279ea0 100644 --- a/src/openai/types/responses/response_input_file.py +++ b/src/openai/types/responses/response_input_file.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -51,6 +51,6 @@ class ResponseInputFile(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/responses/response_input_file_content.py b/src/openai/types/responses/response_input_file_content.py index 1c83786a1f..cd6605cd4c 100644 --- a/src/openai/types/responses/response_input_file_content.py +++ b/src/openai/types/responses/response_input_file_content.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -51,6 +51,6 @@ class ResponseInputFileContent(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/responses/response_input_file_content_param.py b/src/openai/types/responses/response_input_file_content_param.py index b85f301cfe..d718227ab2 100644 --- a/src/openai/types/responses/response_input_file_content_param.py +++ b/src/openai/types/responses/response_input_file_content_param.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -51,6 +51,6 @@ class ResponseInputFileContentParam(TypedDict, total=False): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/responses/response_input_file_param.py b/src/openai/types/responses/response_input_file_param.py index 6e9afcd84a..3e6ff85651 100644 --- a/src/openai/types/responses/response_input_file_param.py +++ b/src/openai/types/responses/response_input_file_param.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -51,6 +51,6 @@ class ResponseInputFileParam(TypedDict, total=False): prompt_cache_breakpoint: PromptCacheBreakpoint """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/responses/response_input_image.py b/src/openai/types/responses/response_input_image.py index 364459bffc..ec5cdbe77f 100644 --- a/src/openai/types/responses/response_input_image.py +++ b/src/openai/types/responses/response_input_image.py @@ -12,7 +12,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -49,6 +49,6 @@ class ResponseInputImage(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/responses/response_input_image_content.py b/src/openai/types/responses/response_input_image_content.py index 1b89fa5c10..7ffad2a185 100644 --- a/src/openai/types/responses/response_input_image_content.py +++ b/src/openai/types/responses/response_input_image_content.py @@ -12,7 +12,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -49,6 +49,6 @@ class ResponseInputImageContent(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/responses/response_input_image_content_param.py b/src/openai/types/responses/response_input_image_content_param.py index fed5dcb614..961b952d2e 100644 --- a/src/openai/types/responses/response_input_image_content_param.py +++ b/src/openai/types/responses/response_input_image_content_param.py @@ -13,7 +13,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -50,6 +50,6 @@ class ResponseInputImageContentParam(TypedDict, total=False): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/responses/response_input_image_param.py b/src/openai/types/responses/response_input_image_param.py index 018d1272ba..99d570f397 100644 --- a/src/openai/types/responses/response_input_image_param.py +++ b/src/openai/types/responses/response_input_image_param.py @@ -13,7 +13,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -50,6 +50,6 @@ class ResponseInputImageParam(TypedDict, total=False): prompt_cache_breakpoint: PromptCacheBreakpoint """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/responses/response_input_text.py b/src/openai/types/responses/response_input_text.py index f7fbe38680..f302cefb91 100644 --- a/src/openai/types/responses/response_input_text.py +++ b/src/openai/types/responses/response_input_text.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -33,6 +33,6 @@ class ResponseInputText(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/responses/response_input_text_content.py b/src/openai/types/responses/response_input_text_content.py index 393e894f38..3e9faa5f76 100644 --- a/src/openai/types/responses/response_input_text_content.py +++ b/src/openai/types/responses/response_input_text_content.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(BaseModel): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Literal["explicit"] @@ -33,6 +33,6 @@ class ResponseInputTextContent(BaseModel): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] = None """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/responses/response_input_text_content_param.py b/src/openai/types/responses/response_input_text_content_param.py index 71622e7777..09480c809f 100644 --- a/src/openai/types/responses/response_input_text_content_param.py +++ b/src/openai/types/responses/response_input_text_content_param.py @@ -11,7 +11,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -33,6 +33,6 @@ class ResponseInputTextContentParam(TypedDict, total=False): prompt_cache_breakpoint: Optional[PromptCacheBreakpoint] """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ diff --git a/src/openai/types/responses/response_input_text_param.py b/src/openai/types/responses/response_input_text_param.py index b15de2e89a..34e2b17c06 100644 --- a/src/openai/types/responses/response_input_text_param.py +++ b/src/openai/types/responses/response_input_text_param.py @@ -10,7 +10,7 @@ class PromptCacheBreakpoint(TypedDict, total=False): """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """ mode: Required[Literal["explicit"]] @@ -32,6 +32,6 @@ class ResponseInputTextParam(TypedDict, total=False): prompt_cache_breakpoint: PromptCacheBreakpoint """Marks the exact end of a reusable prompt prefix. - The breakpoint inherits its TTL from the request's `prompt_cache_options.ttl`; - the boundary is not rounded to a token block. + When `ttl` is omitted, the breakpoint inherits the request's + `prompt_cache_options.ttl`; the boundary is not rounded to a token block. """