diff --git a/.castiron.stats.yml b/.castiron.stats.yml index 6711978fa2..5a0a93c080 100644 --- a/.castiron.stats.yml +++ b/.castiron.stats.yml @@ -1,8 +1,8 @@ schema_version: 1 -generation_id: 8e0b46e2-9f31-4393-9a7e-2be585d07efb +generation_id: 592b2cad-c398-46a9-9df6-7858c09f3b65 openapi_spec_hash: d6717da751735789c482f47d6613cea0 openapi_transformed_spec_hash: 46e4f19c824b10d620e3c7bc4a7d569e -config_hash: 76695a5cf014836ea99ffbd2f387e5c1 -codegen_sha: da0ab234c8b0b445d8a64a420465199b4824a45e -codegen_hash: 3051dd5f14816304442e438ac216fbc29b4e1a9b1f87873da6f3a4a947cd7fa0 -public_codegen_sha: 992b9cbba312b4bf10128b9f1d36112b48340363 +config_hash: 0fe55890f9499fed76b60662804e30f7 +codegen_sha: f028b1b011d31cf0cd8a1939e83bf7ec5d2b8762 +codegen_hash: 2e485073ea1166bc85b3b8c04a512d221b82ef5c490801250ffc209d6292e201 +public_codegen_sha: b045ae7e94f055f952b812dfc4ddee314a691df7 diff --git a/.github/workflows/castiron-custom-code-comment.yml b/.github/workflows/castiron-custom-code-comment.yml index 85a6a460ce..b0128ef689 100644 --- a/.github/workflows/castiron-custom-code-comment.yml +++ b/.github/workflows/castiron-custom-code-comment.yml @@ -14,6 +14,239 @@ concurrency: queue: max # Preserve newer pending evaluations if older runs arrive out of order. jobs: + review-change: + name: Invalidate reviewed policy statuses + if: github.event.workflow_run.event == 'pull_request_review' + 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: + # Trusted main definition: no candidate checkout, artifacts, or executable inputs. + - name: Re-evaluate current PR and queue after review changes + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + // Trusted main orchestration only. Signal fields never choose status targets. + 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.event !== 'pull_request_review' || + run.status !== 'completed' || run.id !== signal.id || run.run_attempt !== signal.run_attempt || + run.head_sha !== signal.head_sha || run.path.split('@', 1)[0] !== '.github/workflows/castiron-custom-code.yml') { + throw new Error('Unexpected or superseded review signal'); + } + const dirtyTargets = new Set(); + // Retry discovery within this job timeout while targets are changing. + while (true) { + const candidates = []; + let pulls = run.pull_requests?.length ? run.pull_requests : await github.paginate( + github.rest.repos.listPullRequestsAssociatedWithCommit, {...context.repo, commit_sha: run.head_sha}); + if (!pulls.length) pulls = await github.paginate(github.rest.pulls.list, { + ...context.repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`, + }); + for (const item of pulls) { + const {data: pr} = await github.rest.pulls.get({...context.repo, pull_number: item.number}); + if (pr.state !== 'open' || pr.base.ref !== 'next' || pr.base.repo.full_name !== repo || + (item.head?.sha && pr.head.sha !== item.head.sha) || !/^[0-9a-f]{40}$/.test(pr.head.sha)) continue; + if (run.head_sha !== pr.head.sha) { + const {data: merge} = await github.rest.git.getRef({...context.repo, ref: `pull/${pr.number}/merge`}); + const {data: commit} = await github.rest.repos.getCommit({...context.repo, ref: run.head_sha}); + if (merge.object.sha !== run.head_sha || commit.parents.length !== 2 || + commit.parents[0].sha !== pr.base.sha || commit.parents[1].sha !== pr.head.sha) continue; + } + candidates.push(pr); + } + if (!candidates.length) return; // Main, closed, and superseded PR reviews have no next target. + if (candidates.length !== 1) throw new Error('Review signal identifies ambiguous next PRs'); + const pr = candidates[0]; + const files = await github.paginate(github.rest.pulls.listFiles, {...context.repo, pull_number: pr.number}); + if (!files.some(file => file.filename === '.castiron-ratchet.json')) return; + // Read authoritative reviews, including edits: submittedAt alone misses + // removal/restoration of an attestation. Never trust candidate event artifacts. + let reviewCursor = null; + const latest = new Map(); + do { + const result = await github.graphql(`query($owner:String!,$name:String!,$number:Int!,$cursor:String) { + repository(owner:$owner,name:$name) { pullRequest(number:$number) { + reviews(first:100,after:$cursor) { nodes { fullDatabaseId state body updatedAt + author { login __typename } authorAssociation commit { oid } } + pageInfo { hasNextPage endCursor } } + } } + }`, {...context.repo, name: context.repo.repo, number: pr.number, cursor: reviewCursor}); + const page = result.repository.pullRequest.reviews; + for (const review of page.nodes) { + const value = review.fullDatabaseId; + 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; + const previous = latest.get(review.author.login); + if (!previous || BigInt(identity) > BigInt(previous.identity)) latest.set(review.author.login, {...review, identity}); + } + if (!page.pageInfo.hasNextPage) break; + if (!page.pageInfo.endCursor || page.pageInfo.endCursor === reviewCursor) throw new Error('Invalid review cursor'); + reviewCursor = page.pageInfo.endCursor; + } while (true); + // Same v2 whole-body contract as the selector/publisher. This can only + // request reevaluation; the normal publisher still decides authorization. + const approvals = [...latest.values()].filter(review => + review.state === 'APPROVED' && review.commit?.oid === pr.head.sha && + review.author.__typename === 'User' && !review.author.login.endsWith('[bot]') && + review.author.login !== pr.user.login && + ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(review.authorAssociation) && + typeof review.body === 'string' && + review.body.trim() === 'I personally reviewed and approve this policy change as a human: ' + pr.head.sha); + const authorized = approvals.length > 0; + if (authorized && (run.actor?.type !== 'User' || + !approvals.some(review => review.author.login === run.actor.login))) return; + for (const review of approvals) { + if (!Number.isFinite(Date.parse(review.updatedAt))) throw new Error('Missing review update time'); + } + const fingerprint = require('node:crypto').createHash('sha256').update(JSON.stringify( + [pr.head.sha, approvals.map(review => [review.identity, review.updatedAt]).sort((a, b) => a[0].localeCompare(b[0]))] + )).digest('hex'); + const marker = `[review ${authorized ? 'approve' : 'revoke'} ${fingerprint}]`; + async function queue() { + let cursor = null; + const entries = []; + do { + const result = await github.graphql(`query($owner:String!,$name:String!,$cursor:String) { + repository(owner:$owner,name:$name) { mergeQueue(branch:"next") { + entries(first:100,after:$cursor) { nodes { headCommit { oid } pullRequest { number headRefOid } } + pageInfo { hasNextPage endCursor } } + } } + }`, {...context.repo, name: context.repo.repo, cursor}); + const page = result.repository.mergeQueue?.entries; + if (!page) return []; + entries.push(...page.nodes); + if (!page.pageInfo.hasNextPage) return entries; + if (!page.pageInfo.endCursor || page.pageInfo.endCursor === cursor) throw new Error('Invalid queue cursor'); + cursor = page.pageInfo.endCursor; + } while (true); + } + const entries = await queue(); + const index = entries.findIndex(entry => entry.pullRequest?.number === pr.number && entry.pullRequest.headRefOid === pr.head.sha); + // Later queue entries can include the affected constituent. Re-evaluate all of them. + const targets = new Map([[pr.head.sha, 'pull_request']]); + for (const entry of index < 0 ? [] : entries.slice(index)) { + if (entry.headCommit === null) continue; // Queue preparation has not produced statuses yet. + if (!/^[0-9a-f]{40}$/.test(entry.headCommit.oid)) throw new Error('Invalid queue SHA'); + targets.set(entry.headCommit.oid, 'merge_group'); + } + // Revoke the authenticated snapshot before slower reads. Pending on an obsolete + // affected SHA cannot authorize a merge, and a later race must not retain success. + const url = `${context.serverUrl}/${repo}/actions/runs/${context.runId}`; + const activeTargets = new Map(); + const priorWriters = new Map(); + async function priorWriter(status) { + if (!/\[review (?:approve|revoke) [0-9a-f]{64}\]$/.test(status.description || '')) return null; + const identity = status.target_url?.slice(`${context.serverUrl}/${repo}/actions/runs/`.length); + if (!/^[1-9][0-9]*$/.test(identity || '') || !Number.isSafeInteger(Number(identity))) return null; + if (!priorWriters.has(identity)) { + const {data: definition} = await github.rest.actions.getWorkflow({ + ...context.repo, workflow_id: 'castiron-custom-code-comment.yml', + }); + let writer; + try { + ({data: writer} = await github.rest.actions.getWorkflowRun({...context.repo, run_id: Number(identity)})); + } catch (error) { + if (error.status !== 404) throw error; + priorWriters.set(identity, null); // Deleted runs cannot authenticate a marker. + return null; + } + const authenticated = definition.path === '.github/workflows/castiron-custom-code-comment.yml' && + writer.workflow_id === definition.id && writer.repository.full_name === repo && + writer.event === 'workflow_run' && writer.id === Number(identity); + priorWriters.set(identity, authenticated ? { + completed: writer.status === 'completed' && writer.conclusion === 'success', + } : null); + } + return priorWriters.get(identity); + } + const names = ['Castiron next / budget-only change', 'Castiron next / custom-code budget']; + for (const [sha, event] of targets) { + const statuses = await github.paginate(github.rest.repos.listCommitStatusesForRef, {...context.repo, ref: sha}); + const trusted = statuses.filter(status => status.creator?.login === 'github-actions[bot]' && + status.target_url?.startsWith(`${context.serverUrl}/${repo}/actions/runs/`)); + const current = new Map(); + for (const status of trusted) if (!current.has(status.context)) current.set(status.context, status); + // Only current pending markers coalesce. A later publisher failure + // supersedes recovery even if a restored review has the same timestamp. + const changed = []; + for (const name of names) { + const status = current.get(name); + // A failed/cancelled writer may have stopped after pending but + // before dispatch. Re-stamp and retry; only successful writers coalesce. + const writer = status?.state === 'pending' ? await priorWriter(status) : null; + const handled = status?.state === 'success' || + (status?.state === 'pending' && status.description?.endsWith(marker) && writer?.completed); + if (authorized ? !handled : + status?.state === 'success' || (writer && !writer.completed)) changed.push(name); + } + if (authorized && names.every(name => current.get(name)?.state === 'success')) continue; + for (const name of changed) { + await github.rest.repos.createCommitStatus({...context.repo, sha, context: name, state: 'pending', + description: `Review changed; reevaluation pending. ${marker}`, target_url: url}); + dirtyTargets.add(sha); + } + if (dirtyTargets.has(sha)) activeTargets.set(sha, event); + } + if (!activeTargets.size) return; + const reruns = []; + for (const [sha, event] of activeTargets) { + const runs = await github.paginate(github.rest.actions.listWorkflowRuns, { + ...context.repo, workflow_id: 'castiron-custom-code.yml', head_sha: sha, event, per_page: 100, + }); + const matches = []; + for (const item of runs) { + if (item.repository.full_name !== repo || item.event !== event || item.head_sha !== sha || + item.path.split('@', 1)[0] !== '.github/workflows/castiron-custom-code.yml') continue; + if (event === 'pull_request') { + let associated = item.pull_requests?.length ? item.pull_requests : await github.paginate( + github.rest.repos.listPullRequestsAssociatedWithCommit, {...context.repo, commit_sha: sha}); + if (!associated.length) associated = await github.paginate(github.rest.pulls.list, { + ...context.repo, state: 'open', head: `${item.head_repository.owner.login}:${item.head_branch}`, + }); + const live = []; + for (const reference of associated) { + const {data: pull} = await github.rest.pulls.get({...context.repo, pull_number: reference.number}); + if (pull.state === 'open' && pull.head.sha === sha && pull.base.repo.full_name === repo && + ['main', 'next'].includes(pull.base.ref)) live.push(pull); + } + // Match the canonical reporter's main preference for shared-head discovery. + const main = live.filter(pull => pull.base.ref === 'main'); + const selected = main.length ? main : live; + if (selected.length !== 1 || selected[0].number !== pr.number) continue; + } + matches.push(item); + } + matches.sort((a, b) => b.id - a.id); + if (matches[0]?.status === 'completed') reruns.push(matches[0].id); + } + // Retry against the new snapshot; already-observed affected statuses stay pending. + const {data: current} = await github.rest.pulls.get({...context.repo, pull_number: pr.number}); + const {data: base} = await github.rest.git.getRef({...context.repo, ref: 'heads/next'}); + if (current.state !== 'open' || current.head.sha !== pr.head.sha || current.base.ref !== 'next' || + current.base.repo.full_name !== repo || current.base.sha !== pr.base.sha || base.object.sha !== pr.base.sha || + JSON.stringify(await queue()) !== JSON.stringify(entries)) continue; + // Only normal authenticated reporter runs may restore success. Never infer approval here. + for (const run_id of reruns) await github.rest.actions.reRunWorkflow({...context.repo, run_id}); + return; + } + 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@')) @@ -153,6 +386,10 @@ jobs: retention-days: 7 budget-status: + concurrency: + group: castiron-budget-writes-${{ github.repository_id }} + cancel-in-progress: false + queue: max name: Publish custom-code budget checks needs: compute if: always() && !cancelled() && needs.compute.result != 'skipped' @@ -284,8 +521,9 @@ jobs: try { const proofs = JSON.parse(process.env.POLICY_REVIEWS); const rules = JSON.parse(process.env.HUMAN_REVIEW_RULES); - if (!Array.isArray(proofs) || rules.version !== 1 || !Array.isArray(rules.associations) || - typeof rules.negative_pattern !== 'string') throw new Error('Missing trusted review contract'); + if (!Array.isArray(proofs) || rules.version !== 2 || !Array.isArray(rules.associations) || + typeof rules.negative_pattern !== 'string' || typeof rules.attestation_prefix !== 'string' || + !rules.attestation_prefix) throw new Error('Missing trusted review contract'); const negative = new RegExp(rules.negative_pattern, 'i'); const seen = new Set(); for (const proof of proofs) { @@ -297,17 +535,36 @@ jobs: const {data: pr} = await github.rest.pulls.get({...context.repo, pull_number: proof.number}); if (pr.state !== 'open' || pr.head.sha !== proof.head_sha || pr.base.ref !== 'next' || pr.base.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) throw new Error('Policy PR changed'); - const reviews = await github.paginate(github.rest.pulls.listReviews, {...context.repo, pull_number: proof.number, per_page: 100}); + let reviewCursor = null; const latest = new Map(); - for (const review of reviews) { - if (review.state === 'PENDING') continue; - const login = review.user.login; - if (!latest.has(login) || review.id > latest.get(login).id) latest.set(login, review); - } + do { + const result = await github.graphql(`query($owner:String!,$name:String!,$number:Int!,$cursor:String) { + repository(owner:$owner,name:$name) { pullRequest(number:$number) { + reviews(first:100,after:$cursor) { nodes { fullDatabaseId state body + author { login __typename } authorAssociation commit { oid } } + pageInfo { hasNextPage endCursor } } + } } + }`, {...context.repo, name: context.repo.repo, number: proof.number, cursor: reviewCursor}); + const page = result.repository.pullRequest.reviews; + for (const review of page.nodes) { + const value = review.fullDatabaseId; + 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; + const previous = latest.get(review.author.login); + if (!previous || BigInt(identity) > BigInt(previous.identity)) latest.set(review.author.login, {...review, identity}); + } + if (!page.pageInfo.hasNextPage) break; + if (!page.pageInfo.endCursor || page.pageInfo.endCursor === reviewCursor) throw new Error('Invalid review cursor'); + reviewCursor = page.pageInfo.endCursor; + } while (true); const approved = [...latest.values()].some(review => review.state === 'APPROVED' && - review.commit_id === proof.head_sha && review.user.type === 'User' && - rules.associations.includes(review.author_association) && !review.user.login.endsWith('[bot]') && - review.user.login !== pr.user.login && !negative.test(review.body || '')); + review.commit?.oid === proof.head_sha && review.author.__typename === 'User' && + rules.associations.includes(review.authorAssociation) && !review.author.login.endsWith('[bot]') && + review.author.login !== pr.user.login && typeof review.body === 'string' && + review.body.trim() === rules.attestation_prefix + proof.head_sha && + !negative.test(review.body)); if (!approved) throw new Error('Human policy approval is no longer valid'); } } catch { fresh = false; } diff --git a/.github/workflows/castiron-custom-code.yml b/.github/workflows/castiron-custom-code.yml index bde6e47d73..e553b6b013 100644 --- a/.github/workflows/castiron-custom-code.yml +++ b/.github/workflows/castiron-custom-code.yml @@ -4,6 +4,8 @@ name: Castiron custom code on: pull_request: types: [opened, synchronize, reopened, ready_for_review, edited] + pull_request_review: + types: [submitted, edited, dismissed] # Notify the existing trusted handler for queue candidates. merge_group: types: [checks_requested] @@ -16,11 +18,11 @@ concurrency: cancel-in-progress: false env: - REPORTER_SHA256: 6b7cfda1f85426daaa2e4f6186e2d1b20ac638dadaa54be8142ecec89fefd880 + REPORTER_SHA256: a52a1aabf4047f4c20e96d1d788c25a4aa78c1987b4fa73a13d22c92ef213a3f jobs: queue-signal: - if: github.event_name == 'merge_group' + if: github.event_name == 'merge_group' || github.event_name == 'pull_request_review' runs-on: ubuntu-latest timeout-minutes: 1 permissions: {} diff --git a/api.md b/api.md index 4aac08d3a4..7e6990c2fa 100644 --- a/api.md +++ b/api.md @@ -119,6 +119,8 @@ Methods: - client.chat.completions.update(completion_id, \*\*params) -> ChatCompletion - client.chat.completions.list(\*\*params) -> SyncCursorPage[ChatCompletion] - client.chat.completions.delete(completion_id) -> ChatCompletionDeleted +- client.chat.completions.parse(\*, messages, model, audio=..., response_format=..., frequency_penalty=..., function_call=..., functions=..., logit_bias=..., logprobs=..., max_completion_tokens=..., max_tokens=..., metadata=..., modalities=..., moderation=..., n=..., parallel_tool_calls=..., prediction=..., presence_penalty=..., prompt_cache_key=..., prompt_cache_options=..., prompt_cache_retention=..., reasoning_effort=..., safety_identifier=..., seed=..., service_tier=..., stop=..., store=..., stream_options=..., temperature=..., tool_choice=..., tools=..., top_logprobs=..., top_p=..., user=..., verbosity=..., web_search_options=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> ParsedChatCompletion[ResponseFormatT] +- client.chat.completions.stream(\*, messages, model, audio=..., response_format=..., frequency_penalty=..., function_call=..., functions=..., logit_bias=..., logprobs=..., max_completion_tokens=..., max_tokens=..., metadata=..., modalities=..., moderation=..., n=..., parallel_tool_calls=..., prediction=..., presence_penalty=..., prompt_cache_key=..., prompt_cache_options=..., prompt_cache_retention=..., reasoning_effort=..., safety_identifier=..., seed=..., service_tier=..., stop=..., store=..., stream_options=..., temperature=..., tool_choice=..., tools=..., top_logprobs=..., top_p=..., user=..., verbosity=..., web_search_options=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> ChatCompletionStreamManager[ResponseFormatT] ### Messages @@ -154,7 +156,7 @@ Methods: - client.files.delete(file_id) -> FileDeleted - client.files.content(file_id) -> HttpxBinaryResponseContent - client.files.retrieve_content(file_id) -> str -- client.files.wait_for_processing(\*args) -> FileObject +- client.files.wait_for_processing(id, poll_interval=..., max_wait_seconds=...) -> FileObject # Images @@ -463,10 +465,10 @@ Methods: - client.vector_stores.files.list(vector_store_id, \*\*params) -> SyncCursorPage[VectorStoreFile] - client.vector_stores.files.delete(file_id, \*, vector_store_id) -> VectorStoreFileDeleted - client.vector_stores.files.content(file_id, \*, vector_store_id) -> SyncPage[FileContentResponse] -- client.vector_stores.files.create_and_poll(\*args) -> VectorStoreFile -- client.vector_stores.files.poll(\*args) -> VectorStoreFile -- client.vector_stores.files.upload(\*args) -> VectorStoreFile -- client.vector_stores.files.upload_and_poll(\*args) -> VectorStoreFile +- client.vector_stores.files.create_and_poll(file_id, vector_store_id=..., attributes=..., poll_interval_ms=..., max_wait_seconds=..., chunking_strategy=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> VectorStoreFile +- client.vector_stores.files.poll(file_id, vector_store_id=..., poll_interval_ms=..., max_wait_seconds=...) -> VectorStoreFile +- client.vector_stores.files.upload(vector_store_id=..., file=..., chunking_strategy=...) -> VectorStoreFile +- client.vector_stores.files.upload_and_poll(vector_store_id=..., file=..., attributes=..., poll_interval_ms=..., max_wait_seconds=..., chunking_strategy=...) -> VectorStoreFile ## FileBatches @@ -482,23 +484,9 @@ Methods: - client.vector_stores.file_batches.retrieve(batch_id, \*, vector_store_id) -> VectorStoreFileBatch - client.vector_stores.file_batches.cancel(batch_id, \*, vector_store_id) -> VectorStoreFileBatch - client.vector_stores.file_batches.list_files(batch_id, \*, vector_store_id, \*\*params) -> SyncCursorPage[VectorStoreFile] -- client.vector_stores.file_batches.create_and_poll(\*args) -> VectorStoreFileBatch -- client.vector_stores.file_batches.poll(\*args) -> VectorStoreFileBatch -- client.vector_stores.file_batches.upload_and_poll(\*args) -> VectorStoreFileBatch - -# Safety - -## Alerts - -Types: - -```python -from openai.types.safety import SafetyAlert -``` - -Methods: - -- client.safety.alerts.retrieve(id) -> SafetyAlert +- client.vector_stores.file_batches.create_and_poll(vector_store_id, attributes=..., chunking_strategy=..., file_ids=..., files=..., poll_interval_ms=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> VectorStoreFileBatch +- client.vector_stores.file_batches.poll(batch_id, vector_store_id=..., poll_interval_ms=...) -> VectorStoreFileBatch +- client.vector_stores.file_batches.upload_and_poll(vector_store_id, files=..., max_concurrency=..., file_ids=..., poll_interval_ms=..., chunking_strategy=...) -> VectorStoreFileBatch # Safety @@ -528,11 +516,6 @@ Methods: # [Webhooks](src/openai/resources/webhooks/api.md) -Methods: - -- client.webhooks.unwrap(payload, headers, \*, secret) -> UnwrapWebhookEvent -- client.webhooks.verify_signature(payload, headers, \*, secret, tolerance) -> None - # Beta ## Agents @@ -680,6 +663,9 @@ Methods: - client.beta.agents.environments.files.create(environment_id, \*\*params) -> EnvironmentFile - client.beta.agents.environments.files.list(environment_id, \*\*params) -> SyncTokenPage[EnvironmentFile] +- client.beta.agents.environments.files.prepare(files, \*, extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> PreparedAgentFiles +- client.beta.agents.environments.files.prepare_directory(root, \*, destination, include, extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> PreparedAgentFiles +- client.beta.agents.environments.files.upload(environment_id, \*, file, path, extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> StagedAgentFile #### Templates @@ -749,6 +735,7 @@ Methods: - client.beta.agents.sessions.update(session_id, \*\*params) -> AgentSession - client.beta.agents.sessions.list(\*\*params) -> SyncCursorPage[AgentSession] - client.beta.agents.sessions.delete(session_id) -> AgentSessionDeleted +- client.beta.agents.sessions.stream(session_id, \*, input, output_type=..., tool_handlers=..., on_tool_error=..., idempotency_key=..., extra_headers=..., timeout=...) -> AgentSessionStream[OutputT] #### Subagents @@ -790,6 +777,7 @@ Methods: - client.beta.agents.sessions.artifacts.list(session_id, \*\*params) -> SyncCursorPage[SessionArtifact] - client.beta.agents.sessions.artifacts.delete(artifact_id, \*, session_id) -> SessionArtifactDeleted - client.beta.agents.sessions.artifacts.content(artifact_id, \*, session_id) -> HttpxBinaryResponseContent +- client.beta.agents.sessions.artifacts.for_result(result) -> AgentResultArtifacts #### Items @@ -887,7 +875,6 @@ from openai.types.beta import ( BetaResponseConfigurationUpdateItem, BetaResponseConfigurationUpdateItemParam, BetaResponseContainerReference, - BetaResponseContent, BetaResponseContentPartAddedEvent, BetaResponseContentPartDoneEvent, BetaResponseConversationParam, @@ -929,7 +916,6 @@ from openai.types.beta import ( BetaResponseInjectEvent, BetaResponseInjectFailedEvent, BetaResponseInput, - BetaResponseInputAudio, BetaResponseInputContent, BetaResponseInputFile, BetaResponseInputFileContent, @@ -950,7 +936,6 @@ from openai.types.beta import ( BetaResponseMcpListToolsCompletedEvent, BetaResponseMcpListToolsFailedEvent, BetaResponseMcpListToolsInProgressEvent, - BetaResponseOutputAudio, BetaResponseOutputItem, BetaResponseOutputItemAddedEvent, BetaResponseOutputItemDoneEvent, @@ -1096,88 +1081,12 @@ Methods: - client.beta.chatkit.threads.list(\*\*params) -> SyncConversationCursorPage[ChatKitThread] - client.beta.chatkit.threads.delete(thread_id) -> ThreadDeleteResponse - client.beta.chatkit.threads.list_items(thread_id, \*\*params) -> SyncConversationCursorPage[Data] + -## Realtime - -Types: - -```python -from openai.types.beta.realtime import ( - ConversationCreatedEvent, - ConversationItem, - ConversationItemContent, - ConversationItemCreateEvent, - ConversationItemCreatedEvent, - ConversationItemDeleteEvent, - ConversationItemDeletedEvent, - ConversationItemInputAudioTranscriptionCompletedEvent, - ConversationItemInputAudioTranscriptionDeltaEvent, - ConversationItemInputAudioTranscriptionFailedEvent, - ConversationItemRetrieveEvent, - ConversationItemTruncateEvent, - ConversationItemTruncatedEvent, - ConversationItemWithReference, - ErrorEvent, - InputAudioBufferAppendEvent, - InputAudioBufferClearEvent, - InputAudioBufferClearedEvent, - InputAudioBufferCommitEvent, - InputAudioBufferCommittedEvent, - InputAudioBufferSpeechStartedEvent, - InputAudioBufferSpeechStoppedEvent, - RateLimitsUpdatedEvent, - RealtimeClientEvent, - RealtimeResponse, - RealtimeResponseStatus, - RealtimeResponseUsage, - RealtimeServerEvent, - ResponseAudioDeltaEvent, - ResponseAudioDoneEvent, - ResponseAudioTranscriptDeltaEvent, - ResponseAudioTranscriptDoneEvent, - ResponseCancelEvent, - ResponseContentPartAddedEvent, - ResponseContentPartDoneEvent, - ResponseCreateEvent, - ResponseCreatedEvent, - ResponseDoneEvent, - ResponseFunctionCallArgumentsDeltaEvent, - ResponseFunctionCallArgumentsDoneEvent, - ResponseOutputItemAddedEvent, - ResponseOutputItemDoneEvent, - ResponseTextDeltaEvent, - ResponseTextDoneEvent, - SessionCreatedEvent, - SessionUpdateEvent, - SessionUpdatedEvent, - TranscriptionSessionUpdate, - TranscriptionSessionUpdatedEvent, -) -``` - -### Sessions - -Types: - -```python -from openai.types.beta.realtime import Session, SessionCreateResponse -``` - -Methods: - -- client.beta.realtime.sessions.create(\*\*params) -> SessionCreateResponse - -### TranscriptionSessions - -Types: + -```python -from openai.types.beta.realtime import TranscriptionSession -``` - -Methods: -- client.beta.realtime.transcription_sessions.create(\*\*params) -> TranscriptionSession +## [Realtime](docs/beta-realtime-api.md) ## Assistants @@ -1192,10 +1101,6 @@ from openai.types.beta import ( CodeInterpreterTool, FileSearchTool, FunctionTool, - MessageStreamEvent, - RunStepStreamEvent, - RunStreamEvent, - ThreadStreamEvent, ) ``` @@ -1229,8 +1134,8 @@ Methods: - client.beta.threads.update(thread_id, \*\*params) -> Thread - client.beta.threads.delete(thread_id) -> ThreadDeleted - client.beta.threads.create_and_run(\*\*params) -> Run -- client.beta.threads.create_and_run_poll(\*args) -> Run -- client.beta.threads.create_and_run_stream(\*args) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT] +- client.beta.threads.create_and_run_poll(\*, assistant_id, instructions=..., max_completion_tokens=..., max_prompt_tokens=..., metadata=..., model=..., parallel_tool_calls=..., response_format=..., temperature=..., thread=..., tool_choice=..., tool_resources=..., tools=..., top_p=..., truncation_strategy=..., poll_interval_ms=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> Run +- client.beta.threads.create_and_run_stream(\*, assistant_id, instructions=..., max_completion_tokens=..., max_prompt_tokens=..., metadata=..., model=..., parallel_tool_calls=..., response_format=..., temperature=..., thread=..., tool_choice=..., tool_resources=..., tools=..., top_p=..., truncation_strategy=..., event_handler=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT] ### Runs @@ -1248,12 +1153,12 @@ Methods: - client.beta.threads.runs.list(thread_id, \*\*params) -> SyncCursorPage[Run] - client.beta.threads.runs.cancel(run_id, \*, thread_id) -> Run - client.beta.threads.runs.submit_tool_outputs(run_id, \*, thread_id, \*\*params) -> Run -- client.beta.threads.runs.create_and_poll(\*args) -> Run -- client.beta.threads.runs.create_and_stream(\*args) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT] -- client.beta.threads.runs.poll(\*args) -> Run -- client.beta.threads.runs.stream(\*args) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT] -- client.beta.threads.runs.submit_tool_outputs_and_poll(\*args) -> Run -- client.beta.threads.runs.submit_tool_outputs_stream(\*args) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT] +- client.beta.threads.runs.create_and_poll(\*, assistant_id, include=..., additional_instructions=..., additional_messages=..., instructions=..., max_completion_tokens=..., max_prompt_tokens=..., metadata=..., model=..., parallel_tool_calls=..., reasoning_effort=..., response_format=..., temperature=..., tool_choice=..., tools=..., top_p=..., truncation_strategy=..., poll_interval_ms=..., thread_id, extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> Run +- client.beta.threads.runs.create_and_stream(\*, assistant_id, additional_instructions=..., additional_messages=..., instructions=..., max_completion_tokens=..., max_prompt_tokens=..., metadata=..., model=..., parallel_tool_calls=..., reasoning_effort=..., response_format=..., temperature=..., tool_choice=..., tools=..., top_p=..., truncation_strategy=..., thread_id, event_handler=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT] +- client.beta.threads.runs.poll(run_id, thread_id, extra_headers=..., extra_query=..., extra_body=..., timeout=..., poll_interval_ms=...) -> Run +- client.beta.threads.runs.stream(\*, assistant_id, include=..., additional_instructions=..., additional_messages=..., instructions=..., max_completion_tokens=..., max_prompt_tokens=..., metadata=..., model=..., parallel_tool_calls=..., reasoning_effort=..., response_format=..., temperature=..., tool_choice=..., tools=..., top_p=..., truncation_strategy=..., thread_id, event_handler=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT] +- client.beta.threads.runs.submit_tool_outputs_and_poll(\*, tool_outputs, run_id, thread_id, poll_interval_ms=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> Run +- client.beta.threads.runs.submit_tool_outputs_stream(\*, tool_outputs, run_id, thread_id, event_handler=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT] #### Steps @@ -1360,6 +1265,7 @@ Methods: - client.uploads.create(\*\*params) -> Upload - client.uploads.cancel(upload_id) -> Upload - client.uploads.complete(upload_id, \*\*params) -> Upload +- client.uploads.upload_file_chunked(\*, file, mime_type, purpose, filename=..., bytes=..., part_size=..., md5=...) -> Upload ## Parts @@ -2090,5 +1996,5 @@ Methods: - client.videos.extend(\*\*params) -> Video - client.videos.get_character(character_id) -> VideoGetCharacterResponse - client.videos.remix(video_id, \*\*params) -> Video -- client.videos.create_and_poll(\*args) -> Video -- client.videos.poll(\*args) -> Video +- client.videos.create_and_poll(\*, prompt, input_reference=..., model=..., seconds=..., size=..., poll_interval_ms=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> Video +- client.videos.poll(video_id, \*, poll_interval_ms=...) -> Video diff --git a/docs/beta-realtime-api.md b/docs/beta-realtime-api.md new file mode 100644 index 0000000000..6f5e42982c --- /dev/null +++ b/docs/beta-realtime-api.md @@ -0,0 +1,81 @@ +## Realtime + +Types: + +```python +from openai.types.beta.realtime import ( + ConversationCreatedEvent, + ConversationItem, + ConversationItemContent, + ConversationItemCreateEvent, + ConversationItemCreatedEvent, + ConversationItemDeleteEvent, + ConversationItemDeletedEvent, + ConversationItemInputAudioTranscriptionCompletedEvent, + ConversationItemInputAudioTranscriptionDeltaEvent, + ConversationItemInputAudioTranscriptionFailedEvent, + ConversationItemRetrieveEvent, + ConversationItemTruncateEvent, + ConversationItemTruncatedEvent, + ConversationItemWithReference, + ErrorEvent, + InputAudioBufferAppendEvent, + InputAudioBufferClearEvent, + InputAudioBufferClearedEvent, + InputAudioBufferCommitEvent, + InputAudioBufferCommittedEvent, + InputAudioBufferSpeechStartedEvent, + InputAudioBufferSpeechStoppedEvent, + RateLimitsUpdatedEvent, + RealtimeClientEvent, + RealtimeResponse, + RealtimeResponseStatus, + RealtimeResponseUsage, + RealtimeServerEvent, + ResponseAudioDeltaEvent, + ResponseAudioDoneEvent, + ResponseAudioTranscriptDeltaEvent, + ResponseAudioTranscriptDoneEvent, + ResponseCancelEvent, + ResponseContentPartAddedEvent, + ResponseContentPartDoneEvent, + ResponseCreateEvent, + ResponseCreatedEvent, + ResponseDoneEvent, + ResponseFunctionCallArgumentsDeltaEvent, + ResponseFunctionCallArgumentsDoneEvent, + ResponseOutputItemAddedEvent, + ResponseOutputItemDoneEvent, + ResponseTextDeltaEvent, + ResponseTextDoneEvent, + SessionCreatedEvent, + SessionUpdateEvent, + SessionUpdatedEvent, + TranscriptionSessionUpdate, + TranscriptionSessionUpdatedEvent, +) +``` + +### Sessions + +Types: + +```python +from openai.types.beta.realtime import Session, SessionCreateResponse +``` + +Methods: + +- client.beta.realtime.sessions.create(\*\*params) -> SessionCreateResponse + +### TranscriptionSessions + +Types: + +```python +from openai.types.beta.realtime import TranscriptionSession +``` + +Methods: + +- client.beta.realtime.transcription_sessions.create(\*\*params) -> TranscriptionSession diff --git a/docs/castiron-generated-code-transition.md b/docs/castiron-generated-code-transition.md index 724c6dbafc..002b4c9441 100644 --- a/docs/castiron-generated-code-transition.md +++ b/docs/castiron-generated-code-transition.md @@ -6,6 +6,278 @@ Eliminate handwritten patches in generated-owned files while preserving every ex No removals or renames of public symbols, new required arguments, narrower input/return types, dependency or Python-floor changes, altered defaults, new payload limits, changed warnings, or changed sync/async behavior are authorized by this transition. Deprecated methods and compatibility import paths remain supported. +## Current public baseline and measured final candidate — 2026-10-22 + +The released baseline `cfa0a79b6395bb0561b28879122a9f90a87cdada` (3.37.0) contains **41 mixed files / 7,409 custom patch lines** (4,890 additions and 2,519 deletions), spanning **39,960 physical lines**. Its public generated checkpoint is `992b9cbba312b4bf10128b9f1d36112b48340363`. + +Candidate `b53d11fd9a9b337a6d7a3412f256dff64ea4d684`, generated from merged compiler `84f592da365c50862876c4dd63cef35eff88c45d`, measures **0 mixed files / 0 custom patch lines** against authentic checkpoint `f028b1b011d31cf0cd8a1939e83bf7ec5d2b8762`. Generation `592b2cad-c398-46a9-9df6-7858c09f3b65` has verified hash `2e485073ea1166bc85b3b8c04a512d221b82ef5c490801250ffc209d6292e201`. All **1,980 generated manifest entries** match checkpoint and candidate hashes and modes. All 41 formerly mixed files match generation; their measured combined physical size is **40,213 lines**, including comments and blanks. The strict report records 41 customizations removed and 31 generated baselines changed. + +This is measured candidate evidence, **not public delivery**. [PR #4157](https://github.com/openai/openai-python/pull/4157) and subsequent authored default activation remain pending. Python ownership, exclusions, budget and counting definitions are unchanged. The Go ratchet remains inactive for Python. + +Canonical documentation now retains all ten supported Videos/Assistants helper listings, including streaming returns, using typed helper metadata and correct relative links. The shared policy checker now evaluates effective next-policy changes and checker rollback when deciding authorization; it retains policy-only updates and required review protections. These generated checker changes do not change the Python policy bytes or ceiling. + +Full SDK lint passed Ruff, Pyright, Mypy (2,064 files) and import checks. Focused runtime validation passed **337 tests with 2 skips**; both are expected creation-stream API-shape cases because those streams do not expose `until_done`. Follow-up drain and creation getter/iteration cases are exercised; the skips are not missing-dependency or environment failures. Merged-source reporting validation passed **220 tests with one existing compiler-contract skip**. The actual merged compiler passed **190 transform and 823 renderer tests**, and all **1,966 raw Python outputs** match the reviewed render. Hosted CI, required review, public delivery and default activation remain pending. + +The released API status-error message fix and Agents result-schema warning fix are preserved exactly in all four changed files. The earlier released model/union-parsing fix, all five 3.37.0 release files and the two Decisions constructor/union declaration files also retain exact bytes and modes. Previous c551, b77 and e45 audit evidence remains historical; its validation is not substituted for this candidate's checks. + +### Current baseline and candidate per-file inventory + +Physical lines include comments and blanks. Added/deleted columns measure the +full canonical-to-integrated patch at each immutable commit, not PR attribution. + +| File | Public physical | Public added | Public deleted | Candidate physical | Candidate patch | +| --- | ---: | ---: | ---: | ---: | ---: | +| `api.md` | 2094 | 126 | 8 | 2000 | 0 | +| `scripts/castiron/CUSTOM_CODE.md` | 217 | 0 | 40 | 279 | 0 | +| `scripts/castiron/custom_code_budget.py` | 702 | 1 | 4 | 769 | 0 | +| `scripts/castiron/custom_code_policy.py` | 481 | 1 | 4 | 530 | 0 | +| `scripts/castiron/custom_code_report.py` | 1250 | 19 | 254 | 1485 | 0 | +| `scripts/castiron/fixtures/github_publisher.cjs` | 69 | 10 | 63 | 122 | 0 | +| `scripts/castiron/test_custom_code_policy.py` | 587 | 0 | 4 | 720 | 0 | +| `scripts/castiron/test_custom_code_publication.py` | 873 | 4 | 600 | 1469 | 0 | +| `scripts/castiron/test_custom_code_report.py` | 1084 | 0 | 489 | 1573 | 0 | +| `src/openai/__init__.py` | 505 | 185 | 1 | 470 | 0 | +| `src/openai/_client.py` | 2440 | 759 | 60 | 1912 | 0 | +| `src/openai/resources/audio/transcriptions.py` | 1089 | 149 | 453 | 1061 | 0 | +| `src/openai/resources/audio/translations.py` | 374 | 21 | 46 | 360 | 0 | +| `src/openai/resources/beta/agents/environments/files.py` | 604 | 6 | 6 | 604 | 0 | +| `src/openai/resources/beta/agents/sessions/artifacts.py` | 573 | 2 | 2 | 573 | 0 | +| `src/openai/resources/beta/agents/sessions/sessions.py` | 1304 | 113 | 11 | 1271 | 0 | +| `src/openai/resources/beta/responses/responses.py` | 5575 | 55 | 55 | 5575 | 0 | +| `src/openai/resources/beta/threads/runs/runs.py` | 3161 | 1290 | 181 | 2889 | 0 | +| `src/openai/resources/beta/threads/threads.py` | 2009 | 390 | 7 | 1951 | 0 | +| `src/openai/resources/chat/completions/completions.py` | 3456 | 603 | 1 | 3363 | 0 | +| `src/openai/resources/conversations/api.md` | 42 | 1 | 0 | 41 | 0 | +| `src/openai/resources/embeddings.py` | 263 | 27 | 21 | 264 | 0 | +| `src/openai/resources/realtime/api.md` | 149 | 0 | 15 | 164 | 0 | +| `src/openai/resources/realtime/realtime.py` | 1660 | 9 | 3 | 1660 | 0 | +| `src/openai/resources/realtime/translations/translations.py` | 1152 | 34 | 18 | 1152 | 0 | +| `src/openai/resources/responses/api.md` | 208 | 2 | 0 | 208 | 0 | +| `src/openai/resources/responses/responses.py` | 5181 | 920 | 74 | 4866 | 0 | +| `src/openai/resources/uploads/uploads.py` | 652 | 6 | 28 | 674 | 0 | +| `src/openai/resources/webhooks/__init__.py` | 42 | 24 | 55 | 43 | 0 | +| `src/openai/resources/webhooks/webhooks.py` | 1146 | 89 | 4 | 1146 | 0 | +| `src/openai/types/__init__.py` | 157 | 4 | 1 | 157 | 0 | +| `src/openai/types/decision_input_image_param.py` | 47 | 11 | 1 | 47 | 0 | +| `src/openai/types/decision_input_message_param.py` | 95 | 6 | 1 | 95 | 0 | +| `src/openai/types/decision_input_text_param.py` | 33 | 11 | 1 | 33 | 0 | +| `src/openai/types/fine_tuning/fine_tuning_job_integration.py` | 5 | 0 | 2 | 5 | 0 | +| `src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py` | 40 | 1 | 1 | 40 | 0 | +| `src/openai/types/realtime/realtime_error_event.py` | 25 | 1 | 1 | 25 | 0 | +| `src/openai/types/responses/tool_param.py` | 398 | 3 | 0 | 399 | 0 | +| `src/openai/types/webhooks/__init__.py` | 73 | 3 | 1 | 73 | 0 | +| `src/openai/types/webhooks/live_call_incoming_webhook_event.py` | 71 | 2 | 1 | 71 | 0 | +| `src/openai/types/webhooks/unwrap_webhook_event.py` | 74 | 2 | 2 | 74 | 0 | + +### Current transition mechanisms — all 41 baseline files + +This mapping covers the current cfa baseline exactly once per file. It replaces no +historical milestone: the older 27-file track table remains a dated snapshot. +All 41 files match generation in measured b53, including both released Decisions compatibility declarations. Merged compiler build, canonical regeneration and zero-patch verification are complete. Public delivery and authored default activation remain pending. + +| Area and files | Compiler change and retained behavior | +| --- | --- | +| **Shared reporting and policy templates**
`scripts/castiron/CUSTOM_CODE.md`
`scripts/castiron/custom_code_budget.py`
`scripts/castiron/custom_code_policy.py`
`scripts/castiron/custom_code_report.py`
`scripts/castiron/fixtures/github_publisher.cjs`
`scripts/castiron/test_custom_code_policy.py`
`scripts/castiron/test_custom_code_publication.py`
`scripts/castiron/test_custom_code_report.py` | Regenerate the shared documentation, reporter, policy, publisher fixture and regression tests from canonical templates. Template fixes preserve pagination, large identifier handling, setup calls and strict typing. The shared Go ratchet remains opt-in; Python ownership, exclusions and budget are unchanged. Updated shared next-policy selection compares effective policy and checker ancestry; policy-only updates and required authorization remain enforced. Python policy bytes, ceiling, ownership and counting are unchanged. | +| **Client and package declarations**
`src/openai/_client.py`
`src/openai/__init__.py` | Typed client/package metadata generates signatures, imports, defaults, exports and delegation to handwritten authentication and module-runtime code. Explicit import scopes, binding validation and native member placement preserve original-module state, subclass dispatch, monkeypatching and sync/async behavior. | +| **Parsing and request facades**
`src/openai/resources/audio/transcriptions.py`
`src/openai/resources/audio/translations.py`
`src/openai/resources/chat/completions/completions.py`
`src/openai/resources/responses/responses.py` | Typed helper signatures, overloads, request context, parser bindings and stream factories generate public facades. Existing handwritten parsing and request implementations retain sentinel handling, payload construction, stream lifecycle and response types; generated wrappers do not duplicate that runtime logic. | +| **Assistants and Sessions facades**
`src/openai/resources/beta/threads/runs/runs.py`
`src/openai/resources/beta/threads/threads.py`
`src/openai/resources/beta/agents/sessions/sessions.py` | Typed resource helpers, positional request options, method ordering and deprecation metadata generate the existing public entry points. Handwritten Assistants and Sessions implementations retain polling, streaming, warning timing, tool dispatch and cleanup semantics. | +| **Previously released upload and file helpers**
`src/openai/resources/beta/agents/environments/files.py`
`src/openai/resources/beta/agents/sessions/artifacts.py`
`src/openai/resources/uploads/uploads.py` | Retain released helper and upload bindings in the composed configuration and regenerate their facades. Existing handwritten implementations continue to own file processing and uploads, including synchronous factories on async resources, subclass hooks, lazy defaults/logger timing and payload limits. | +| **Embedding parsing**
`src/openai/resources/embeddings.py` | The typed runtime hook delegates embedding response handling to the existing handwritten parser while retaining request defaults, encoding behavior and response transformation semantics. | +| **Realtime and legacy response connections**
`src/openai/resources/realtime/realtime.py`
`src/openai/resources/realtime/translations/translations.py`
`src/openai/resources/beta/responses/responses.py` | Typed WebSocket runtime bindings and event-method ordering generate connection facades. Existing transport code retains query composition, client properties, older-runtime fallback and sync/async behavior; the managed Beta projection keeps its released declaration order. | +| **Webhook runtime and compatibility exports**
`src/openai/resources/webhooks/__init__.py`
`src/openai/resources/webhooks/webhooks.py`
`src/openai/types/webhooks/__init__.py`
`src/openai/types/webhooks/live_call_incoming_webhook_event.py`
`src/openai/types/webhooks/unwrap_webhook_event.py` | Typed runtime bindings, package exports, compatibility aliases and deprecation annotations generate the wrappers and imports. Existing webhook verification remains handwritten; event import paths and deprecated class names remain available without changing verification behavior. | +| **Existing compatibility type declarations**
`src/openai/types/decision_input_image_param.py`
`src/openai/types/decision_input_text_param.py`
`src/openai/types/fine_tuning/fine_tuning_job_integration.py`
`src/openai/types/responses/tool_param.py` | Typed compatibility declarations retain PromptCacheBreakpoint, the established fine-tuning module export shape and ParseableToolParam union membership. Generate these declarations directly rather than preserving handwritten additions in generated type modules. | +| **Newly released Decisions constructor and union**
`src/openai/types/decision_input_message_param.py`
`src/openai/types/__init__.py` | Merged typed legacy TypedDict/union capability emits the released callable DecisionInputMessageParam subclass and separate DecisionInputMessageUnionParam export. Both generated modules are byte-identical to released public files; the two historical c551 residual patches are eliminated in b53. | +| **API documentation**
`api.md`
`src/openai/resources/conversations/api.md`
`src/openai/resources/realtime/api.md`
`src/openai/resources/responses/api.md` | Canonical documentation rendering combines typed helper signatures, compatibility facts and external-resource references with the released schema. Regeneration removes stale generated documentation edits while retaining the public helper and resource documentation surface. Merged rendering restores all ten supported Videos/Assistants helper listings directly from typed signatures, deduplicates existing entries and resolves standalone relative links. | +| **Realtime schema descriptions**
`src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py`
`src/openai/types/realtime/realtime_error_event.py` | Propagate the authoritative schema wording through ordinary model rendering, eliminating stale description edits without changing fields, event types or runtime behavior. | + +### Remaining delivery and activation sequence + +1. Apply verified transition documentation, refresh final-head evidence and finish current-head hosted CI and normal required reviews. +2. Complete normal public promotion, recheck released main and use the protected public merge workflow. +3. Verify the public merge and post-merge CI before marking delivered true. +4. After verified public runtime delivery, activate authored defaults with additive composition, retained opt-ins/imports and Beta generator guards. + +### Historical c551 audit against fa9 (before the e8 rebase) + +The current measured public baseline is `e8a0fd28bbdc1699c97ff0f602b1af3dd378382d`: +**41 mixed files / 7,409 custom patch lines** (4,890 additions and 2,519 deletions), +containing **39,960 physical lines**. Its authentic public generated checkpoint is +`992b9cbba312b4bf10128b9f1d36112b48340363`, generation +`8e0b46e2-9f31-4393-9a7e-2be585d07efb`, with 1,980 manifest entries. +The strict public reporter verified recorded checkpoint hash and ancestry. +The earlier 27-file / 6,186-line audit at `7db9d884` remains historical; its counts +must not be used as the current public baseline after the newly released Decisions +and reporting changes. + +[Release #4154](https://github.com/openai/openai-python/pull/4154) advanced public main +from fa9 to e8 with version 3.37.0 and its changelog only. The five changed files are +`.release-please-manifest.json`, `CHANGELOG.md`, `pyproject.toml`, +`src/openai/_version.py` and `uv.lock`; dependency resolution is unchanged. +Stats, API/config checkpoint and generated manifest are byte-identical. A fresh +strict public report confirms every mixed-file byte hash and all debt counts are +unchanged. The next ordinary rebase must retain these five released files; their +exact hashes and modes are recorded in the inventory. + +The candidate below was measured against fa9 and has **not** been regenerated +against e8. Its prior checks do not verify the future release-baseline rebase. + +The intermediate candidate `c5519111ba3b907ba226175f2401ec8b18c18fad` has +**2 mixed files / 12 custom patch lines** (10 additions and 2 deletions). +Its generated checkpoint is `da5d06c76d64ed28f1ec415c924323d16900cadc`, generation +`9c4bdbf4-d266-4211-a6ce-6e39bbee056d`, hash +`5dce7cd552b6d28818b7660de7c735877fd738f233f253a34f602cf8939a537b`. +All 1,980 generated manifest hashes/modes are authentic; 1,978 integrated entries +match. Thirty-nine formerly mixed files now match generation. These are candidate +measurements, **not a delivered reduction or a final zero result**. Public delivery +through [PR #4157](https://github.com/openai/openai-python/pull/4157) and subsequent +default configuration activation remain pending. + +The residual files are `src/openai/types/decision_input_message_param.py` (6 added, +1 deleted) and `src/openai/types/__init__.py` (4 added, 1 deleted). They preserve the +released callable `DecisionInputMessageParam` TypedDict subclass and separate +`DecisionInputMessageUnionParam` export. Canonical typed compiler support must +retain both contracts; deleting these patches without equivalent generated output +would be a compatibility regression. + +Local full SDK lint passed: Mypy checked 2,064 source files, and Pyright, Ruff and +import checks passed. The focused runtime suite passed 166 tests in 2.41 seconds. +These checks apply to c551; the future regenerated candidate requires fresh checks. +A 37-file released-content preservation ledger records 34 byte-identical files; +the three differences are generation stats, `api.md` and the generated manifest. Released +Decisions behavior and workflow updates remain retained; no test or policy bypass +is implied by regenerated manifest changes. + + +The following snapshots retain historical measurements, not current delivery claims. + +## Historical 7db baseline and e320 candidate snapshot + +[Public #4155](https://github.com/openai/openai-python/pull/4155) merged as +`7db9d884c33b0d5ef252e9a4b6e7ae0bbd217e5b`; its [post-merge CI](https://github.com/openai/openai-python/actions/runs/39730902045) passed. +The measured pre-transition baseline at that commit is **27 mixed files / 6,186 custom patch lines** +(4,979 additions and 1,207 deletions). These files contain 32,760 physical lines +versus 28,988 generated lines. This historical baseline and its per-file inventory +are retained for comparison with the consolidated transition. + +The compiler capabilities are merged in Delta 2593695, following the merged helper/request foundation in Delta 2593282. + +The measured private candidate at `e320019182d287cb7470500ab6a69bb95bd5ce54` +has an authoritative checkpoint report of **0 mixed files / 0 custom patch +lines**, removing all 27 remaining customizations. The generated checkpoint is +`9293fea304d8c31dc1b24e3d9970db82752819ff`; the custom patch is empty and all +1,972 generated manifest entries match. These are immutable candidate measurements +that precede public delivery through [PR #4157](https://github.com/openai/openai-python/pull/4157). + +Normal regeneration from merged compiler `babbc1ed17987d1c6017b20236058550a2e37cec` +completed without conflicts, producing generation `489447d0-e0d6-455f-a82b-f7c6d8c41bbd` +and checkpoint content hash `5f1421c905f7f97582e04beddc7179ec77da6a8a69cf59656f2ed282e9345d49`. +The reporting-test fixes merged in Delta 2594557, +Delta 2594802 and +Delta 2595513. +The last removes two unused assignments while preserving their setup calls. +The compiler also adds `CreateChatCompletionRequestMessageParam`: it is the same +runtime union object as the existing `ChatCompletionMessageParam`, with identical +six members and ordering. Existing methods and type definitions remain unchanged +from the preceding candidate. + +Full SDK lint passed, including Mypy over 2,056 files and Pyright. The focused +reporting/publication suite ran 102 tests in 68.186 seconds: 101 passed and one +existing compiler-contract test was skipped. Public delivery requires current-head +CI, review and protected merging; default configuration activation follows it. + +### Measured candidate file inventory + +These are physical lines, including comments and blanks, at candidate `e3200191`. +Every listed file is byte- and mode-identical to its authentic generated checkpoint. + +| Former mixed file | Released patch lines | Candidate physical lines | Candidate patch lines | +| --- | ---: | ---: | ---: | +| `src/openai/resources/beta/threads/runs/runs.py` | 1,471 | 2,889 | 0 | +| `src/openai/resources/responses/responses.py` | 994 | 4,866 | 0 | +| `src/openai/resources/audio/transcriptions.py` | 888 | 1,061 | 0 | +| `src/openai/_client.py` | 819 | 1,912 | 0 | +| `src/openai/resources/chat/completions/completions.py` | 604 | 3,363 | 0 | +| `src/openai/resources/beta/threads/threads.py` | 397 | 1,951 | 0 | +| `src/openai/__init__.py` | 186 | 470 | 0 | +| `api.md` | 127 | 1,978 | 0 | +| `src/openai/resources/beta/agents/sessions/sessions.py` | 124 | 1,271 | 0 | +| `src/openai/resources/audio/translations.py` | 115 | 360 | 0 | +| `src/openai/resources/beta/responses/responses.py` | 110 | 5,575 | 0 | +| `src/openai/resources/webhooks/webhooks.py` | 93 | 1,146 | 0 | +| `src/openai/resources/webhooks/__init__.py` | 79 | 43 | 0 | +| `src/openai/resources/realtime/translations/translations.py` | 52 | 1,152 | 0 | +| `src/openai/resources/embeddings.py` | 48 | 264 | 0 | +| `src/openai/resources/realtime/api.md` | 20 | 164 | 0 | +| `src/openai/resources/realtime/realtime.py` | 12 | 1,660 | 0 | +| `src/openai/types/decision_input_image_param.py` | 12 | 47 | 0 | +| `src/openai/types/decision_input_text_param.py` | 12 | 33 | 0 | +| `src/openai/types/webhooks/__init__.py` | 4 | 73 | 0 | +| `src/openai/types/webhooks/unwrap_webhook_event.py` | 4 | 74 | 0 | +| `src/openai/types/responses/tool.py` | 3 | 398 | 0 | +| `src/openai/types/responses/tool_param.py` | 3 | 399 | 0 | +| `src/openai/types/webhooks/live_call_incoming_webhook_event.py` | 3 | 71 | 0 | +| `src/openai/types/fine_tuning/fine_tuning_job_integration.py` | 2 | 5 | 0 | +| `src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py` | 2 | 40 | 0 | +| `src/openai/types/realtime/realtime_error_event.py` | 2 | 25 | 0 | + +The 27 files total 31,290 physical lines. Strict manifest coverage is 1,972 entries; +the legacy reporter counts 1,971 paths because it excludes workflows and includes +the manifest itself. These are different ownership measurements, not conflicting +file counts. The nine handwritten runtime modules remain outside generated ownership. + +The remaining work ships in **one consolidated SDK PR**. Compiler prerequisites +can be reviewed and merged independently. The parallel implementation lanes are: + +| Area | Tracks | Mixed files | Patch lines | Compiler capability and handwritten boundary | +| --- | --- | ---: | ---: | --- | +| Client and package | T2 | 2 | 1,005 | Typed client/package declarations delegate lifecycle and routing behavior to handwritten runtime modules; preserve original-module state and lexical superclass dispatch. | +| Parsing and request hooks | T3, T5, T6, T13 | 6 | 2,759 | Typed helper signatures, overloads, factories, request context and stream classes; parsing and request behavior stay handwritten. | +| Assistants and Sessions | T4, T9 | 3 | 1,992 | Generated polling/streaming facades preserve method order, deprecation decorators and positional request options; existing helper bodies move to handwritten modules. | +| Types, transports and docs | T1, T11, T12, T16 | 16 | 430 | Typed compatibility declarations, import bindings, transport options and documentation references; existing security/transport implementations stay intact. | + +Completion sequence: + +1. Merge compiler capabilities through ordinary review and green CI, preserving + unchanged output for configurations that do not opt in. +2. Build from merged compiler source. Preserve all 15 released migration settings + and use authenticated authored configuration on a feature branch for the final + SDK generation. Configuration composition must preserve loaded values and + ordinary parser/source limits. Default activation follows runtime delivery. +3. Generate every facade, integrate handwritten runtime files, and run the normal + formatter. Preserve signatures, overloads, raw docs, declaration order, exports, + defaults, subclass/monkeypatch behavior, sync/async semantics and warnings. +4. Run relevant SDK behavior/security/streaming suites and both type checkers. + Run the unchanged authoritative ownership/patch reporter against authentic + checkpoints; require **zero mixed files and zero custom patch lines**, without + changing ownership, exclusions or budgets. Record final per-file physical lines. +5. Request reviews for each required PR/Delta in #sdk-reviews, address all findings, + pass current-head CI and protected merging, then verify public post-merge CI. + +The implementation previews and compiler tests are development evidence. They do +not establish an additional delivered reduction; the final report and public +merge remain required. Machine-readable history and this plan are retained in +`docs/castiron-transition-inventory.json`. + +### Prepared handwritten runtime boundary + +The consolidated preview adds eight handwritten runtime modules and updates the +existing embeddings parser. They contain **3,427 physical lines** including blanks +and comments; this is implementation retained in handwritten ownership, not code +removed or a delivered mixed-file reduction. Refresh these counts after normal +final generation. + +| Handwritten module | Physical lines | Change | +| --- | ---: | --- | +| `src/openai/lib/_assistants_runs.py` | 768 | New | +| `src/openai/lib/_assistants_threads.py` | 287 | New | +| `src/openai/lib/_chat_completion_resources.py` | 490 | New | +| `src/openai/lib/_client_auth.py` | 977 | New | +| `src/openai/lib/_module_runtime.py` | 120 | New | +| `src/openai/lib/_parsing/_embeddings.py` | 60 | Updated | +| `src/openai/lib/_request_context.py` | 13 | New | +| `src/openai/lib/_response_resources.py` | 643 | New | +| `src/openai/lib/beta/agents/_create.py` | 69 | New | + ## Delivered integration — uploads, aliases and Live defaults [#4152](https://github.com/openai/openai-python/pull/4152) merged through the normal public queue on 2026-10-22 as `a29dc3947aeccecc2e85d8fbad4d7c03b8af13e9`. This delivered **48 → 32 mixed files** and **9,173 → 6,845 custom patch lines**, a net reduction of **2,328**. Private #315 is closed as completed; its generation checkpoints are retained. The 16 files below match generation byte for byte and account for 1,838 previous patch lines; generated-baseline changes explain the difference from the net reduction. @@ -31,13 +303,13 @@ No removals or renames of public symbols, new required arguments, narrower input The released integration preserves upload factories, subclass dispatch, lazy defaults and logging, aliases, Live queue defaults, and current/older-runtime WebSocket behavior. Its validation included 1,606 runtime/client/security tests with 16 skips, 158 reporting tests with one skip, full lint/type checks and independent checkpoint/contract review. All review feedback was addressed before normal protected merge. [Main post-merge CI](https://github.com/openai/openai-python/actions/runs/39727147208) passed. Historical private-candidate evidence is retained under `delivered_integrations` in the JSON. -## Current published baseline — 2026-10-22 +## Historical published baseline before polling — 2026-10-22 Released commit `a29dc3947aeccecc2e85d8fbad4d7c03b8af13e9` contains **32 mixed files; 5,632 additions + 1,213 deletions = 6,845 custom patch lines**, with **37,881 integrated versus 33,462 generated physical lines**. The public generated snapshot is `75ba2ba983afb0a8823fae82670c5323c0ee9908`, generation `9fa1f60f-3df4-4d35-8b22-19dbbb6c80fb`, content hash `f71f5997995e620542e459fbe3a23789b2fbacf139d5d0c805e79c456abd2f49`. The unchanged reporter verified real public checkpoint ancestry and the recorded hash with `--public --fetch --require-head-hash`; run from a clone whose origin is the public SDK, using this released SHA for both base and head to reproduce the inventory. -The snapshot owns 1,970 paths, including 1,797 source paths. Another 205 source paths / 35,731 physical lines remain outside generated ownership. The polling candidate retains this ownership, the existing exclusions and the 10,000-line budget. Physical lines include comments and blanks; patch additions plus deletions are not a count of executable statements. The original October 19 audit below remains historical; `current_audit` records this released baseline, and `published_audit_history` retains the previous 48-file audit. +The snapshot owns 1,970 paths, including 1,797 source paths. Another 205 source paths / 35,731 physical lines remain outside generated ownership. The polling integration retained this ownership, the existing exclusions and the 10,000-line budget. Physical lines include comments and blanks; patch additions plus deletions are not a count of executable statements. The original October 19 audit below remains historical; `published_audit_history` retains this baseline and the previous 48-file audit; the historical audit records retain the later 27-file polling release; `current_audit` now records fa9. -### Current per-file inventory +### Historical per-file inventory before polling | File | Integrated lines | Generated lines | Added | Deleted | Patch | Track | | --- | ---: | ---: | ---: | ---: | ---: | --- | @@ -74,9 +346,9 @@ The snapshot owns 1,970 paths, including 1,797 source paths. Another 205 source | `src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py` | 40 | 40 | 1 | 1 | 2 | T1 | | `src/openai/types/realtime/realtime_error_event.py` | 25 | 25 | 1 | 1 | 2 | T1 | -## Verified polling integration candidate — 2026-10-22 +## Delivered polling integration — 2026-10-22 -[Private #326](https://github.com/openai/openai-python-internal/pull/326) measures **32 → 27 mixed files** and **6,845 → 6,186 custom patch lines**, a further **659** reduction. This is a separately verified private candidate, not yet a merged public reduction. The 27 remaining files contain 4,979 additions + 1,207 deletions and 32,760 integrated versus 28,988 generated physical lines. +[Public #4155](https://github.com/openai/openai-python/pull/4155) delivered **32 → 27 mixed files** and **6,845 → 6,186 custom patch lines**, a further **659** reduction. Private #326 is closed as completed; its private checkpoint evidence below is retained. The 27 remaining files contain 4,979 additions + 1,207 deletions and 32,760 integrated versus 28,988 generated physical lines. Evaluated code: `aaa793c62214fa0c1541581aff6b4235fe5b9d24`. Normal generation used merged compiler `6959b4685e426934700ebb40c159e382714c8542`, pure checkpoint `7e3f90acf16f400505edeff5eb14ebb11981975a`, configuration checkpoint `db11a1a29f20f388bf6443d8bf5666e492f94777`, generation `18a47a5c-822b-47f7-9e5e-afd9c57dc6f6`, and content hash `d0c03ec7e35a1c576cfc7197aa8a3bd4e2b5e240ad90621c86437967444e3ef8`. The unchanged reporter (SHA-256 `6b7cfda1f85426daaa2e4f6186e2d1b20ac638dadaa54be8142ecec89fefd880`) verifies the authentic private checkpoint ancestry and recorded hash. @@ -94,9 +366,9 @@ The five files below now match the pure checkpoint exactly. Their 659 prior patc Validation: **303 polling/runtime compatibility tests passed** across synchronous, asynchronous and Trio behavior; the normal lint script passed Ruff, Pyright (zero errors), Mypy (2,047 files) and import verification. All **112 public facade contracts** and **16 runtime exports** are retained. Independent review verified that the ten delegate runtime bodies match released method bodies after receiver/validator-alias normalization, every argument is forwarded unchanged, and all other method bodies (including video polling) remain unchanged. Deadlines, validation-before-side-effects, cancellation, upload ownership, concurrency, headers, deprecation warnings and terminal-state behavior remain covered. -Local dependency freshness checks fail on both the unchanged released predecessor and this candidate under the managed registry. Local checks use the exact committed dependency set installed with `uv sync --frozen --all-extras` and no re-resolution; registry security settings and lockfiles are retained. Current hosted locked-install checks remain required before promotion. SDK CODEOWNER review and normal public checks/queue are also required; the independent audit does not replace them. +Local dependency freshness checks fail on both the unchanged released predecessor and this candidate under the managed registry. Local checks use the exact committed dependency set installed with `uv sync --frozen --all-extras` and no re-resolution; registry security settings and lockfiles are retained. The public promotion subsequently passed its required reviews/checks and normal queue; post-merge CI passed. The independent audit did not replace those gates. -### Remaining candidate inventory +### Remaining released inventory | File | Integrated lines | Generated lines | Added | Deleted | Patch | Track | | --- | ---: | ---: | ---: | ---: | ---: | --- | @@ -128,7 +400,7 @@ Local dependency freshness checks fail on both the unchanged released predecesso | `src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py` | 40 | 40 | 1 | 1 | 2 | T1 | | `src/openai/types/realtime/realtime_error_event.py` | 25 | 25 | 1 | 1 | 2 | T1 | -Machine-readable pending evidence is under `pending_candidate_audit` in [castiron-transition-inventory.json](castiron-transition-inventory.json). Reproduce from an internal SDK checkout containing the evaluated commit: +Historical private-candidate evidence is retained under `delivered_integrations`; the public release is recorded under `current_audit` in [castiron-transition-inventory.json](castiron-transition-inventory.json). Reproduce the historical private evaluation from an internal SDK checkout containing that commit: ```sh python3 -I scripts/castiron/custom_code_report.py report \ @@ -265,7 +537,7 @@ The maintained compiler is `api/castiron` in the OpenAI monorepo; the standalone ### Typed extension declarations and delegates -Extend the compiler model with typed resource helper declarations: method name and placement, parameter/overload sources, generic return types, sync/async mode, delegate target, request preparation/post-parser hooks, specialized stream classes, and raw/streaming-wrapper exposure. Validate these facts and project them into the Python renderer. Use existing configuration/model mechanisms where they fit; do not accept arbitrary code injection, raw SDK-file snapshots, or path-specific source replacement as the extension API. +Extend the compiler model with typed resource helper declarations: method name and placement, parameter/overload sources, generic return types, sync/async mode, delegate target, request preparation/post-parser hooks, specialized stream classes, and raw/streaming-wrapper exposure. Validate these facts and project them into the Python renderer. Use existing configuration/model mechanisms where they fit; do not accept arbitrary code injection, raw SDK-file snapshots, or path-specific source replacement as the extension API. Retained sync and async signatures must declare every local used by the typed request payload, path, validators and preparation hooks; reject omissions before emitting code. Resolve imports through exact generated or explicitly declared module/symbol pairs, deduplicate only proven re-exports, and check collisions against the globals actually emitted, including typed logger bindings. Generate exact signatures rather than `*args, **kwargs` facades. Derive shared parameters from canonical endpoint facts so future schema changes reach helper methods, overloads, and docs together. Existing tests comparing helper and endpoint signatures are part of the contract. Keep orchestration in the existing handwritten modules where possible: polling/deadlines, file preparation, parsing, streaming accumulation, authentication/provider integration, and upload lifecycle. Generated delegates are the stable boundary. @@ -445,17 +717,37 @@ If one successor needs a human review, CI repair, or another merged prerequisite | T0 generated reporting restoration | Original cleanup and renewed restoration merged | Original [#4113](https://github.com/openai/openai-python/pull/4113) remains historical. The renewed canonical reporting restoration is delivered in [#4152](https://github.com/openai/openai-python/pull/4152), merge a29dc394; all seven reporting files listed above match generation. | | T8 upload helper contract coverage | Merged; successor eligible | [#4112](https://github.com/openai/openai-python/pull/4112) merged on 2026-10-19 as `163a2966140bc0399af7d81e03580d9d907a51df`. Preserve its 16 sync/async contracts in the typed upload-delegation successor. The default 64 MiB boundary remains an additional coverage requirement. This test-only merge does not reduce mixed-file counts or approve a later upload migration. | | T10 video polling compatibility | Merged; successor eligible | [#4115](https://github.com/openai/openai-python/pull/4115) merged on 2026-10-19 as `969ae2c734a508ebc5e3370c9dc41bd15602a4a4`. Retain all 21 public-entrypoint sync/async contracts and the interval mutation evidence in the polling-family successor. This test-only merge does not reduce mixed-file counts. | -| T15 Beta resource alias generation | Compiler and SDK integration merged | [Delta 2566635](https://flow.apps.openai.org/deltas/openai/openai/2566635) merged as 509f49c1. [#4152](https://github.com/openai/openai-python/pull/4152) delivers the generated aliases, removing the 21-line Beta patch while preserving sync/async cached properties and collision guards. | -| T11 Live queue generation | Compiler and SDK integration merged | [Delta 2565610](https://flow.apps.openai.org/deltas/openai/openai/2565610) and [#4152](https://github.com/openai/openai-python/pull/4152) deliver generation for Live, forks and sideband, retaining byte limits, direct constructors and caller-owned empty queues. All three files match generation. | -| T1 authored schema documentation | Redundant candidate; merged source already present | [Delta 2565621](https://flow.apps.openai.org/deltas/openai/openai/2565621) duplicates the corrections already merged through [Delta 1490194](https://flow.apps.openai.org/deltas/openai/openai/1490194), commit `97ba353f886c5248912eb50a516cb1aab2fee4c6`. The duplicate is closed with supersession evidence. The generated/integrated SDK descriptions still differ at the current audit; verify input propagation and regenerate before claiming the two-file/four-line reduction. | -| T7 / T10 / T14 polling family | Compiler/runtime preparation merged; private integration verified | [Delta 2565776](https://flow.apps.openai.org/deltas/openai/openai/2565776) and runtime [#4148](https://github.com/openai/openai-python/pull/4148) are merged. [Private #326](https://github.com/openai/openai-python-internal/pull/326) applies exactly seven settings on released a29dc394; five files / 659 patch lines clear, 112 contracts and 16 exports remain, and 303 tests pass. Public promotion/merge remain pending. | -| T8 / T9 upload and agent facades | Compiler, runtime and SDK integration merged | [Delta 2568634](https://flow.apps.openai.org/deltas/openai/openai/2568634), runtime [#4146](https://github.com/openai/openai-python/pull/4146) and integration [#4152](https://github.com/openai/openai-python/pull/4152) are merged. Uploads, environment files and session artifacts match generation; synchronous factories on async resources, subclass dispatch, lazy defaults/logger timing, ordering, cleanup and the 64 MiB boundary remain covered. The separate sessions facade still has 124 patch lines. | -| T0 recurring reporting formatting | Canonical source and public integration merged | [Deltas 2588691](https://flow.apps.openai.org/deltas/openai/openai/2588691) and [2591310](https://flow.apps.openai.org/deltas/openai/openai/2591310) fix shared-template formatting and preserve the requested unittest import style. [#4152](https://github.com/openai/openai-python/pull/4152) clears seven reporting files / 1,023 prior patch lines. Budget and exclusions are unchanged. | -| T11 client WebSocket query composition | Compiler fixes and public integration merged | [Deltas 2589816](https://flow.apps.openai.org/deltas/openai/openai/2589816) and [2590767](https://flow.apps.openai.org/deltas/openai/openai/2590767) are delivered in [#4152](https://github.com/openai/openai-python/pull/4152). Released query semantics, runtime/subclass dispatch, older-runtime fallback, client properties and prepared translations remain verified. Client and translations files still have unrelated custom patches. | -| Earlier stable web-search naming | Superseded; stale candidate closed | [Internal #55](https://github.com/openai/openai-python-internal/pull/55) closed after confirming merged [Delta 2513050](https://flow.apps.openai.org/deltas/openai/openai/2513050) and exact generated/integrated equality of both stable web-search modules on current main. Do not reintroduce its stale generation metadata or historical cross-language naming change. | +| T15 Beta resource alias generation | Compiler and SDK integration merged | Delta 2566635 merged as 509f49c1. [#4152](https://github.com/openai/openai-python/pull/4152) delivers the generated aliases, removing the 21-line Beta patch while preserving sync/async cached properties and collision guards. | +| T11 Live queue generation | Compiler and SDK integration merged | Delta 2565610 and [#4152](https://github.com/openai/openai-python/pull/4152) deliver generation for Live, forks and sideband, retaining byte limits, direct constructors and caller-owned empty queues. All three files match generation. | +| T1 authored schema documentation | Redundant candidate; merged source already present | Delta 2565621 duplicates the corrections already merged through Delta 1490194, commit `97ba353f886c5248912eb50a516cb1aab2fee4c6`. The duplicate is closed with supersession evidence. The generated/integrated SDK descriptions still differ at the current audit; verify input propagation and regenerate before claiming the two-file/four-line reduction. | +| T7 / T10 / T14 polling family | Compiler, runtime and public integration merged | Delta 2565776 and runtime [#4148](https://github.com/openai/openai-python/pull/4148) are merged. Private #326 applies exactly seven settings on released a29dc394; five files / 659 patch lines clear, 112 contracts and 16 exports remain, and 303 tests pass. [Public #4155](https://github.com/openai/openai-python/pull/4155) merged as `7db9d884`; post-merge CI passed. | +| T8 / T9 upload and agent facades | Compiler, runtime and SDK integration merged | Delta 2568634, runtime [#4146](https://github.com/openai/openai-python/pull/4146) and integration [#4152](https://github.com/openai/openai-python/pull/4152) are merged. Uploads, environment files and session artifacts match generation; synchronous factories on async resources, subclass dispatch, lazy defaults/logger timing, ordering, cleanup and the 64 MiB boundary remain covered. The separate sessions facade still has 124 patch lines. | +| T0 recurring reporting formatting | Canonical source and public integration merged | Deltas 2588691 and 2591310 fix shared-template formatting and preserve the requested unittest import style. [#4152](https://github.com/openai/openai-python/pull/4152) clears seven reporting files / 1,023 prior patch lines. Budget and exclusions are unchanged. | +| T11 client WebSocket query composition | Compiler fixes and public integration merged | Deltas 2589816 and 2590767 are delivered in [#4152](https://github.com/openai/openai-python/pull/4152). Released query semantics, runtime/subclass dispatch, older-runtime fallback, client properties and prepared translations remain verified. Client and translations files still have unrelated custom patches. | +| Earlier stable web-search naming | Superseded; stale candidate closed | Internal #55 closed after confirming merged Delta 2513050 and exact generated/integrated equality of both stable web-search modules on current main. Do not reintroduce its stale generation metadata or historical cross-language naming change. | +| Remaining compatibility imports | Compiler merged; post-merge CI passed | Delta2592476, merge `66acf710bd337b3a9be0b0ad3412d9083e57d9bc`; exact-merge master CI 8915304 passed. | +| Assistants deprecation ordering | Compiler merged | Delta2592631, merge `b0da3235c40a165713cbb742b28d5cbe67cb9e20`; CI and protected validation passed. The response-format overload review finding is fixed. | +| Remaining types/transport/docs bindings | Compiler merged; exact-merge post-CI passed | Delta2592815 merged as `5f5b122cf752c580d70ca3bcd106ffa99c01c799`; current CI, protected validation and exact-merge master CI8917738 passed. The inactive-template whitespace regression is fixed without fixture changes, and the ordinary master conflict is resolved. | +| Shared helper/request hooks | Compiler merged; post-merge CI passed | Delta 2593282 merged as `a7b49aa295d583a377fcea247591b4621efe7a8d`. Current-head CI, protected validation and post-merge CI 8919804 passed. Three unrelated soft failures remain documented; no policies or unrelated code were changed. | +| Client/package declarations | Merged; nine review findings resolved | Delta 2593695, head `4ae71419874d772688a49f0d6efc8279e1e744db`, preserves all 31 upstream package projections. Import/reference resolution, native/class bindings and early/late exports have passing regressions. All 813 renderer tests, library/test Clippy and both immutable parity cases pass; all 1,957 generated files are byte-identical to the validated preview. All nine formal threads are resolved and current-head review approved. Merged as `34e4caadc819f51b8267061485739b6481644593`; protected merge and validation passed. Exact-merge post-merge CI passed. | + +The prepared consolidated preview has the following validation. Hosted checks and +review remain required on the published source and final SDK heads. + +| Validation | Result and scope | +| --- | --- | +| Compiler | 813 renderer tests and unchanged-policy library/test Clippy pass. All 1,957 preview output files are unchanged; both selected immutable exact/extended parity cases pass on the current source. | +| SDK declarations | 687 contracts preserved: 273 parsing/resource, 168 Assistants/Sessions and 246 client/package. | +| Normal SDK lint | Ruff, Pyright, Mypy across 2,055 source files, and import verification pass. | +| Package build | Local canonical locked build is blocked by managed-registry/lock-source mismatch on unchanged baseline metadata. Final hosted build and wheel checks on Python 3.10/3.14, including isolated legacy HTTPX compatibility, remain required. | +| Client behavior | 605 pass, two skipped; client Mypy also passes. | +| Resource/runtime behavior | 1,796 pass, 15 skipped; the latest three-file Audio/Responses change passes 1,194 affected tests. These runs overlap and must not be added together. | +| Large payloads and transport | 106 pass sequentially, including >32 MiB JSON/structured streaming, >64 MiB SSE, gzip, cleanup and cancellation; fixtures and limits unchanged. The 34 legacy HTTPX cases skip because the optional dependency/lane is absent locally. Hosted legacy compatibility coverage remains required. | +| Structural compatibility | All 14 compatibility module ASTs (79 classes/188 methods) match the release; four transport/security files are byte-identical. | +| Annotation bindings | 424 bindings and seven generic variables accounted for, including bindings checked on their moved runtime owner. | The ledger reports observed work; projected reductions are not merged reductions. Refresh it with PR/Delta links and exact tested heads as publication completes. ### Initial environment note -The pinned main checkout failed local `uv lock --check` during setup. The committed dependencies were installed using `uv sync --frozen --all-extras --no-install-project`, without changing project metadata or the lockfile. Record focused test results separately; this local setup result is not a successful locked-policy validation and should not be repaired by an unrelated dependency update in a transition PR. +The pinned main checkout failed local `uv lock --check` during setup. The same failure occurs in the final preview: the managed package index differs from the registry recorded in the unchanged lockfile, so canonical `uv export --locked` rejects it despite byte-identical released project/lock metadata. The committed dependencies were installed using `uv sync --frozen --all-extras --no-install-project`, without changing project metadata or the lockfile. Record focused test results separately; this local setup result is not a successful locked-policy validation and should not be repaired by an unrelated dependency update in a transition PR. diff --git a/docs/castiron-transition-inventory.json b/docs/castiron-transition-inventory.json index 31ab9fbcf8..596d0002dd 100644 --- a/docs/castiron-transition-inventory.json +++ b/docs/castiron-transition-inventory.json @@ -451,696 +451,333 @@ "commit": "9d12b565554c08bcee6287c0d8f671b8c97ed911", "evidence": "https://github.com/openai/openai-python/actions/runs/39315896992" } + }, + { + "date": "2026-10-22", + "pull_request": "https://github.com/openai/openai-python/pull/4155", + "merge_commit": "7db9d884c33b0d5ef252e9a4b6e7ae0bbd217e5b", + "public_codegen_commit": "5466ab74e52ebd7b818ac772b88a5074f114355b", + "totals": { + "mixed_files": 27, + "added_lines": 4979, + "deleted_lines": 1207, + "custom_patch_lines": 6186, + "current_physical_lines": 32760, + "generated_physical_lines": 28988 + }, + "post_merge_checks": { + "state": "successful", + "commit": "7db9d884c33b0d5ef252e9a4b6e7ae0bbd217e5b", + "evidence": "https://github.com/openai/openai-python/actions/runs/39730902045" + } } ], "current_audit": { "audit_date": "2026-10-22", - "sdk_commit": "a29dc3947aeccecc2e85d8fbad4d7c03b8af13e9", - "public_codegen_commit": "75ba2ba983afb0a8823fae82670c5323c0ee9908", - "generation_id": "9fa1f60f-3df4-4d35-8b22-19dbbb6c80fb", - "codegen_hash": "f71f5997995e620542e459fbe3a23789b2fbacf139d5d0c805e79c456abd2f49", - "reporter_sha256": "6b7cfda1f85426daaa2e4f6186e2d1b20ac638dadaa54be8142ecec89fefd880", - "measurement": "generated-owned paths; git diff --numstat --no-renames; additions plus deletions; physical lines include comments and blanks", + "sdk_commit": "cfa0a79b6395bb0561b28879122a9f90a87cdada", + "public_codegen_commit": "992b9cbba312b4bf10128b9f1d36112b48340363", + "authenticated_codegen_commit": "da0ab234c8b0b445d8a64a420465199b4824a45e", + "generation_id": "8e0b46e2-9f31-4393-9a7e-2be585d07efb", + "codegen_hash": "3051dd5f14816304442e438ac216fbc29b4e1a9b1f87873da6f3a4a947cd7fa0", + "status": "released public baseline; transition not delivered", "totals": { - "added": 5632, - "removed": 1213, - "patch": 6845, - "current_lines": 37881, - "generated_lines": 33462, - "mixed_files": 32 + "mixed_files": 41, + "added": 4890, + "removed": 2519, + "patch": 7409, + "current_lines": 39960 }, + "generated_manifest_entries": 1980, "files": [ { - "path": "src/openai/resources/beta/threads/runs/runs.py", - "added": 1290, - "removed": 181, - "patch": 1471, - "present": true, - "current_lines": 3161, - "generated_lines": 2052, - "track": "T4" + "path": "api.md", + "physical_lines": 2094, + "added": 126, + "removed": 8, + "sha256": "668a9076e479f27645ff1a53816b7d983c2767ad0a2520bbb08ccdf319e55f1a" }, { - "path": "src/openai/resources/responses/responses.py", - "added": 920, - "removed": 74, - "patch": 994, - "present": true, - "current_lines": 5181, - "generated_lines": 4335, - "track": "T5" + "path": "scripts/castiron/CUSTOM_CODE.md", + "physical_lines": 217, + "added": 0, + "removed": 40, + "sha256": "6578db0d17cf297ea01b862f772a716bef56a81192498f423463505950707701" }, { - "path": "src/openai/resources/audio/transcriptions.py", - "added": 287, - "removed": 601, - "patch": 888, - "present": true, - "current_lines": 1089, - "generated_lines": 1403, - "track": "T3" + "path": "scripts/castiron/custom_code_budget.py", + "physical_lines": 702, + "added": 1, + "removed": 4, + "sha256": "3f576ca4ddf34fdb3c43093192ca382e38ac66a703c1ff000f87fa288c3faa6c" }, { - "path": "src/openai/_client.py", - "added": 759, - "removed": 60, - "patch": 819, - "present": true, - "current_lines": 2440, - "generated_lines": 1741, - "track": "T2" + "path": "scripts/castiron/custom_code_policy.py", + "physical_lines": 481, + "added": 1, + "removed": 4, + "sha256": "e9b39f4f881a3adfe1cb6a01bb2bb0522463c98c554b3c21ae8d3b2f13ce23f5" }, { - "path": "src/openai/resources/chat/completions/completions.py", - "added": 603, - "removed": 1, - "patch": 604, - "present": true, - "current_lines": 3456, - "generated_lines": 2854, - "track": "T6" + "path": "scripts/castiron/custom_code_report.py", + "physical_lines": 1250, + "added": 19, + "removed": 254, + "sha256": "6b7cfda1f85426daaa2e4f6186e2d1b20ac638dadaa54be8142ecec89fefd880" }, { - "path": "src/openai/resources/beta/threads/threads.py", - "added": 390, - "removed": 7, - "patch": 397, - "present": true, - "current_lines": 2009, - "generated_lines": 1626, - "track": "T4" + "path": "scripts/castiron/fixtures/github_publisher.cjs", + "physical_lines": 69, + "added": 10, + "removed": 63, + "sha256": "40e08910601ccac410f22ed9397129b2cc1ff985e96109cda8d05608e58eaa56" }, { - "path": "src/openai/resources/vector_stores/file_batches.py", - "added": 242, - "removed": 3, - "patch": 245, - "present": true, - "current_lines": 867, - "generated_lines": 628, - "track": "T7" + "path": "scripts/castiron/test_custom_code_policy.py", + "physical_lines": 587, + "added": 0, + "removed": 4, + "sha256": "01b102c8e1f854ec2c4d2480dbcd55ae38008812fb3196e0309eb6ac8fbbb6d4" }, { - "path": "src/openai/resources/vector_stores/files.py", - "added": 220, - "removed": 1, - "patch": 221, - "present": true, - "current_lines": 1032, - "generated_lines": 813, - "track": "T7" + "path": "scripts/castiron/test_custom_code_publication.py", + "physical_lines": 873, + "added": 4, + "removed": 600, + "sha256": "23d28a75f62d65c0313081194726d34c85241234de9dbd7f085d2c2b1aa0406d" + }, + { + "path": "scripts/castiron/test_custom_code_report.py", + "physical_lines": 1084, + "added": 0, + "removed": 489, + "sha256": "f8f6b883c545963b4f82d8b078734298294607a9371e70e01fb0c3a37304be61" }, { "path": "src/openai/__init__.py", + "physical_lines": 505, "added": 185, "removed": 1, - "patch": 186, - "present": true, - "current_lines": 505, - "generated_lines": 321, - "track": "T2" + "sha256": "8f4fdba5193967d09ca79bc4504e79bd8c20ba4bd8f09c2df3817501540a39db" }, { - "path": "src/openai/resources/videos.py", - "added": 153, - "removed": 2, - "patch": 155, - "present": true, - "current_lines": 1457, - "generated_lines": 1306, - "track": "T10" + "path": "src/openai/_client.py", + "physical_lines": 2440, + "added": 759, + "removed": 60, + "sha256": "6de3a89ee9cb99104118fddd0b44ba13dbe94b78d088cd025ebae85e4c3b884e" }, { - "path": "api.md", - "added": 119, - "removed": 8, - "patch": 127, - "present": true, - "current_lines": 2090, - "generated_lines": 1979, - "track": "T16" + "path": "src/openai/resources/audio/transcriptions.py", + "physical_lines": 1089, + "added": 149, + "removed": 453, + "sha256": "3c8406db40522bf11e8bcdc77bcd06d9a12a7f0f6dda578fa2765e7ee6d42b76" + }, + { + "path": "src/openai/resources/audio/translations.py", + "physical_lines": 374, + "added": 21, + "removed": 46, + "sha256": "f921e5be3dbe5f2dee40e85f02aa40850fab8cd0db401fdb0342180ac096aac0" + }, + { + "path": "src/openai/resources/beta/agents/environments/files.py", + "physical_lines": 604, + "added": 6, + "removed": 6, + "sha256": "5d59d1b50d55a4747fff39c8a93e1a30733a6b9a1da0f0a2728151afbeeab850" + }, + { + "path": "src/openai/resources/beta/agents/sessions/artifacts.py", + "physical_lines": 573, + "added": 2, + "removed": 2, + "sha256": "c3bea665d60be82024758eb3ddee8c317ca9809b3e06004a8b51d04e223206f6" }, { "path": "src/openai/resources/beta/agents/sessions/sessions.py", + "physical_lines": 1304, "added": 113, "removed": 11, - "patch": 124, - "present": true, - "current_lines": 1304, - "generated_lines": 1202, - "track": "T9" - }, - { - "path": "src/openai/resources/audio/translations.py", - "added": 40, - "removed": 75, - "patch": 115, - "present": true, - "current_lines": 374, - "generated_lines": 409, - "track": "T3" + "sha256": "3077c41f66c90d54aa9f629500447277759f6caf8feb1202f357be1389dc421a" }, { "path": "src/openai/resources/beta/responses/responses.py", + "physical_lines": 5575, "added": 55, "removed": 55, - "patch": 110, - "present": true, - "current_lines": 5575, - "generated_lines": 5575, - "track": "T5" + "sha256": "aca7b656abd01d90bc23030690562738aaa0921bb33de8016e379e6aec34d294" }, { - "path": "src/openai/resources/webhooks/webhooks.py", - "added": 89, - "removed": 4, - "patch": 93, - "present": true, - "current_lines": 1146, - "generated_lines": 1061, - "track": "T12" + "path": "src/openai/resources/beta/threads/runs/runs.py", + "physical_lines": 3161, + "added": 1290, + "removed": 181, + "sha256": "dd2491939d56955dbc92fa6cdcdd92c4d0ffa780cad8e6167526c00eba149b3b" }, { - "path": "src/openai/resources/webhooks/__init__.py", - "added": 24, - "removed": 55, - "patch": 79, - "present": true, - "current_lines": 42, - "generated_lines": 73, - "track": "T12" + "path": "src/openai/resources/beta/threads/threads.py", + "physical_lines": 2009, + "added": 390, + "removed": 7, + "sha256": "aed41898a61e4f92be48ac39892f53ef983cc6384e9fff2e8a163751de3f2de5" }, { - "path": "src/openai/resources/realtime/translations/translations.py", - "added": 34, - "removed": 18, - "patch": 52, - "present": true, - "current_lines": 1152, - "generated_lines": 1136, - "track": "T11" + "path": "src/openai/resources/chat/completions/completions.py", + "physical_lines": 3456, + "added": 603, + "removed": 1, + "sha256": "01f0326e163f2a4a5bdc56301905958ecc16c9e4a2a47499c5ee6588995916f2" + }, + { + "path": "src/openai/resources/conversations/api.md", + "physical_lines": 42, + "added": 1, + "removed": 0, + "sha256": "6a36bb38e1e971829eba8655fe5056053f79d3ee2d75554e7ff3361b0457bf79" }, { "path": "src/openai/resources/embeddings.py", + "physical_lines": 263, "added": 27, "removed": 21, - "patch": 48, - "present": true, - "current_lines": 263, - "generated_lines": 257, - "track": "T13" - }, - { - "path": "src/openai/resources/files.py", - "added": 26, - "removed": 0, - "patch": 26, - "present": true, - "current_lines": 826, - "generated_lines": 800, - "track": "T14" + "sha256": "3c8f49bd504319f298b4cc5cc67d537e434814a3b06f352b41ad4a04fddaa262" }, { "path": "src/openai/resources/realtime/api.md", + "physical_lines": 149, "added": 0, - "removed": 20, - "patch": 20, - "present": true, - "current_lines": 149, - "generated_lines": 169, - "track": "T16" + "removed": 15, + "sha256": "8e3ed6e8e52a5bee45022706a34ac17bf3a6a160b67e2c3ff3232de8fea30de0" }, { "path": "src/openai/resources/realtime/realtime.py", + "physical_lines": 1660, "added": 9, "removed": 3, - "patch": 12, - "present": true, - "current_lines": 1660, - "generated_lines": 1654, - "track": "T11" - }, - { - "path": "src/openai/types/decision_input_image_param.py", - "added": 11, - "removed": 1, - "patch": 12, - "present": true, - "current_lines": 47, - "generated_lines": 37, - "track": "T1" - }, - { - "path": "src/openai/types/decision_input_text_param.py", - "added": 11, - "removed": 1, - "patch": 12, - "present": true, - "current_lines": 33, - "generated_lines": 23, - "track": "T1" - }, - { - "path": "tests/api_resources/test_videos.py", - "added": 12, - "removed": 0, - "patch": 12, - "present": true, - "current_lines": 939, - "generated_lines": 927, - "track": "T10" + "sha256": "0285ac5edb9b8b17f296b2d513068cb35c3413895e35424221765cc4a15c8592" }, { - "path": "src/openai/types/webhooks/__init__.py", - "added": 3, - "removed": 1, - "patch": 4, - "present": true, - "current_lines": 73, - "generated_lines": 71, - "track": "T1" + "path": "src/openai/resources/realtime/translations/translations.py", + "physical_lines": 1152, + "added": 34, + "removed": 18, + "sha256": "805923247278589582e4d4db869aa55f37a18130178d6c467f1c59b67825259c" }, { - "path": "src/openai/types/webhooks/unwrap_webhook_event.py", + "path": "src/openai/resources/responses/api.md", + "physical_lines": 208, "added": 2, - "removed": 2, - "patch": 4, - "present": true, - "current_lines": 74, - "generated_lines": 74, - "track": "T1" - }, - { - "path": "src/openai/types/responses/tool.py", - "added": 1, - "removed": 2, - "patch": 3, - "present": true, - "current_lines": 398, - "generated_lines": 399, - "track": "T1" - }, - { - "path": "src/openai/types/responses/tool_param.py", - "added": 3, "removed": 0, - "patch": 3, - "present": true, - "current_lines": 398, - "generated_lines": 395, - "track": "T1" - }, - { - "path": "src/openai/types/webhooks/live_call_incoming_webhook_event.py", - "added": 2, - "removed": 1, - "patch": 3, - "present": true, - "current_lines": 71, - "generated_lines": 70, - "track": "T1" - }, - { - "path": "src/openai/types/fine_tuning/fine_tuning_job_integration.py", - "added": 0, - "removed": 2, - "patch": 2, - "present": true, - "current_lines": 5, - "generated_lines": 7, - "track": "T1" - }, - { - "path": "src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py", - "added": 1, - "removed": 1, - "patch": 2, - "present": true, - "current_lines": 40, - "generated_lines": 40, - "track": "T1" - }, - { - "path": "src/openai/types/realtime/realtime_error_event.py", - "added": 1, - "removed": 1, - "patch": 2, - "present": true, - "current_lines": 25, - "generated_lines": 25, - "track": "T1" - } - ], - "ownership": { - "generated_paths": 1970, - "generated_source_paths": 1797, - "excluded_source_paths": 205, - "excluded_source_lines": 35731 - } - }, - "pending_candidate_audit": { - "audit_date": "2026-10-22", - "sdk_commit": "aaa793c62214fa0c1541581aff6b4235fe5b9d24", - "generation_id": "18a47a5c-822b-47f7-9e5e-afd9c57dc6f6", - "codegen_hash": "d0c03ec7e35a1c576cfc7197aa8a3bd4e2b5e240ad90621c86437967444e3ef8", - "reporter_sha256": "6b7cfda1f85426daaa2e4f6186e2d1b20ac638dadaa54be8142ecec89fefd880", - "measurement": "generated-owned paths; git diff --numstat --no-renames; additions plus deletions; physical lines include comments and blanks", - "totals": { - "added": 4979, - "removed": 1207, - "patch": 6186, - "current_lines": 32760, - "generated_lines": 28988, - "mixed_files": 27 - }, - "files": [ - { - "path": "src/openai/resources/beta/threads/runs/runs.py", - "added": 1290, - "removed": 181, - "patch": 1471, - "present": true, - "current_lines": 3161, - "generated_lines": 2052, - "track": "T4" + "sha256": "1a12a9f78e43efbfbed1e1d0c13ba023fbe71d540351c527cf9fbeb87a928a0b" }, { "path": "src/openai/resources/responses/responses.py", + "physical_lines": 5181, "added": 920, "removed": 74, - "patch": 994, - "present": true, - "current_lines": 5181, - "generated_lines": 4335, - "track": "T5" - }, - { - "path": "src/openai/resources/audio/transcriptions.py", - "added": 287, - "removed": 601, - "patch": 888, - "present": true, - "current_lines": 1089, - "generated_lines": 1403, - "track": "T3" - }, - { - "path": "src/openai/_client.py", - "added": 759, - "removed": 60, - "patch": 819, - "present": true, - "current_lines": 2440, - "generated_lines": 1741, - "track": "T2" - }, - { - "path": "src/openai/resources/chat/completions/completions.py", - "added": 603, - "removed": 1, - "patch": 604, - "present": true, - "current_lines": 3456, - "generated_lines": 2854, - "track": "T6" - }, - { - "path": "src/openai/resources/beta/threads/threads.py", - "added": 390, - "removed": 7, - "patch": 397, - "present": true, - "current_lines": 2009, - "generated_lines": 1626, - "track": "T4" + "sha256": "0aa6e43a2f059bba750249f50b94306fe2c513c3968b5f70b018c10c9f09123f" }, { - "path": "src/openai/__init__.py", - "added": 185, - "removed": 1, - "patch": 186, - "present": true, - "current_lines": 505, - "generated_lines": 321, - "track": "T2" - }, - { - "path": "api.md", - "added": 119, - "removed": 8, - "patch": 127, - "present": true, - "current_lines": 2090, - "generated_lines": 1979, - "track": "T16" - }, - { - "path": "src/openai/resources/beta/agents/sessions/sessions.py", - "added": 113, - "removed": 11, - "patch": 124, - "present": true, - "current_lines": 1304, - "generated_lines": 1202, - "track": "T9" + "path": "src/openai/resources/uploads/uploads.py", + "physical_lines": 652, + "added": 6, + "removed": 28, + "sha256": "cd5d957c36eb1e5da2bf0174d86817368e990e2657c5960a9cd5b8a9e94d0802" }, { - "path": "src/openai/resources/audio/translations.py", - "added": 40, - "removed": 75, - "patch": 115, - "present": true, - "current_lines": 374, - "generated_lines": 409, - "track": "T3" - }, - { - "path": "src/openai/resources/beta/responses/responses.py", - "added": 55, + "path": "src/openai/resources/webhooks/__init__.py", + "physical_lines": 42, + "added": 24, "removed": 55, - "patch": 110, - "present": true, - "current_lines": 5575, - "generated_lines": 5575, - "track": "T5" + "sha256": "426b361f424312c4bf7390560f00566d4ee89da90db3463cc2d74f46ac43ba55" }, { "path": "src/openai/resources/webhooks/webhooks.py", + "physical_lines": 1146, "added": 89, "removed": 4, - "patch": 93, - "present": true, - "current_lines": 1146, - "generated_lines": 1061, - "track": "T12" - }, - { - "path": "src/openai/resources/webhooks/__init__.py", - "added": 24, - "removed": 55, - "patch": 79, - "present": true, - "current_lines": 42, - "generated_lines": 73, - "track": "T12" - }, - { - "path": "src/openai/resources/realtime/translations/translations.py", - "added": 34, - "removed": 18, - "patch": 52, - "present": true, - "current_lines": 1152, - "generated_lines": 1136, - "track": "T11" - }, - { - "path": "src/openai/resources/embeddings.py", - "added": 27, - "removed": 21, - "patch": 48, - "present": true, - "current_lines": 263, - "generated_lines": 257, - "track": "T13" - }, - { - "path": "src/openai/resources/realtime/api.md", - "added": 0, - "removed": 20, - "patch": 20, - "present": true, - "current_lines": 149, - "generated_lines": 169, - "track": "T16" - }, - { - "path": "src/openai/resources/realtime/realtime.py", - "added": 9, - "removed": 3, - "patch": 12, - "present": true, - "current_lines": 1660, - "generated_lines": 1654, - "track": "T11" + "sha256": "f730653c5937f4c723f5519c7e86823fcba0ee088c270ecbd8e02fd10e49c654" }, { - "path": "src/openai/types/decision_input_image_param.py", - "added": 11, + "path": "src/openai/types/__init__.py", + "physical_lines": 157, + "added": 4, "removed": 1, - "patch": 12, - "present": true, - "current_lines": 47, - "generated_lines": 37, - "track": "T1" + "sha256": "98f15c4ee123816edfbbd0b758a67ca30b80f80939d158e8bed9a1b518d7c6ae" }, { - "path": "src/openai/types/decision_input_text_param.py", + "path": "src/openai/types/decision_input_image_param.py", + "physical_lines": 47, "added": 11, "removed": 1, - "patch": 12, - "present": true, - "current_lines": 33, - "generated_lines": 23, - "track": "T1" + "sha256": "347c633b1312fb62ecceffc147d0a250954392a09c07fa680d585dd41a3f8c60" }, { - "path": "src/openai/types/webhooks/__init__.py", - "added": 3, + "path": "src/openai/types/decision_input_message_param.py", + "physical_lines": 95, + "added": 6, "removed": 1, - "patch": 4, - "present": true, - "current_lines": 73, - "generated_lines": 71, - "track": "T1" - }, - { - "path": "src/openai/types/webhooks/unwrap_webhook_event.py", - "added": 2, - "removed": 2, - "patch": 4, - "present": true, - "current_lines": 74, - "generated_lines": 74, - "track": "T1" - }, - { - "path": "src/openai/types/responses/tool.py", - "added": 1, - "removed": 2, - "patch": 3, - "present": true, - "current_lines": 398, - "generated_lines": 399, - "track": "T1" - }, - { - "path": "src/openai/types/responses/tool_param.py", - "added": 3, - "removed": 0, - "patch": 3, - "present": true, - "current_lines": 398, - "generated_lines": 395, - "track": "T1" + "sha256": "22aff1b989c87ee05e3041c1caa9f3dd8540cbb381d96281ee75237b508297aa" }, { - "path": "src/openai/types/webhooks/live_call_incoming_webhook_event.py", - "added": 2, + "path": "src/openai/types/decision_input_text_param.py", + "physical_lines": 33, + "added": 11, "removed": 1, - "patch": 3, - "present": true, - "current_lines": 71, - "generated_lines": 70, - "track": "T1" + "sha256": "416864cec231da63e0324268e57192de9902399cf0119d061467c6117a3090c7" }, { "path": "src/openai/types/fine_tuning/fine_tuning_job_integration.py", + "physical_lines": 5, "added": 0, "removed": 2, - "patch": 2, - "present": true, - "current_lines": 5, - "generated_lines": 7, - "track": "T1" + "sha256": "add6f540e2493ce1c9e4b18c451669acd44443f84ffc364d03ba5299134c3f85" }, { "path": "src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py", + "physical_lines": 40, "added": 1, "removed": 1, - "patch": 2, - "present": true, - "current_lines": 40, - "generated_lines": 40, - "track": "T1" + "sha256": "e0e955bb2d08d25f9217a2b3f48b2ac0a20328af2ac1765bfb94f616de9c1e7f" }, { "path": "src/openai/types/realtime/realtime_error_event.py", + "physical_lines": 25, "added": 1, "removed": 1, - "patch": 2, - "present": true, - "current_lines": 25, - "generated_lines": 25, - "track": "T1" - } - ], - "ownership": { - "generated_paths": 1970, - "generated_source_paths": 1797, - "excluded_source_paths": 205, - "excluded_source_lines": 35731 - }, - "private_codegen_commit": "7e3f90acf16f400505edeff5eb14ebb11981975a", - "status": "private candidate; not a merged public reduction", - "pull_request": "https://github.com/openai/openai-python-internal/pull/326", - "base_sdk_commit": "a29dc3947aeccecc2e85d8fbad4d7c03b8af13e9", - "compiler_commit": "6959b4685e426934700ebb40c159e382714c8542", - "configuration_checkpoint": "db11a1a29f20f388bf6443d8bf5666e492f94777", - "verification": { - "public": false, - "fetch": true, - "require_head_hash": true, - "counts": { - "after": 27, - "baseline_changed": 5, - "before": 32, - "unchanged": 27 - } - }, - "removed_files": [ - { - "path": "src/openai/resources/files.py", - "previous_patch": 26, - "current_lines": 826, - "generated_lines": 826 + "sha256": "3caef26e78cca8e07685e5c40b8f12f7cc3416f473407eb254b6c7a38b6b52fe" }, { - "path": "src/openai/resources/vector_stores/file_batches.py", - "previous_patch": 245, - "current_lines": 776, - "generated_lines": 776 + "path": "src/openai/types/responses/tool_param.py", + "physical_lines": 398, + "added": 3, + "removed": 0, + "sha256": "e1b18d7cc6437215da9f1ca8f73f67807c335dc546328f02598a5d84fcc24733" }, { - "path": "src/openai/resources/vector_stores/files.py", - "previous_patch": 221, - "current_lines": 987, - "generated_lines": 987 + "path": "src/openai/types/webhooks/__init__.py", + "physical_lines": 73, + "added": 3, + "removed": 1, + "sha256": "a5774cf8e193a6d9299039fa823adb236b2dbb5cce675ccc9efbed2a613b1aa2" }, { - "path": "src/openai/resources/videos.py", - "previous_patch": 155, - "current_lines": 1444, - "generated_lines": 1444 + "path": "src/openai/types/webhooks/live_call_incoming_webhook_event.py", + "physical_lines": 71, + "added": 2, + "removed": 1, + "sha256": "2dd3bc70a9df1f12e5333983946e1df037a69f712ce104c75324768034df5c24" }, { - "path": "tests/api_resources/test_videos.py", - "previous_patch": 12, - "current_lines": 939, - "generated_lines": 939 + "path": "src/openai/types/webhooks/unwrap_webhook_event.py", + "physical_lines": 74, + "added": 2, + "removed": 2, + "sha256": "64626476d92e18f4727df732943b4479b6c0f9119f443f05a14aec7932a6fd2f" } ], - "changed_remaining_patches": [] + "verification": "Strict authentic-checkpoint report verifies the released baseline and candidate, with recorded hashes required. Physical counts and per-file patches are measured from immutable Git trees." }, "delivered_integrations": [ { @@ -1628,26 +1265,23 @@ } ], "private_candidate_code_commit": "5fcd05c9064e5bcfa820aed283d9570e1f997255", - "private_pull_request": "https://github.com/openai/openai-python-internal/pull/315", + "private_pull_request": "[private review reference omitted]", "public_codegen_commit": "75ba2ba983afb0a8823fae82670c5323c0ee9908" - } - ], - "published_audit_history": [ + }, { "audit_date": "2026-10-22", - "sdk_commit": "49e6757e464ce03b6f47c89740feaa75870ba168", - "public_codegen_commit": "f99cb5bb5fc87965058d7055e382a25d1d064bcc", - "generation_id": "dd7765a4-3f6a-4a04-aaca-19aaa7507cdd", - "codegen_hash": "a7aa886594dd13f43e180a8516000bdf96481ed147ba52f0f2def6e243bb03b5", + "sdk_commit": "aaa793c62214fa0c1541581aff6b4235fe5b9d24", + "generation_id": "18a47a5c-822b-47f7-9e5e-afd9c57dc6f6", + "codegen_hash": "d0c03ec7e35a1c576cfc7197aa8a3bd4e2b5e240ad90621c86437967444e3ef8", "reporter_sha256": "6b7cfda1f85426daaa2e4f6186e2d1b20ac638dadaa54be8142ecec89fefd880", "measurement": "generated-owned paths; git diff --numstat --no-renames; additions plus deletions; physical lines include comments and blanks", "totals": { - "mixed_files": 48, - "added_lines": 7565, - "deleted_lines": 1608, - "custom_patch_lines": 9173, - "current_physical_lines": 50243, - "generated_physical_lines": 44286 + "added": 4979, + "removed": 1207, + "patch": 6186, + "current_lines": 32760, + "generated_lines": 28988, + "mixed_files": 27 }, "files": [ { @@ -1662,12 +1296,12 @@ }, { "path": "src/openai/resources/responses/responses.py", - "added": 986, - "removed": 110, - "patch": 1096, + "added": 920, + "removed": 74, + "patch": 994, "present": true, - "current_lines": 5171, - "generated_lines": 4295, + "current_lines": 5181, + "generated_lines": 4335, "track": "T5" }, { @@ -1686,20 +1320,10 @@ "removed": 60, "patch": 819, "present": true, - "current_lines": 2401, - "generated_lines": 1702, + "current_lines": 2440, + "generated_lines": 1741, "track": "T2" }, - { - "path": "scripts/castiron/test_custom_code_report.py", - "added": 621, - "removed": 0, - "patch": 621, - "present": true, - "current_lines": 1081, - "generated_lines": 460, - "track": "T0" - }, { "path": "src/openai/resources/chat/completions/completions.py", "added": 603, @@ -1720,56 +1344,6 @@ "generated_lines": 1626, "track": "T4" }, - { - "path": "src/openai/resources/vector_stores/file_batches.py", - "added": 242, - "removed": 3, - "patch": 245, - "present": true, - "current_lines": 867, - "generated_lines": 628, - "track": "T7" - }, - { - "path": "src/openai/resources/uploads/uploads.py", - "added": 221, - "removed": 0, - "patch": 221, - "present": true, - "current_lines": 765, - "generated_lines": 544, - "track": "T8" - }, - { - "path": "src/openai/resources/vector_stores/files.py", - "added": 220, - "removed": 1, - "patch": 221, - "present": true, - "current_lines": 1032, - "generated_lines": 813, - "track": "T7" - }, - { - "path": "src/openai/resources/beta/responses/responses.py", - "added": 122, - "removed": 92, - "patch": 214, - "present": true, - "current_lines": 5565, - "generated_lines": 5535, - "track": "T5" - }, - { - "path": "scripts/castiron/custom_code_report.py", - "added": 189, - "removed": 17, - "patch": 206, - "present": true, - "current_lines": 1250, - "generated_lines": 1078, - "track": "T0" - }, { "path": "src/openai/__init__.py", "added": 185, @@ -1780,56 +1354,6 @@ "generated_lines": 321, "track": "T2" }, - { - "path": "src/openai/resources/live/sideband.py", - "added": 89, - "removed": 70, - "patch": 159, - "present": true, - "current_lines": 1413, - "generated_lines": 1394, - "track": "T11" - }, - { - "path": "src/openai/resources/videos.py", - "added": 153, - "removed": 2, - "patch": 155, - "present": true, - "current_lines": 1457, - "generated_lines": 1306, - "track": "T10" - }, - { - "path": "src/openai/resources/live/forks.py", - "added": 85, - "removed": 56, - "patch": 141, - "present": true, - "current_lines": 1446, - "generated_lines": 1417, - "track": "T11" - }, - { - "path": "src/openai/resources/realtime/translations/translations.py", - "added": 98, - "removed": 40, - "patch": 138, - "present": true, - "current_lines": 1152, - "generated_lines": 1094, - "track": "T11" - }, - { - "path": "src/openai/resources/realtime/realtime.py", - "added": 80, - "removed": 54, - "patch": 134, - "present": true, - "current_lines": 1650, - "generated_lines": 1624, - "track": "T11" - }, { "path": "api.md", "added": 119, @@ -1840,26 +1364,6 @@ "generated_lines": 1979, "track": "T16" }, - { - "path": "src/openai/resources/beta/agents/environments/files.py", - "added": 125, - "removed": 2, - "patch": 127, - "present": true, - "current_lines": 596, - "generated_lines": 473, - "track": "T9" - }, - { - "path": "src/openai/resources/live/live.py", - "added": 81, - "removed": 44, - "patch": 125, - "present": true, - "current_lines": 1643, - "generated_lines": 1606, - "track": "T11" - }, { "path": "src/openai/resources/beta/agents/sessions/sessions.py", "added": 113, @@ -1880,6 +1384,16 @@ "generated_lines": 409, "track": "T3" }, + { + "path": "src/openai/resources/beta/responses/responses.py", + "added": 55, + "removed": 55, + "patch": 110, + "present": true, + "current_lines": 5575, + "generated_lines": 5575, + "track": "T5" + }, { "path": "src/openai/resources/webhooks/webhooks.py", "added": 89, @@ -1890,16 +1404,6 @@ "generated_lines": 1061, "track": "T12" }, - { - "path": "tests/api_resources/test_videos.py", - "added": 54, - "removed": 34, - "patch": 88, - "present": true, - "current_lines": 939, - "generated_lines": 919, - "track": "T10" - }, { "path": "src/openai/resources/webhooks/__init__.py", "added": 24, @@ -1911,24 +1415,14 @@ "track": "T12" }, { - "path": "scripts/castiron/test_custom_code_budget.py", - "added": 66, - "removed": 4, - "patch": 70, - "present": true, - "current_lines": 773, - "generated_lines": 711, - "track": "T0" - }, - { - "path": "scripts/castiron/custom_code_budget.py", - "added": 41, - "removed": 20, - "patch": 61, + "path": "src/openai/resources/realtime/translations/translations.py", + "added": 34, + "removed": 18, + "patch": 52, "present": true, - "current_lines": 758, - "generated_lines": 737, - "track": "T0" + "current_lines": 1152, + "generated_lines": 1136, + "track": "T11" }, { "path": "src/openai/resources/embeddings.py", @@ -1940,46 +1434,6 @@ "generated_lines": 257, "track": "T13" }, - { - "path": "scripts/castiron/test_custom_code_github.py", - "added": 31, - "removed": 0, - "patch": 31, - "present": true, - "current_lines": 653, - "generated_lines": 622, - "track": "T0" - }, - { - "path": "scripts/castiron/test_custom_code_publication.py", - "added": 26, - "removed": 1, - "patch": 27, - "present": true, - "current_lines": 567, - "generated_lines": 542, - "track": "T0" - }, - { - "path": "src/openai/resources/files.py", - "added": 26, - "removed": 0, - "patch": 26, - "present": true, - "current_lines": 826, - "generated_lines": 800, - "track": "T14" - }, - { - "path": "src/openai/resources/beta/beta.py", - "added": 21, - "removed": 0, - "patch": 21, - "present": true, - "current_lines": 269, - "generated_lines": 248, - "track": "T15" - }, { "path": "src/openai/resources/realtime/api.md", "added": 0, @@ -1990,6 +1444,16 @@ "generated_lines": 169, "track": "T16" }, + { + "path": "src/openai/resources/realtime/realtime.py", + "added": 9, + "removed": 3, + "patch": 12, + "present": true, + "current_lines": 1660, + "generated_lines": 1654, + "track": "T11" + }, { "path": "src/openai/types/decision_input_image_param.py", "added": 11, @@ -2011,44 +1475,14 @@ "track": "T1" }, { - "path": "src/openai/resources/beta/agents/sessions/artifacts.py", - "added": 10, - "removed": 0, - "patch": 10, + "path": "src/openai/types/webhooks/__init__.py", + "added": 3, + "removed": 1, + "patch": 4, "present": true, - "current_lines": 561, - "generated_lines": 551, - "track": "T9" - }, - { - "path": "tests/api_resources/admin/organization/projects/test_api_keys.py", - "added": 10, - "removed": 0, - "patch": 10, - "present": true, - "current_lines": 442, - "generated_lines": 432, - "track": "T1" - }, - { - "path": "scripts/castiron/fixtures/github_publisher.cjs", - "added": 6, - "removed": 1, - "patch": 7, - "present": true, - "current_lines": 64, - "generated_lines": 59, - "track": "T0" - }, - { - "path": "src/openai/types/webhooks/__init__.py", - "added": 3, - "removed": 1, - "patch": 4, - "present": true, - "current_lines": 73, - "generated_lines": 71, - "track": "T1" + "current_lines": 73, + "generated_lines": 71, + "track": "T1" }, { "path": "src/openai/types/webhooks/unwrap_webhook_event.py", @@ -2119,24 +1553,4540 @@ "current_lines": 25, "generated_lines": 25, "track": "T1" - }, - { - "path": "src/openai/types/__init__.py", - "added": 1, - "removed": 0, - "patch": 1, - "present": true, - "current_lines": 150, - "generated_lines": 149, - "track": "T2" } ], "ownership": { - "generated_paths": 1968, + "generated_paths": 1970, "generated_source_paths": 1797, "excluded_source_paths": 205, - "excluded_source_lines": 35728 - } + "excluded_source_lines": 35731 + }, + "private_codegen_commit": "7e3f90acf16f400505edeff5eb14ebb11981975a", + "status": "delivered publicly; private candidate closed", + "pull_request": "[private review reference omitted]", + "base_sdk_commit": "a29dc3947aeccecc2e85d8fbad4d7c03b8af13e9", + "compiler_commit": "6959b4685e426934700ebb40c159e382714c8542", + "configuration_checkpoint": "db11a1a29f20f388bf6443d8bf5666e492f94777", + "verification": { + "public": false, + "fetch": true, + "require_head_hash": true, + "counts": { + "after": 27, + "baseline_changed": 5, + "before": 32, + "unchanged": 27 + } + }, + "removed_files": [ + { + "path": "src/openai/resources/files.py", + "previous_patch": 26, + "current_lines": 826, + "generated_lines": 826 + }, + { + "path": "src/openai/resources/vector_stores/file_batches.py", + "previous_patch": 245, + "current_lines": 776, + "generated_lines": 776 + }, + { + "path": "src/openai/resources/vector_stores/files.py", + "previous_patch": 221, + "current_lines": 987, + "generated_lines": 987 + }, + { + "path": "src/openai/resources/videos.py", + "previous_patch": 155, + "current_lines": 1444, + "generated_lines": 1444 + }, + { + "path": "tests/api_resources/test_videos.py", + "previous_patch": 12, + "current_lines": 939, + "generated_lines": 939 + } + ], + "changed_remaining_patches": [], + "public_pull_request": "https://github.com/openai/openai-python/pull/4155", + "public_merge_commit": "7db9d884c33b0d5ef252e9a4b6e7ae0bbd217e5b", + "public_codegen_commit": "5466ab74e52ebd7b818ac772b88a5074f114355b", + "postmerge_ci": "https://github.com/openai/openai-python/actions/runs/39730902045" } - ] + ], + "published_audit_history": [ + { + "audit_date": "2026-10-22", + "sdk_commit": "49e6757e464ce03b6f47c89740feaa75870ba168", + "public_codegen_commit": "f99cb5bb5fc87965058d7055e382a25d1d064bcc", + "generation_id": "dd7765a4-3f6a-4a04-aaca-19aaa7507cdd", + "codegen_hash": "a7aa886594dd13f43e180a8516000bdf96481ed147ba52f0f2def6e243bb03b5", + "reporter_sha256": "6b7cfda1f85426daaa2e4f6186e2d1b20ac638dadaa54be8142ecec89fefd880", + "measurement": "generated-owned paths; git diff --numstat --no-renames; additions plus deletions; physical lines include comments and blanks", + "totals": { + "mixed_files": 48, + "added_lines": 7565, + "deleted_lines": 1608, + "custom_patch_lines": 9173, + "current_physical_lines": 50243, + "generated_physical_lines": 44286 + }, + "files": [ + { + "path": "src/openai/resources/beta/threads/runs/runs.py", + "added": 1290, + "removed": 181, + "patch": 1471, + "present": true, + "current_lines": 3161, + "generated_lines": 2052, + "track": "T4" + }, + { + "path": "src/openai/resources/responses/responses.py", + "added": 986, + "removed": 110, + "patch": 1096, + "present": true, + "current_lines": 5171, + "generated_lines": 4295, + "track": "T5" + }, + { + "path": "src/openai/resources/audio/transcriptions.py", + "added": 287, + "removed": 601, + "patch": 888, + "present": true, + "current_lines": 1089, + "generated_lines": 1403, + "track": "T3" + }, + { + "path": "src/openai/_client.py", + "added": 759, + "removed": 60, + "patch": 819, + "present": true, + "current_lines": 2401, + "generated_lines": 1702, + "track": "T2" + }, + { + "path": "scripts/castiron/test_custom_code_report.py", + "added": 621, + "removed": 0, + "patch": 621, + "present": true, + "current_lines": 1081, + "generated_lines": 460, + "track": "T0" + }, + { + "path": "src/openai/resources/chat/completions/completions.py", + "added": 603, + "removed": 1, + "patch": 604, + "present": true, + "current_lines": 3456, + "generated_lines": 2854, + "track": "T6" + }, + { + "path": "src/openai/resources/beta/threads/threads.py", + "added": 390, + "removed": 7, + "patch": 397, + "present": true, + "current_lines": 2009, + "generated_lines": 1626, + "track": "T4" + }, + { + "path": "src/openai/resources/vector_stores/file_batches.py", + "added": 242, + "removed": 3, + "patch": 245, + "present": true, + "current_lines": 867, + "generated_lines": 628, + "track": "T7" + }, + { + "path": "src/openai/resources/uploads/uploads.py", + "added": 221, + "removed": 0, + "patch": 221, + "present": true, + "current_lines": 765, + "generated_lines": 544, + "track": "T8" + }, + { + "path": "src/openai/resources/vector_stores/files.py", + "added": 220, + "removed": 1, + "patch": 221, + "present": true, + "current_lines": 1032, + "generated_lines": 813, + "track": "T7" + }, + { + "path": "src/openai/resources/beta/responses/responses.py", + "added": 122, + "removed": 92, + "patch": 214, + "present": true, + "current_lines": 5565, + "generated_lines": 5535, + "track": "T5" + }, + { + "path": "scripts/castiron/custom_code_report.py", + "added": 189, + "removed": 17, + "patch": 206, + "present": true, + "current_lines": 1250, + "generated_lines": 1078, + "track": "T0" + }, + { + "path": "src/openai/__init__.py", + "added": 185, + "removed": 1, + "patch": 186, + "present": true, + "current_lines": 505, + "generated_lines": 321, + "track": "T2" + }, + { + "path": "src/openai/resources/live/sideband.py", + "added": 89, + "removed": 70, + "patch": 159, + "present": true, + "current_lines": 1413, + "generated_lines": 1394, + "track": "T11" + }, + { + "path": "src/openai/resources/videos.py", + "added": 153, + "removed": 2, + "patch": 155, + "present": true, + "current_lines": 1457, + "generated_lines": 1306, + "track": "T10" + }, + { + "path": "src/openai/resources/live/forks.py", + "added": 85, + "removed": 56, + "patch": 141, + "present": true, + "current_lines": 1446, + "generated_lines": 1417, + "track": "T11" + }, + { + "path": "src/openai/resources/realtime/translations/translations.py", + "added": 98, + "removed": 40, + "patch": 138, + "present": true, + "current_lines": 1152, + "generated_lines": 1094, + "track": "T11" + }, + { + "path": "src/openai/resources/realtime/realtime.py", + "added": 80, + "removed": 54, + "patch": 134, + "present": true, + "current_lines": 1650, + "generated_lines": 1624, + "track": "T11" + }, + { + "path": "api.md", + "added": 119, + "removed": 8, + "patch": 127, + "present": true, + "current_lines": 2090, + "generated_lines": 1979, + "track": "T16" + }, + { + "path": "src/openai/resources/beta/agents/environments/files.py", + "added": 125, + "removed": 2, + "patch": 127, + "present": true, + "current_lines": 596, + "generated_lines": 473, + "track": "T9" + }, + { + "path": "src/openai/resources/live/live.py", + "added": 81, + "removed": 44, + "patch": 125, + "present": true, + "current_lines": 1643, + "generated_lines": 1606, + "track": "T11" + }, + { + "path": "src/openai/resources/beta/agents/sessions/sessions.py", + "added": 113, + "removed": 11, + "patch": 124, + "present": true, + "current_lines": 1304, + "generated_lines": 1202, + "track": "T9" + }, + { + "path": "src/openai/resources/audio/translations.py", + "added": 40, + "removed": 75, + "patch": 115, + "present": true, + "current_lines": 374, + "generated_lines": 409, + "track": "T3" + }, + { + "path": "src/openai/resources/webhooks/webhooks.py", + "added": 89, + "removed": 4, + "patch": 93, + "present": true, + "current_lines": 1146, + "generated_lines": 1061, + "track": "T12" + }, + { + "path": "tests/api_resources/test_videos.py", + "added": 54, + "removed": 34, + "patch": 88, + "present": true, + "current_lines": 939, + "generated_lines": 919, + "track": "T10" + }, + { + "path": "src/openai/resources/webhooks/__init__.py", + "added": 24, + "removed": 55, + "patch": 79, + "present": true, + "current_lines": 42, + "generated_lines": 73, + "track": "T12" + }, + { + "path": "scripts/castiron/test_custom_code_budget.py", + "added": 66, + "removed": 4, + "patch": 70, + "present": true, + "current_lines": 773, + "generated_lines": 711, + "track": "T0" + }, + { + "path": "scripts/castiron/custom_code_budget.py", + "added": 41, + "removed": 20, + "patch": 61, + "present": true, + "current_lines": 758, + "generated_lines": 737, + "track": "T0" + }, + { + "path": "src/openai/resources/embeddings.py", + "added": 27, + "removed": 21, + "patch": 48, + "present": true, + "current_lines": 263, + "generated_lines": 257, + "track": "T13" + }, + { + "path": "scripts/castiron/test_custom_code_github.py", + "added": 31, + "removed": 0, + "patch": 31, + "present": true, + "current_lines": 653, + "generated_lines": 622, + "track": "T0" + }, + { + "path": "scripts/castiron/test_custom_code_publication.py", + "added": 26, + "removed": 1, + "patch": 27, + "present": true, + "current_lines": 567, + "generated_lines": 542, + "track": "T0" + }, + { + "path": "src/openai/resources/files.py", + "added": 26, + "removed": 0, + "patch": 26, + "present": true, + "current_lines": 826, + "generated_lines": 800, + "track": "T14" + }, + { + "path": "src/openai/resources/beta/beta.py", + "added": 21, + "removed": 0, + "patch": 21, + "present": true, + "current_lines": 269, + "generated_lines": 248, + "track": "T15" + }, + { + "path": "src/openai/resources/realtime/api.md", + "added": 0, + "removed": 20, + "patch": 20, + "present": true, + "current_lines": 149, + "generated_lines": 169, + "track": "T16" + }, + { + "path": "src/openai/types/decision_input_image_param.py", + "added": 11, + "removed": 1, + "patch": 12, + "present": true, + "current_lines": 47, + "generated_lines": 37, + "track": "T1" + }, + { + "path": "src/openai/types/decision_input_text_param.py", + "added": 11, + "removed": 1, + "patch": 12, + "present": true, + "current_lines": 33, + "generated_lines": 23, + "track": "T1" + }, + { + "path": "src/openai/resources/beta/agents/sessions/artifacts.py", + "added": 10, + "removed": 0, + "patch": 10, + "present": true, + "current_lines": 561, + "generated_lines": 551, + "track": "T9" + }, + { + "path": "tests/api_resources/admin/organization/projects/test_api_keys.py", + "added": 10, + "removed": 0, + "patch": 10, + "present": true, + "current_lines": 442, + "generated_lines": 432, + "track": "T1" + }, + { + "path": "scripts/castiron/fixtures/github_publisher.cjs", + "added": 6, + "removed": 1, + "patch": 7, + "present": true, + "current_lines": 64, + "generated_lines": 59, + "track": "T0" + }, + { + "path": "src/openai/types/webhooks/__init__.py", + "added": 3, + "removed": 1, + "patch": 4, + "present": true, + "current_lines": 73, + "generated_lines": 71, + "track": "T1" + }, + { + "path": "src/openai/types/webhooks/unwrap_webhook_event.py", + "added": 2, + "removed": 2, + "patch": 4, + "present": true, + "current_lines": 74, + "generated_lines": 74, + "track": "T1" + }, + { + "path": "src/openai/types/responses/tool.py", + "added": 1, + "removed": 2, + "patch": 3, + "present": true, + "current_lines": 398, + "generated_lines": 399, + "track": "T1" + }, + { + "path": "src/openai/types/responses/tool_param.py", + "added": 3, + "removed": 0, + "patch": 3, + "present": true, + "current_lines": 398, + "generated_lines": 395, + "track": "T1" + }, + { + "path": "src/openai/types/webhooks/live_call_incoming_webhook_event.py", + "added": 2, + "removed": 1, + "patch": 3, + "present": true, + "current_lines": 71, + "generated_lines": 70, + "track": "T1" + }, + { + "path": "src/openai/types/fine_tuning/fine_tuning_job_integration.py", + "added": 0, + "removed": 2, + "patch": 2, + "present": true, + "current_lines": 5, + "generated_lines": 7, + "track": "T1" + }, + { + "path": "src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py", + "added": 1, + "removed": 1, + "patch": 2, + "present": true, + "current_lines": 40, + "generated_lines": 40, + "track": "T1" + }, + { + "path": "src/openai/types/realtime/realtime_error_event.py", + "added": 1, + "removed": 1, + "patch": 2, + "present": true, + "current_lines": 25, + "generated_lines": 25, + "track": "T1" + }, + { + "path": "src/openai/types/__init__.py", + "added": 1, + "removed": 0, + "patch": 1, + "present": true, + "current_lines": 150, + "generated_lines": 149, + "track": "T2" + } + ], + "ownership": { + "generated_paths": 1968, + "generated_source_paths": 1797, + "excluded_source_paths": 205, + "excluded_source_lines": 35728 + } + }, + { + "audit_date": "2026-10-22", + "sdk_commit": "a29dc3947aeccecc2e85d8fbad4d7c03b8af13e9", + "public_codegen_commit": "75ba2ba983afb0a8823fae82670c5323c0ee9908", + "generation_id": "9fa1f60f-3df4-4d35-8b22-19dbbb6c80fb", + "codegen_hash": "f71f5997995e620542e459fbe3a23789b2fbacf139d5d0c805e79c456abd2f49", + "reporter_sha256": "6b7cfda1f85426daaa2e4f6186e2d1b20ac638dadaa54be8142ecec89fefd880", + "measurement": "generated-owned paths; git diff --numstat --no-renames; additions plus deletions; physical lines include comments and blanks", + "totals": { + "added": 5632, + "removed": 1213, + "patch": 6845, + "current_lines": 37881, + "generated_lines": 33462, + "mixed_files": 32 + }, + "files": [ + { + "path": "src/openai/resources/beta/threads/runs/runs.py", + "added": 1290, + "removed": 181, + "patch": 1471, + "present": true, + "current_lines": 3161, + "generated_lines": 2052, + "track": "T4" + }, + { + "path": "src/openai/resources/responses/responses.py", + "added": 920, + "removed": 74, + "patch": 994, + "present": true, + "current_lines": 5181, + "generated_lines": 4335, + "track": "T5" + }, + { + "path": "src/openai/resources/audio/transcriptions.py", + "added": 287, + "removed": 601, + "patch": 888, + "present": true, + "current_lines": 1089, + "generated_lines": 1403, + "track": "T3" + }, + { + "path": "src/openai/_client.py", + "added": 759, + "removed": 60, + "patch": 819, + "present": true, + "current_lines": 2440, + "generated_lines": 1741, + "track": "T2" + }, + { + "path": "src/openai/resources/chat/completions/completions.py", + "added": 603, + "removed": 1, + "patch": 604, + "present": true, + "current_lines": 3456, + "generated_lines": 2854, + "track": "T6" + }, + { + "path": "src/openai/resources/beta/threads/threads.py", + "added": 390, + "removed": 7, + "patch": 397, + "present": true, + "current_lines": 2009, + "generated_lines": 1626, + "track": "T4" + }, + { + "path": "src/openai/resources/vector_stores/file_batches.py", + "added": 242, + "removed": 3, + "patch": 245, + "present": true, + "current_lines": 867, + "generated_lines": 628, + "track": "T7" + }, + { + "path": "src/openai/resources/vector_stores/files.py", + "added": 220, + "removed": 1, + "patch": 221, + "present": true, + "current_lines": 1032, + "generated_lines": 813, + "track": "T7" + }, + { + "path": "src/openai/__init__.py", + "added": 185, + "removed": 1, + "patch": 186, + "present": true, + "current_lines": 505, + "generated_lines": 321, + "track": "T2" + }, + { + "path": "src/openai/resources/videos.py", + "added": 153, + "removed": 2, + "patch": 155, + "present": true, + "current_lines": 1457, + "generated_lines": 1306, + "track": "T10" + }, + { + "path": "api.md", + "added": 119, + "removed": 8, + "patch": 127, + "present": true, + "current_lines": 2090, + "generated_lines": 1979, + "track": "T16" + }, + { + "path": "src/openai/resources/beta/agents/sessions/sessions.py", + "added": 113, + "removed": 11, + "patch": 124, + "present": true, + "current_lines": 1304, + "generated_lines": 1202, + "track": "T9" + }, + { + "path": "src/openai/resources/audio/translations.py", + "added": 40, + "removed": 75, + "patch": 115, + "present": true, + "current_lines": 374, + "generated_lines": 409, + "track": "T3" + }, + { + "path": "src/openai/resources/beta/responses/responses.py", + "added": 55, + "removed": 55, + "patch": 110, + "present": true, + "current_lines": 5575, + "generated_lines": 5575, + "track": "T5" + }, + { + "path": "src/openai/resources/webhooks/webhooks.py", + "added": 89, + "removed": 4, + "patch": 93, + "present": true, + "current_lines": 1146, + "generated_lines": 1061, + "track": "T12" + }, + { + "path": "src/openai/resources/webhooks/__init__.py", + "added": 24, + "removed": 55, + "patch": 79, + "present": true, + "current_lines": 42, + "generated_lines": 73, + "track": "T12" + }, + { + "path": "src/openai/resources/realtime/translations/translations.py", + "added": 34, + "removed": 18, + "patch": 52, + "present": true, + "current_lines": 1152, + "generated_lines": 1136, + "track": "T11" + }, + { + "path": "src/openai/resources/embeddings.py", + "added": 27, + "removed": 21, + "patch": 48, + "present": true, + "current_lines": 263, + "generated_lines": 257, + "track": "T13" + }, + { + "path": "src/openai/resources/files.py", + "added": 26, + "removed": 0, + "patch": 26, + "present": true, + "current_lines": 826, + "generated_lines": 800, + "track": "T14" + }, + { + "path": "src/openai/resources/realtime/api.md", + "added": 0, + "removed": 20, + "patch": 20, + "present": true, + "current_lines": 149, + "generated_lines": 169, + "track": "T16" + }, + { + "path": "src/openai/resources/realtime/realtime.py", + "added": 9, + "removed": 3, + "patch": 12, + "present": true, + "current_lines": 1660, + "generated_lines": 1654, + "track": "T11" + }, + { + "path": "src/openai/types/decision_input_image_param.py", + "added": 11, + "removed": 1, + "patch": 12, + "present": true, + "current_lines": 47, + "generated_lines": 37, + "track": "T1" + }, + { + "path": "src/openai/types/decision_input_text_param.py", + "added": 11, + "removed": 1, + "patch": 12, + "present": true, + "current_lines": 33, + "generated_lines": 23, + "track": "T1" + }, + { + "path": "tests/api_resources/test_videos.py", + "added": 12, + "removed": 0, + "patch": 12, + "present": true, + "current_lines": 939, + "generated_lines": 927, + "track": "T10" + }, + { + "path": "src/openai/types/webhooks/__init__.py", + "added": 3, + "removed": 1, + "patch": 4, + "present": true, + "current_lines": 73, + "generated_lines": 71, + "track": "T1" + }, + { + "path": "src/openai/types/webhooks/unwrap_webhook_event.py", + "added": 2, + "removed": 2, + "patch": 4, + "present": true, + "current_lines": 74, + "generated_lines": 74, + "track": "T1" + }, + { + "path": "src/openai/types/responses/tool.py", + "added": 1, + "removed": 2, + "patch": 3, + "present": true, + "current_lines": 398, + "generated_lines": 399, + "track": "T1" + }, + { + "path": "src/openai/types/responses/tool_param.py", + "added": 3, + "removed": 0, + "patch": 3, + "present": true, + "current_lines": 398, + "generated_lines": 395, + "track": "T1" + }, + { + "path": "src/openai/types/webhooks/live_call_incoming_webhook_event.py", + "added": 2, + "removed": 1, + "patch": 3, + "present": true, + "current_lines": 71, + "generated_lines": 70, + "track": "T1" + }, + { + "path": "src/openai/types/fine_tuning/fine_tuning_job_integration.py", + "added": 0, + "removed": 2, + "patch": 2, + "present": true, + "current_lines": 5, + "generated_lines": 7, + "track": "T1" + }, + { + "path": "src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py", + "added": 1, + "removed": 1, + "patch": 2, + "present": true, + "current_lines": 40, + "generated_lines": 40, + "track": "T1" + }, + { + "path": "src/openai/types/realtime/realtime_error_event.py", + "added": 1, + "removed": 1, + "patch": 2, + "present": true, + "current_lines": 25, + "generated_lines": 25, + "track": "T1" + } + ], + "ownership": { + "generated_paths": 1970, + "generated_source_paths": 1797, + "excluded_source_paths": 205, + "excluded_source_lines": 35731 + } + }, + { + "audit_date": "2026-10-22", + "sdk_commit": "7db9d884c33b0d5ef252e9a4b6e7ae0bbd217e5b", + "public_codegen_commit": "5466ab74e52ebd7b818ac772b88a5074f114355b", + "generation_id": "18a47a5c-822b-47f7-9e5e-afd9c57dc6f6", + "codegen_hash": "d0c03ec7e35a1c576cfc7197aa8a3bd4e2b5e240ad90621c86437967444e3ef8", + "reporter_sha256": "6b7cfda1f85426daaa2e4f6186e2d1b20ac638dadaa54be8142ecec89fefd880", + "measurement": "generated-owned paths; git diff --numstat --no-renames; additions plus deletions; physical lines include comments and blanks", + "totals": { + "added": 4979, + "removed": 1207, + "patch": 6186, + "current_lines": 32760, + "generated_lines": 28988, + "mixed_files": 27 + }, + "files": [ + { + "path": "src/openai/resources/beta/threads/runs/runs.py", + "added": 1290, + "removed": 181, + "patch": 1471, + "present": true, + "current_lines": 3161, + "generated_lines": 2052, + "track": "T4" + }, + { + "path": "src/openai/resources/responses/responses.py", + "added": 920, + "removed": 74, + "patch": 994, + "present": true, + "current_lines": 5181, + "generated_lines": 4335, + "track": "T5" + }, + { + "path": "src/openai/resources/audio/transcriptions.py", + "added": 287, + "removed": 601, + "patch": 888, + "present": true, + "current_lines": 1089, + "generated_lines": 1403, + "track": "T3" + }, + { + "path": "src/openai/_client.py", + "added": 759, + "removed": 60, + "patch": 819, + "present": true, + "current_lines": 2440, + "generated_lines": 1741, + "track": "T2" + }, + { + "path": "src/openai/resources/chat/completions/completions.py", + "added": 603, + "removed": 1, + "patch": 604, + "present": true, + "current_lines": 3456, + "generated_lines": 2854, + "track": "T6" + }, + { + "path": "src/openai/resources/beta/threads/threads.py", + "added": 390, + "removed": 7, + "patch": 397, + "present": true, + "current_lines": 2009, + "generated_lines": 1626, + "track": "T4" + }, + { + "path": "src/openai/__init__.py", + "added": 185, + "removed": 1, + "patch": 186, + "present": true, + "current_lines": 505, + "generated_lines": 321, + "track": "T2" + }, + { + "path": "api.md", + "added": 119, + "removed": 8, + "patch": 127, + "present": true, + "current_lines": 2090, + "generated_lines": 1979, + "track": "T16" + }, + { + "path": "src/openai/resources/beta/agents/sessions/sessions.py", + "added": 113, + "removed": 11, + "patch": 124, + "present": true, + "current_lines": 1304, + "generated_lines": 1202, + "track": "T9" + }, + { + "path": "src/openai/resources/audio/translations.py", + "added": 40, + "removed": 75, + "patch": 115, + "present": true, + "current_lines": 374, + "generated_lines": 409, + "track": "T3" + }, + { + "path": "src/openai/resources/beta/responses/responses.py", + "added": 55, + "removed": 55, + "patch": 110, + "present": true, + "current_lines": 5575, + "generated_lines": 5575, + "track": "T5" + }, + { + "path": "src/openai/resources/webhooks/webhooks.py", + "added": 89, + "removed": 4, + "patch": 93, + "present": true, + "current_lines": 1146, + "generated_lines": 1061, + "track": "T12" + }, + { + "path": "src/openai/resources/webhooks/__init__.py", + "added": 24, + "removed": 55, + "patch": 79, + "present": true, + "current_lines": 42, + "generated_lines": 73, + "track": "T12" + }, + { + "path": "src/openai/resources/realtime/translations/translations.py", + "added": 34, + "removed": 18, + "patch": 52, + "present": true, + "current_lines": 1152, + "generated_lines": 1136, + "track": "T11" + }, + { + "path": "src/openai/resources/embeddings.py", + "added": 27, + "removed": 21, + "patch": 48, + "present": true, + "current_lines": 263, + "generated_lines": 257, + "track": "T13" + }, + { + "path": "src/openai/resources/realtime/api.md", + "added": 0, + "removed": 20, + "patch": 20, + "present": true, + "current_lines": 149, + "generated_lines": 169, + "track": "T16" + }, + { + "path": "src/openai/resources/realtime/realtime.py", + "added": 9, + "removed": 3, + "patch": 12, + "present": true, + "current_lines": 1660, + "generated_lines": 1654, + "track": "T11" + }, + { + "path": "src/openai/types/decision_input_image_param.py", + "added": 11, + "removed": 1, + "patch": 12, + "present": true, + "current_lines": 47, + "generated_lines": 37, + "track": "T1" + }, + { + "path": "src/openai/types/decision_input_text_param.py", + "added": 11, + "removed": 1, + "patch": 12, + "present": true, + "current_lines": 33, + "generated_lines": 23, + "track": "T1" + }, + { + "path": "src/openai/types/webhooks/__init__.py", + "added": 3, + "removed": 1, + "patch": 4, + "present": true, + "current_lines": 73, + "generated_lines": 71, + "track": "T1" + }, + { + "path": "src/openai/types/webhooks/unwrap_webhook_event.py", + "added": 2, + "removed": 2, + "patch": 4, + "present": true, + "current_lines": 74, + "generated_lines": 74, + "track": "T1" + }, + { + "path": "src/openai/types/responses/tool.py", + "added": 1, + "removed": 2, + "patch": 3, + "present": true, + "current_lines": 398, + "generated_lines": 399, + "track": "T1" + }, + { + "path": "src/openai/types/responses/tool_param.py", + "added": 3, + "removed": 0, + "patch": 3, + "present": true, + "current_lines": 398, + "generated_lines": 395, + "track": "T1" + }, + { + "path": "src/openai/types/webhooks/live_call_incoming_webhook_event.py", + "added": 2, + "removed": 1, + "patch": 3, + "present": true, + "current_lines": 71, + "generated_lines": 70, + "track": "T1" + }, + { + "path": "src/openai/types/fine_tuning/fine_tuning_job_integration.py", + "added": 0, + "removed": 2, + "patch": 2, + "present": true, + "current_lines": 5, + "generated_lines": 7, + "track": "T1" + }, + { + "path": "src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py", + "added": 1, + "removed": 1, + "patch": 2, + "present": true, + "current_lines": 40, + "generated_lines": 40, + "track": "T1" + }, + { + "path": "src/openai/types/realtime/realtime_error_event.py", + "added": 1, + "removed": 1, + "patch": 2, + "present": true, + "current_lines": 25, + "generated_lines": 25, + "track": "T1" + } + ], + "ownership": { + "generated_paths": 1970, + "generated_source_paths": 1797, + "excluded_source_paths": 205, + "excluded_source_lines": 35731 + }, + "status": "merged; post-merge CI passed", + "pull_request": "https://github.com/openai/openai-python/pull/4155", + "postmerge_ci": "https://github.com/openai/openai-python/actions/runs/39730902045" + }, + { + "audit_date": "2026-10-22", + "sdk_commit": "fa9a1616fd175e68dec9960d33dee2caefc0e8d3", + "public_codegen_commit": "992b9cbba312b4bf10128b9f1d36112b48340363", + "generation_id": "8e0b46e2-9f31-4393-9a7e-2be585d07efb", + "codegen_hash": "3051dd5f14816304442e438ac216fbc29b4e1a9b1f87873da6f3a4a947cd7fa0", + "status": "released public baseline; transition not delivered", + "totals": { + "mixed_files": 41, + "added": 4890, + "removed": 2519, + "patch": 7409, + "current_lines": 39960 + }, + "generated_manifest_entries": 1980, + "files": [ + { + "path": "api.md", + "physical_lines": 2094, + "added": 126, + "removed": 8, + "sha256": "668a9076e479f27645ff1a53816b7d983c2767ad0a2520bbb08ccdf319e55f1a" + }, + { + "path": "scripts/castiron/CUSTOM_CODE.md", + "physical_lines": 217, + "added": 0, + "removed": 40, + "sha256": "6578db0d17cf297ea01b862f772a716bef56a81192498f423463505950707701" + }, + { + "path": "scripts/castiron/custom_code_budget.py", + "physical_lines": 702, + "added": 1, + "removed": 4, + "sha256": "3f576ca4ddf34fdb3c43093192ca382e38ac66a703c1ff000f87fa288c3faa6c" + }, + { + "path": "scripts/castiron/custom_code_policy.py", + "physical_lines": 481, + "added": 1, + "removed": 4, + "sha256": "e9b39f4f881a3adfe1cb6a01bb2bb0522463c98c554b3c21ae8d3b2f13ce23f5" + }, + { + "path": "scripts/castiron/custom_code_report.py", + "physical_lines": 1250, + "added": 19, + "removed": 254, + "sha256": "6b7cfda1f85426daaa2e4f6186e2d1b20ac638dadaa54be8142ecec89fefd880" + }, + { + "path": "scripts/castiron/fixtures/github_publisher.cjs", + "physical_lines": 69, + "added": 10, + "removed": 63, + "sha256": "40e08910601ccac410f22ed9397129b2cc1ff985e96109cda8d05608e58eaa56" + }, + { + "path": "scripts/castiron/test_custom_code_policy.py", + "physical_lines": 587, + "added": 0, + "removed": 4, + "sha256": "01b102c8e1f854ec2c4d2480dbcd55ae38008812fb3196e0309eb6ac8fbbb6d4" + }, + { + "path": "scripts/castiron/test_custom_code_publication.py", + "physical_lines": 873, + "added": 4, + "removed": 600, + "sha256": "23d28a75f62d65c0313081194726d34c85241234de9dbd7f085d2c2b1aa0406d" + }, + { + "path": "scripts/castiron/test_custom_code_report.py", + "physical_lines": 1084, + "added": 0, + "removed": 489, + "sha256": "f8f6b883c545963b4f82d8b078734298294607a9371e70e01fb0c3a37304be61" + }, + { + "path": "src/openai/__init__.py", + "physical_lines": 505, + "added": 185, + "removed": 1, + "sha256": "8f4fdba5193967d09ca79bc4504e79bd8c20ba4bd8f09c2df3817501540a39db" + }, + { + "path": "src/openai/_client.py", + "physical_lines": 2440, + "added": 759, + "removed": 60, + "sha256": "6de3a89ee9cb99104118fddd0b44ba13dbe94b78d088cd025ebae85e4c3b884e" + }, + { + "path": "src/openai/resources/audio/transcriptions.py", + "physical_lines": 1089, + "added": 149, + "removed": 453, + "sha256": "3c8406db40522bf11e8bcdc77bcd06d9a12a7f0f6dda578fa2765e7ee6d42b76" + }, + { + "path": "src/openai/resources/audio/translations.py", + "physical_lines": 374, + "added": 21, + "removed": 46, + "sha256": "f921e5be3dbe5f2dee40e85f02aa40850fab8cd0db401fdb0342180ac096aac0" + }, + { + "path": "src/openai/resources/beta/agents/environments/files.py", + "physical_lines": 604, + "added": 6, + "removed": 6, + "sha256": "5d59d1b50d55a4747fff39c8a93e1a30733a6b9a1da0f0a2728151afbeeab850" + }, + { + "path": "src/openai/resources/beta/agents/sessions/artifacts.py", + "physical_lines": 573, + "added": 2, + "removed": 2, + "sha256": "c3bea665d60be82024758eb3ddee8c317ca9809b3e06004a8b51d04e223206f6" + }, + { + "path": "src/openai/resources/beta/agents/sessions/sessions.py", + "physical_lines": 1304, + "added": 113, + "removed": 11, + "sha256": "3077c41f66c90d54aa9f629500447277759f6caf8feb1202f357be1389dc421a" + }, + { + "path": "src/openai/resources/beta/responses/responses.py", + "physical_lines": 5575, + "added": 55, + "removed": 55, + "sha256": "aca7b656abd01d90bc23030690562738aaa0921bb33de8016e379e6aec34d294" + }, + { + "path": "src/openai/resources/beta/threads/runs/runs.py", + "physical_lines": 3161, + "added": 1290, + "removed": 181, + "sha256": "dd2491939d56955dbc92fa6cdcdd92c4d0ffa780cad8e6167526c00eba149b3b" + }, + { + "path": "src/openai/resources/beta/threads/threads.py", + "physical_lines": 2009, + "added": 390, + "removed": 7, + "sha256": "aed41898a61e4f92be48ac39892f53ef983cc6384e9fff2e8a163751de3f2de5" + }, + { + "path": "src/openai/resources/chat/completions/completions.py", + "physical_lines": 3456, + "added": 603, + "removed": 1, + "sha256": "01f0326e163f2a4a5bdc56301905958ecc16c9e4a2a47499c5ee6588995916f2" + }, + { + "path": "src/openai/resources/conversations/api.md", + "physical_lines": 42, + "added": 1, + "removed": 0, + "sha256": "6a36bb38e1e971829eba8655fe5056053f79d3ee2d75554e7ff3361b0457bf79" + }, + { + "path": "src/openai/resources/embeddings.py", + "physical_lines": 263, + "added": 27, + "removed": 21, + "sha256": "3c8f49bd504319f298b4cc5cc67d537e434814a3b06f352b41ad4a04fddaa262" + }, + { + "path": "src/openai/resources/realtime/api.md", + "physical_lines": 149, + "added": 0, + "removed": 15, + "sha256": "8e3ed6e8e52a5bee45022706a34ac17bf3a6a160b67e2c3ff3232de8fea30de0" + }, + { + "path": "src/openai/resources/realtime/realtime.py", + "physical_lines": 1660, + "added": 9, + "removed": 3, + "sha256": "0285ac5edb9b8b17f296b2d513068cb35c3413895e35424221765cc4a15c8592" + }, + { + "path": "src/openai/resources/realtime/translations/translations.py", + "physical_lines": 1152, + "added": 34, + "removed": 18, + "sha256": "805923247278589582e4d4db869aa55f37a18130178d6c467f1c59b67825259c" + }, + { + "path": "src/openai/resources/responses/api.md", + "physical_lines": 208, + "added": 2, + "removed": 0, + "sha256": "1a12a9f78e43efbfbed1e1d0c13ba023fbe71d540351c527cf9fbeb87a928a0b" + }, + { + "path": "src/openai/resources/responses/responses.py", + "physical_lines": 5181, + "added": 920, + "removed": 74, + "sha256": "0aa6e43a2f059bba750249f50b94306fe2c513c3968b5f70b018c10c9f09123f" + }, + { + "path": "src/openai/resources/uploads/uploads.py", + "physical_lines": 652, + "added": 6, + "removed": 28, + "sha256": "cd5d957c36eb1e5da2bf0174d86817368e990e2657c5960a9cd5b8a9e94d0802" + }, + { + "path": "src/openai/resources/webhooks/__init__.py", + "physical_lines": 42, + "added": 24, + "removed": 55, + "sha256": "426b361f424312c4bf7390560f00566d4ee89da90db3463cc2d74f46ac43ba55" + }, + { + "path": "src/openai/resources/webhooks/webhooks.py", + "physical_lines": 1146, + "added": 89, + "removed": 4, + "sha256": "f730653c5937f4c723f5519c7e86823fcba0ee088c270ecbd8e02fd10e49c654" + }, + { + "path": "src/openai/types/__init__.py", + "physical_lines": 157, + "added": 4, + "removed": 1, + "sha256": "98f15c4ee123816edfbbd0b758a67ca30b80f80939d158e8bed9a1b518d7c6ae" + }, + { + "path": "src/openai/types/decision_input_image_param.py", + "physical_lines": 47, + "added": 11, + "removed": 1, + "sha256": "347c633b1312fb62ecceffc147d0a250954392a09c07fa680d585dd41a3f8c60" + }, + { + "path": "src/openai/types/decision_input_message_param.py", + "physical_lines": 95, + "added": 6, + "removed": 1, + "sha256": "22aff1b989c87ee05e3041c1caa9f3dd8540cbb381d96281ee75237b508297aa" + }, + { + "path": "src/openai/types/decision_input_text_param.py", + "physical_lines": 33, + "added": 11, + "removed": 1, + "sha256": "416864cec231da63e0324268e57192de9902399cf0119d061467c6117a3090c7" + }, + { + "path": "src/openai/types/fine_tuning/fine_tuning_job_integration.py", + "physical_lines": 5, + "added": 0, + "removed": 2, + "sha256": "add6f540e2493ce1c9e4b18c451669acd44443f84ffc364d03ba5299134c3f85" + }, + { + "path": "src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py", + "physical_lines": 40, + "added": 1, + "removed": 1, + "sha256": "e0e955bb2d08d25f9217a2b3f48b2ac0a20328af2ac1765bfb94f616de9c1e7f" + }, + { + "path": "src/openai/types/realtime/realtime_error_event.py", + "physical_lines": 25, + "added": 1, + "removed": 1, + "sha256": "3caef26e78cca8e07685e5c40b8f12f7cc3416f473407eb254b6c7a38b6b52fe" + }, + { + "path": "src/openai/types/responses/tool_param.py", + "physical_lines": 398, + "added": 3, + "removed": 0, + "sha256": "e1b18d7cc6437215da9f1ca8f73f67807c335dc546328f02598a5d84fcc24733" + }, + { + "path": "src/openai/types/webhooks/__init__.py", + "physical_lines": 73, + "added": 3, + "removed": 1, + "sha256": "a5774cf8e193a6d9299039fa823adb236b2dbb5cce675ccc9efbed2a613b1aa2" + }, + { + "path": "src/openai/types/webhooks/live_call_incoming_webhook_event.py", + "physical_lines": 71, + "added": 2, + "removed": 1, + "sha256": "2dd3bc70a9df1f12e5333983946e1df037a69f712ce104c75324768034df5c24" + }, + { + "path": "src/openai/types/webhooks/unwrap_webhook_event.py", + "physical_lines": 74, + "added": 2, + "removed": 2, + "sha256": "64626476d92e18f4727df732943b4479b6c0f9119f443f05a14aec7932a6fd2f" + } + ], + "verification": "Authentic reporter --public --fetch --require-head-hash, base=head=fa9a1616fd175e68dec9960d33dee2caefc0e8d3; generated checkpoint/hash verified." + }, + { + "audit_date": "2026-10-22", + "sdk_commit": "05dfea0a1cb41a3e6ae1cc3bd13145b57cf3ffb8", + "public_codegen_commit": "992b9cbba312b4bf10128b9f1d36112b48340363", + "generation_id": "8e0b46e2-9f31-4393-9a7e-2be585d07efb", + "codegen_hash": "3051dd5f14816304442e438ac216fbc29b4e1a9b1f87873da6f3a4a947cd7fa0", + "status": "released public baseline; transition not delivered", + "totals": { + "mixed_files": 41, + "added": 4890, + "removed": 2519, + "patch": 7409, + "current_lines": 39960 + }, + "generated_manifest_entries": 1980, + "verification": "Authentic --public --fetch --require-head-hash report at base=head=e8a0fd28bbdc1699c97ff0f602b1af3dd378382d; same verified checkpoint/manifest and exact41-file debt ledger as fa9.", + "release_only_advance_from": "fa9a1616fd175e68dec9960d33dee2caefc0e8d3", + "release_preservation": { + "baseline": "fa9a1616fd175e68dec9960d33dee2caefc0e8d3", + "released": "e8a0fd28bbdc1699c97ff0f602b1af3dd378382d", + "files": [ + { + "path": ".release-please-manifest.json", + "baseline_sha256": "c59a5d69e086dce493253558e189ce4e2a51f996d433d6ba6880859a114adfe3", + "released_sha256": "0066daf14f84fa9acba051b9353ada8f2c694d7b9fc4f39e200d05f6d63844ea", + "released_physical_lines": 3, + "released_mode": "100644" + }, + { + "path": "CHANGELOG.md", + "baseline_sha256": "210950dd11b28f0d4d676a70362ccc7449fbe8109eb9669ba7dae4cb21d33fa0", + "released_sha256": "2fd2cb159a9de92ce59c9582d158f4bc66774c361326a8fe74b2cf44496052d6", + "released_physical_lines": 5049, + "released_mode": "100644" + }, + { + "path": "pyproject.toml", + "baseline_sha256": "7a250b315732c9ab23749756426f3e19adc34965013ccdd6f18a09091e1968b1", + "released_sha256": "313aed18e64b72242c791327aec53e894ff501a13261c579f96869422722981d", + "released_physical_lines": 331, + "released_mode": "100644" + }, + { + "path": "src/openai/_version.py", + "baseline_sha256": "4e7f720211e79a933e995a9724719d366ed348807a3fc24b9a23adbaf05ff532", + "released_sha256": "79285c9c3bc4d27b0184b8924f418c3b312ee02c321d748c07a88c0affd9e8c9", + "released_physical_lines": 2, + "released_mode": "100644" + }, + { + "path": "uv.lock", + "baseline_sha256": "7fe76ae9522a3b89b61d12c00bee0cf7ff8ae9bbb33159a1db20fb4e0938a953", + "released_sha256": "b23bae66a4ef28755110f0da323f14bf38294cbcd9f96e7586544a392532bbcf", + "released_physical_lines": 2919, + "released_mode": "100644" + } + ], + "requirement": "Future ordinary rebase must retain these exact released files unless separately reviewed changes supersede them." + } + } + ], + "final_migration_plan": { + "status": "Measured b53 candidate: zero mixed files and zero custom patch lines; public delivery and default activation pending.", + "baseline_sdk_commit": "cfa0a79b6395bb0561b28879122a9f90a87cdada", + "baseline_mixed_files": 41, + "baseline_custom_patch_lines": 7409, + "target_mixed_files": 0, + "target_custom_patch_lines": 0, + "delivery": "Consolidated public SDK PR after final validation and ordinary review/promotion gates", + "lanes": [ + { + "name": "Client and package", + "tracks": [ + "T2" + ], + "files": 2, + "patch_lines": 1005 + }, + { + "name": "Parsing and request hooks", + "tracks": [ + "T3", + "T5", + "T6", + "T13" + ], + "files": 6, + "patch_lines": 2759 + }, + { + "name": "Assistants and Sessions", + "tracks": [ + "T4", + "T9" + ], + "files": 3, + "patch_lines": 1992 + }, + { + "name": "Types, transports and documentation", + "tracks": [ + "T1", + "T11", + "T12", + "T16" + ], + "files": 16, + "patch_lines": 430 + } + ], + "compatibility": "Preserve every released developer-facing declaration and behavior; retain ownership, exclusions, budgets, dependency locks and Python floor", + "activation": "Authored feature-branch inputs may drive final generation after compiler merge; ordinary default activation follows runtime delivery", + "completion_evidence": "Unchanged authoritative reporter with authentic checkpoints and manifest hashes, full relevant tests/type checks, reviews, protected public merge and post-merge CI", + "prepared_handwritten_runtime": { + "measurement": "Physical lines including blanks and comments in prepared handwritten runtime modules; not a mixed-file reduction or executable-LOC count. Normal final generation/reporting remains required.", + "files": [ + { + "path": "src/openai/lib/_assistants_runs.py", + "status": "new handwritten module", + "physical_lines": 768 + }, + { + "path": "src/openai/lib/_assistants_threads.py", + "status": "new handwritten module", + "physical_lines": 287 + }, + { + "path": "src/openai/lib/_chat_completion_resources.py", + "status": "new handwritten module", + "physical_lines": 490 + }, + { + "path": "src/openai/lib/_client_auth.py", + "status": "new handwritten module", + "physical_lines": 976 + }, + { + "path": "src/openai/lib/_module_runtime.py", + "status": "new handwritten module", + "physical_lines": 120 + }, + { + "path": "src/openai/lib/_parsing/_embeddings.py", + "status": "modified handwritten module", + "physical_lines": 60 + }, + { + "path": "src/openai/lib/_request_context.py", + "status": "new handwritten module", + "physical_lines": 13 + }, + { + "path": "src/openai/lib/_response_resources.py", + "status": "new handwritten module", + "physical_lines": 643 + }, + { + "path": "src/openai/lib/beta/agents/_create.py", + "status": "new handwritten module", + "physical_lines": 69 + } + ], + "total_physical_lines": 3426 + }, + "client_package_compiler": { + "delta": "[private review reference omitted]", + "head": "4ae71419874d772688a49f0d6efc8279e1e744db", + "state": "merged; protected validation and post-merge CI passed", + "renderer_tests_passed": 813, + "generated_preview_files_unchanged": 1957, + "library_and_test_clippy": "passed", + "immutable_parity_cases_passed": 2, + "formal_review_findings_addressed": 9, + "merged_sha": "34e4caadc819f51b8267061485739b6481644593" + }, + "original_baseline_sdk_commit": "7db9d884c33b0d5ef252e9a4b6e7ae0bbd217e5b", + "lane_counts_scope": "Historical original 27-file baseline; current 41-file inventory is current_audit.files", + "remaining_sequence": [ + "Apply verified transition documentation, refresh final-head evidence and finish current-head hosted CI and normal required reviews.", + "Complete normal public promotion, recheck released main and use the protected public merge workflow.", + "Verify the public merge and post-merge CI before marking delivered true.", + "After verified public runtime delivery, activate authored defaults with additive composition, retained opt-ins/imports and Beta generator guards." + ], + "candidate_sdk_commit": "b53d11fd9a9b337a6d7a3412f256dff64ea4d684", + "candidate_mixed_files": 0, + "candidate_custom_patch_lines": 0, + "delivered": false + }, + "candidate_audit": { + "sdk_commit": "b53d11fd9a9b337a6d7a3412f256dff64ea4d684", + "baseline_sdk_commit": "cfa0a79b6395bb0561b28879122a9f90a87cdada", + "codegen_commit": "f028b1b011d31cf0cd8a1939e83bf7ec5d2b8762", + "generation_id": "592b2cad-c398-46a9-9df6-7858c09f3b65", + "codegen_hash": "2e485073ea1166bc85b3b8c04a512d221b82ef5c490801250ffc209d6292e201", + "compiler_commit": "84f592da365c50862876c4dd63cef35eff88c45d", + "mixed_files": 0, + "custom_patch_lines": 0, + "patch_additions": 0, + "patch_deletions": 0, + "former_mixed_files_now_matching": 41, + "generated_manifest_entries_verified": 1980, + "generated_manifest_entries_matching": 1980, + "delivered": false, + "status": "Measured b53 candidate: zero mixed files and zero custom patch lines; public delivery and default activation pending.", + "files": [ + { + "path": "api.md", + "baseline_physical_lines": 2094, + "baseline_added": 126, + "baseline_removed": 8, + "baseline_patch": 134, + "candidate_physical_lines": 2000, + "checkpoint_physical_lines": 2000, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "f58148943cc440a8f82d8b78a88ce875a4826cf9ce68c9c8894af806337cb66e", + "checkpoint_sha256": "f58148943cc440a8f82d8b78a88ce875a4826cf9ce68c9c8894af806337cb66e", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "scripts/castiron/CUSTOM_CODE.md", + "baseline_physical_lines": 217, + "baseline_added": 0, + "baseline_removed": 40, + "baseline_patch": 40, + "candidate_physical_lines": 279, + "checkpoint_physical_lines": 279, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "c80c8e834f55f3cfbf690f6bffb70d7b0a10317261a407b46686d810e07f6a61", + "checkpoint_sha256": "c80c8e834f55f3cfbf690f6bffb70d7b0a10317261a407b46686d810e07f6a61", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "scripts/castiron/custom_code_budget.py", + "baseline_physical_lines": 702, + "baseline_added": 1, + "baseline_removed": 4, + "baseline_patch": 5, + "candidate_physical_lines": 769, + "checkpoint_physical_lines": 769, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "3c96300546afb5e57fbfb596d4e1bde90821928e44904c4aeb91e2d7e4e27ff5", + "checkpoint_sha256": "3c96300546afb5e57fbfb596d4e1bde90821928e44904c4aeb91e2d7e4e27ff5", + "candidate_mode": "100755", + "checkpoint_mode": "100755", + "bytes_equal": true + }, + { + "path": "scripts/castiron/custom_code_policy.py", + "baseline_physical_lines": 481, + "baseline_added": 1, + "baseline_removed": 4, + "baseline_patch": 5, + "candidate_physical_lines": 530, + "checkpoint_physical_lines": 530, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "f2d21cf2f66b1907ea177930270b6f161951b67af59bc34724f62743d6561a67", + "checkpoint_sha256": "f2d21cf2f66b1907ea177930270b6f161951b67af59bc34724f62743d6561a67", + "candidate_mode": "100755", + "checkpoint_mode": "100755", + "bytes_equal": true + }, + { + "path": "scripts/castiron/custom_code_report.py", + "baseline_physical_lines": 1250, + "baseline_added": 19, + "baseline_removed": 254, + "baseline_patch": 273, + "candidate_physical_lines": 1485, + "checkpoint_physical_lines": 1485, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "a52a1aabf4047f4c20e96d1d788c25a4aa78c1987b4fa73a13d22c92ef213a3f", + "checkpoint_sha256": "a52a1aabf4047f4c20e96d1d788c25a4aa78c1987b4fa73a13d22c92ef213a3f", + "candidate_mode": "100755", + "checkpoint_mode": "100755", + "bytes_equal": true + }, + { + "path": "scripts/castiron/fixtures/github_publisher.cjs", + "baseline_physical_lines": 69, + "baseline_added": 10, + "baseline_removed": 63, + "baseline_patch": 73, + "candidate_physical_lines": 122, + "checkpoint_physical_lines": 122, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "062eb3da63eb0a25cf8de21e9aa0025afb1322eaf4ae59d5d6b3515b3dabda4b", + "checkpoint_sha256": "062eb3da63eb0a25cf8de21e9aa0025afb1322eaf4ae59d5d6b3515b3dabda4b", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "scripts/castiron/test_custom_code_policy.py", + "baseline_physical_lines": 587, + "baseline_added": 0, + "baseline_removed": 4, + "baseline_patch": 4, + "candidate_physical_lines": 720, + "checkpoint_physical_lines": 720, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "eb49e1d2f55ea369010c1ea1e074d822de687abf439bdc8b4c2c29e7ce38f4db", + "checkpoint_sha256": "eb49e1d2f55ea369010c1ea1e074d822de687abf439bdc8b4c2c29e7ce38f4db", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "scripts/castiron/test_custom_code_publication.py", + "baseline_physical_lines": 873, + "baseline_added": 4, + "baseline_removed": 600, + "baseline_patch": 604, + "candidate_physical_lines": 1469, + "checkpoint_physical_lines": 1469, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "38d723f458225eb2d4745aca4044f6e4283f10b2a9d3a5257349b04ecd1c8c75", + "checkpoint_sha256": "38d723f458225eb2d4745aca4044f6e4283f10b2a9d3a5257349b04ecd1c8c75", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "scripts/castiron/test_custom_code_report.py", + "baseline_physical_lines": 1084, + "baseline_added": 0, + "baseline_removed": 489, + "baseline_patch": 489, + "candidate_physical_lines": 1573, + "checkpoint_physical_lines": 1573, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "f4d90fd38b17aac59096488391b65ef9da5493d2049d4c038d5926a960257ca7", + "checkpoint_sha256": "f4d90fd38b17aac59096488391b65ef9da5493d2049d4c038d5926a960257ca7", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/__init__.py", + "baseline_physical_lines": 505, + "baseline_added": 185, + "baseline_removed": 1, + "baseline_patch": 186, + "candidate_physical_lines": 470, + "checkpoint_physical_lines": 470, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "a410d257a94f2c4f6cd0951a178131e8d883e21a69ef6bea245271db215ba196", + "checkpoint_sha256": "a410d257a94f2c4f6cd0951a178131e8d883e21a69ef6bea245271db215ba196", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/_client.py", + "baseline_physical_lines": 2440, + "baseline_added": 759, + "baseline_removed": 60, + "baseline_patch": 819, + "candidate_physical_lines": 1912, + "checkpoint_physical_lines": 1912, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "616e33f35af68a15cd312d9dc68414f41080a7c6275aa61f56cc1c34dad4fa29", + "checkpoint_sha256": "616e33f35af68a15cd312d9dc68414f41080a7c6275aa61f56cc1c34dad4fa29", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/audio/transcriptions.py", + "baseline_physical_lines": 1089, + "baseline_added": 149, + "baseline_removed": 453, + "baseline_patch": 602, + "candidate_physical_lines": 1061, + "checkpoint_physical_lines": 1061, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "8bfaa74f7b00a61984e2ee801357dbf29fdda7f7388d245df7458f0c20820ea9", + "checkpoint_sha256": "8bfaa74f7b00a61984e2ee801357dbf29fdda7f7388d245df7458f0c20820ea9", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/audio/translations.py", + "baseline_physical_lines": 374, + "baseline_added": 21, + "baseline_removed": 46, + "baseline_patch": 67, + "candidate_physical_lines": 360, + "checkpoint_physical_lines": 360, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "e9e2fc5ac27461366225c8e6ca352a8db67b789c816bb0074b21c078e99afc05", + "checkpoint_sha256": "e9e2fc5ac27461366225c8e6ca352a8db67b789c816bb0074b21c078e99afc05", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/beta/agents/environments/files.py", + "baseline_physical_lines": 604, + "baseline_added": 6, + "baseline_removed": 6, + "baseline_patch": 12, + "candidate_physical_lines": 604, + "checkpoint_physical_lines": 604, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "80794856126bd21246130cd64f3935f5a07b0e2d5fa5ae3a2de1afbf80d8eee0", + "checkpoint_sha256": "80794856126bd21246130cd64f3935f5a07b0e2d5fa5ae3a2de1afbf80d8eee0", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/beta/agents/sessions/artifacts.py", + "baseline_physical_lines": 573, + "baseline_added": 2, + "baseline_removed": 2, + "baseline_patch": 4, + "candidate_physical_lines": 573, + "checkpoint_physical_lines": 573, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "d551e527bbfd700bb85608226ffd942117bc2bf8a0c33e9a8456a08fdc6470ed", + "checkpoint_sha256": "d551e527bbfd700bb85608226ffd942117bc2bf8a0c33e9a8456a08fdc6470ed", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/beta/agents/sessions/sessions.py", + "baseline_physical_lines": 1304, + "baseline_added": 113, + "baseline_removed": 11, + "baseline_patch": 124, + "candidate_physical_lines": 1271, + "checkpoint_physical_lines": 1271, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "53987b64fb0b775e755a727f91d4486228dc2735947a3935e0073ca132c5a612", + "checkpoint_sha256": "53987b64fb0b775e755a727f91d4486228dc2735947a3935e0073ca132c5a612", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/beta/responses/responses.py", + "baseline_physical_lines": 5575, + "baseline_added": 55, + "baseline_removed": 55, + "baseline_patch": 110, + "candidate_physical_lines": 5575, + "checkpoint_physical_lines": 5575, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "aca7b656abd01d90bc23030690562738aaa0921bb33de8016e379e6aec34d294", + "checkpoint_sha256": "aca7b656abd01d90bc23030690562738aaa0921bb33de8016e379e6aec34d294", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/beta/threads/runs/runs.py", + "baseline_physical_lines": 3161, + "baseline_added": 1290, + "baseline_removed": 181, + "baseline_patch": 1471, + "candidate_physical_lines": 2889, + "checkpoint_physical_lines": 2889, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "a430fe262b62d81ac6e1d8161e2e4d23a16c01c4bcdcc98a900392db5327d97c", + "checkpoint_sha256": "a430fe262b62d81ac6e1d8161e2e4d23a16c01c4bcdcc98a900392db5327d97c", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/beta/threads/threads.py", + "baseline_physical_lines": 2009, + "baseline_added": 390, + "baseline_removed": 7, + "baseline_patch": 397, + "candidate_physical_lines": 1951, + "checkpoint_physical_lines": 1951, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "1a49dcdeb83ce84b16160b2e17cc4cbfee76c8f93267fe22a727bc420dd92c3c", + "checkpoint_sha256": "1a49dcdeb83ce84b16160b2e17cc4cbfee76c8f93267fe22a727bc420dd92c3c", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/chat/completions/completions.py", + "baseline_physical_lines": 3456, + "baseline_added": 603, + "baseline_removed": 1, + "baseline_patch": 604, + "candidate_physical_lines": 3363, + "checkpoint_physical_lines": 3363, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "0a0d3dfc38a12dff71afba526d745792f33db3e720c04ba755bb966c726f8a16", + "checkpoint_sha256": "0a0d3dfc38a12dff71afba526d745792f33db3e720c04ba755bb966c726f8a16", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/conversations/api.md", + "baseline_physical_lines": 42, + "baseline_added": 1, + "baseline_removed": 0, + "baseline_patch": 1, + "candidate_physical_lines": 41, + "checkpoint_physical_lines": 41, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "97ed38af759c8c47e8df79f2335a0cd9d2871cfaaea7423d4ae4eaea5a63a1b1", + "checkpoint_sha256": "97ed38af759c8c47e8df79f2335a0cd9d2871cfaaea7423d4ae4eaea5a63a1b1", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/embeddings.py", + "baseline_physical_lines": 263, + "baseline_added": 27, + "baseline_removed": 21, + "baseline_patch": 48, + "candidate_physical_lines": 264, + "checkpoint_physical_lines": 264, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "88aed80308e7ca8280fa29ebbf093a533f4dc327a902ace6467c5b8f52e4b387", + "checkpoint_sha256": "88aed80308e7ca8280fa29ebbf093a533f4dc327a902ace6467c5b8f52e4b387", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/realtime/api.md", + "baseline_physical_lines": 149, + "baseline_added": 0, + "baseline_removed": 15, + "baseline_patch": 15, + "candidate_physical_lines": 164, + "checkpoint_physical_lines": 164, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "f977c3b6d8ac7161eeb7025492ac820fc4a17465a54dbec223359b8d72ec6346", + "checkpoint_sha256": "f977c3b6d8ac7161eeb7025492ac820fc4a17465a54dbec223359b8d72ec6346", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/realtime/realtime.py", + "baseline_physical_lines": 1660, + "baseline_added": 9, + "baseline_removed": 3, + "baseline_patch": 12, + "candidate_physical_lines": 1660, + "checkpoint_physical_lines": 1660, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "0285ac5edb9b8b17f296b2d513068cb35c3413895e35424221765cc4a15c8592", + "checkpoint_sha256": "0285ac5edb9b8b17f296b2d513068cb35c3413895e35424221765cc4a15c8592", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/realtime/translations/translations.py", + "baseline_physical_lines": 1152, + "baseline_added": 34, + "baseline_removed": 18, + "baseline_patch": 52, + "candidate_physical_lines": 1152, + "checkpoint_physical_lines": 1152, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "805923247278589582e4d4db869aa55f37a18130178d6c467f1c59b67825259c", + "checkpoint_sha256": "805923247278589582e4d4db869aa55f37a18130178d6c467f1c59b67825259c", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/responses/api.md", + "baseline_physical_lines": 208, + "baseline_added": 2, + "baseline_removed": 0, + "baseline_patch": 2, + "candidate_physical_lines": 208, + "checkpoint_physical_lines": 208, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "840e8dc7c7440209ec84924cc82fa028934956e63dbbea5059885d90812da2a3", + "checkpoint_sha256": "840e8dc7c7440209ec84924cc82fa028934956e63dbbea5059885d90812da2a3", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/responses/responses.py", + "baseline_physical_lines": 5181, + "baseline_added": 920, + "baseline_removed": 74, + "baseline_patch": 994, + "candidate_physical_lines": 4866, + "checkpoint_physical_lines": 4866, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "0fd253db414975a83148d3ff07b4e5c9c5329a1a6c33c4f2ca99294ef286d7ec", + "checkpoint_sha256": "0fd253db414975a83148d3ff07b4e5c9c5329a1a6c33c4f2ca99294ef286d7ec", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/uploads/uploads.py", + "baseline_physical_lines": 652, + "baseline_added": 6, + "baseline_removed": 28, + "baseline_patch": 34, + "candidate_physical_lines": 674, + "checkpoint_physical_lines": 674, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "83658b3e4ff7d6c273673b46bc7b34a9302b4b138a89c24ab63da07d6e44a8dd", + "checkpoint_sha256": "83658b3e4ff7d6c273673b46bc7b34a9302b4b138a89c24ab63da07d6e44a8dd", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/webhooks/__init__.py", + "baseline_physical_lines": 42, + "baseline_added": 24, + "baseline_removed": 55, + "baseline_patch": 79, + "candidate_physical_lines": 43, + "checkpoint_physical_lines": 43, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "c1e56f4b97470de30bd95fc65ab4bc5f6c3f2ea37224c3ce4e6dd6db9e2bbb3c", + "checkpoint_sha256": "c1e56f4b97470de30bd95fc65ab4bc5f6c3f2ea37224c3ce4e6dd6db9e2bbb3c", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/resources/webhooks/webhooks.py", + "baseline_physical_lines": 1146, + "baseline_added": 89, + "baseline_removed": 4, + "baseline_patch": 93, + "candidate_physical_lines": 1146, + "checkpoint_physical_lines": 1146, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "f730653c5937f4c723f5519c7e86823fcba0ee088c270ecbd8e02fd10e49c654", + "checkpoint_sha256": "f730653c5937f4c723f5519c7e86823fcba0ee088c270ecbd8e02fd10e49c654", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/types/__init__.py", + "baseline_physical_lines": 157, + "baseline_added": 4, + "baseline_removed": 1, + "baseline_patch": 5, + "candidate_physical_lines": 157, + "checkpoint_physical_lines": 157, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "98f15c4ee123816edfbbd0b758a67ca30b80f80939d158e8bed9a1b518d7c6ae", + "checkpoint_sha256": "98f15c4ee123816edfbbd0b758a67ca30b80f80939d158e8bed9a1b518d7c6ae", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/types/decision_input_image_param.py", + "baseline_physical_lines": 47, + "baseline_added": 11, + "baseline_removed": 1, + "baseline_patch": 12, + "candidate_physical_lines": 47, + "checkpoint_physical_lines": 47, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "347c633b1312fb62ecceffc147d0a250954392a09c07fa680d585dd41a3f8c60", + "checkpoint_sha256": "347c633b1312fb62ecceffc147d0a250954392a09c07fa680d585dd41a3f8c60", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/types/decision_input_message_param.py", + "baseline_physical_lines": 95, + "baseline_added": 6, + "baseline_removed": 1, + "baseline_patch": 7, + "candidate_physical_lines": 95, + "checkpoint_physical_lines": 95, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "22aff1b989c87ee05e3041c1caa9f3dd8540cbb381d96281ee75237b508297aa", + "checkpoint_sha256": "22aff1b989c87ee05e3041c1caa9f3dd8540cbb381d96281ee75237b508297aa", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/types/decision_input_text_param.py", + "baseline_physical_lines": 33, + "baseline_added": 11, + "baseline_removed": 1, + "baseline_patch": 12, + "candidate_physical_lines": 33, + "checkpoint_physical_lines": 33, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "416864cec231da63e0324268e57192de9902399cf0119d061467c6117a3090c7", + "checkpoint_sha256": "416864cec231da63e0324268e57192de9902399cf0119d061467c6117a3090c7", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/types/fine_tuning/fine_tuning_job_integration.py", + "baseline_physical_lines": 5, + "baseline_added": 0, + "baseline_removed": 2, + "baseline_patch": 2, + "candidate_physical_lines": 5, + "checkpoint_physical_lines": 5, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "add6f540e2493ce1c9e4b18c451669acd44443f84ffc364d03ba5299134c3f85", + "checkpoint_sha256": "add6f540e2493ce1c9e4b18c451669acd44443f84ffc364d03ba5299134c3f85", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py", + "baseline_physical_lines": 40, + "baseline_added": 1, + "baseline_removed": 1, + "baseline_patch": 2, + "candidate_physical_lines": 40, + "checkpoint_physical_lines": 40, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "cc0ebd7e315fd30a64e9df0ff1e8940fc06d2d3d0fdf887dba749759d4d418f9", + "checkpoint_sha256": "cc0ebd7e315fd30a64e9df0ff1e8940fc06d2d3d0fdf887dba749759d4d418f9", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/types/realtime/realtime_error_event.py", + "baseline_physical_lines": 25, + "baseline_added": 1, + "baseline_removed": 1, + "baseline_patch": 2, + "candidate_physical_lines": 25, + "checkpoint_physical_lines": 25, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "7bfb5d08c6e790a4edb5b85a4cb765f34faa7e43e4992ba6a7691fe579749f3c", + "checkpoint_sha256": "7bfb5d08c6e790a4edb5b85a4cb765f34faa7e43e4992ba6a7691fe579749f3c", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/types/responses/tool_param.py", + "baseline_physical_lines": 398, + "baseline_added": 3, + "baseline_removed": 0, + "baseline_patch": 3, + "candidate_physical_lines": 399, + "checkpoint_physical_lines": 399, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "3c9a6d95a41c4870e190679eb6d1c9b7a16440bb32001a3954cbae6ab6e541f5", + "checkpoint_sha256": "3c9a6d95a41c4870e190679eb6d1c9b7a16440bb32001a3954cbae6ab6e541f5", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/types/webhooks/__init__.py", + "baseline_physical_lines": 73, + "baseline_added": 3, + "baseline_removed": 1, + "baseline_patch": 4, + "candidate_physical_lines": 73, + "checkpoint_physical_lines": 73, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "a5774cf8e193a6d9299039fa823adb236b2dbb5cce675ccc9efbed2a613b1aa2", + "checkpoint_sha256": "a5774cf8e193a6d9299039fa823adb236b2dbb5cce675ccc9efbed2a613b1aa2", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/types/webhooks/live_call_incoming_webhook_event.py", + "baseline_physical_lines": 71, + "baseline_added": 2, + "baseline_removed": 1, + "baseline_patch": 3, + "candidate_physical_lines": 71, + "checkpoint_physical_lines": 71, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "2dd3bc70a9df1f12e5333983946e1df037a69f712ce104c75324768034df5c24", + "checkpoint_sha256": "2dd3bc70a9df1f12e5333983946e1df037a69f712ce104c75324768034df5c24", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + }, + { + "path": "src/openai/types/webhooks/unwrap_webhook_event.py", + "baseline_physical_lines": 74, + "baseline_added": 2, + "baseline_removed": 2, + "baseline_patch": 4, + "candidate_physical_lines": 74, + "checkpoint_physical_lines": 74, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "f1285c95b535b806614b4da2b35607fb1c8759dd23520d3b79c2506ef1e69a70", + "checkpoint_sha256": "f1285c95b535b806614b4da2b35607fb1c8759dd23520d3b79c2506ef1e69a70", + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "bytes_equal": true + } + ], + "physical_lines_in_former_mixed_files": 40213, + "manifest_verification": { + "candidate": "b53d11fd9a9b337a6d7a3412f256dff64ea4d684", + "generated_checkpoint": "f028b1b011d31cf0cd8a1939e83bf7ec5d2b8762", + "entries_verified": 1980, + "matching_entries": 1980, + "residual_mixed_files": [], + "all_generated_hashes_and_modes_authentic": true + }, + "public_preservation": { + "baseline": "cfa0a79b6395bb0561b28879122a9f90a87cdada", + "candidate": "b53d11fd9a9b337a6d7a3412f256dff64ea4d684", + "files": [ + { + "path": "src/openai/_base_client.py", + "sha256": "bb9862b955e03e11f3a1c0ca95095806b49a7c0b3fec7108e9473122326cd11b", + "mode": "100644", + "physical_lines": 2399, + "bytes_and_mode_equal": true + }, + { + "path": "src/openai/lib/beta/agents/_result.py", + "sha256": "5c7a3c1400a89ebe1fe2d1c701b067b03e40a7ab4a6d9e29537256f500327a3e", + "mode": "100644", + "physical_lines": 262, + "bytes_and_mode_equal": true + }, + { + "path": "tests/lib/streaming/agents/test_results.py", + "sha256": "8955daa9a62f67bc3e700ebde410dc76a56a0336d72a8b8439806e5cefdbc588", + "mode": "100644", + "physical_lines": 560, + "bytes_and_mode_equal": true + }, + { + "path": "tests/test_status_error_messages.py", + "sha256": "5a6065dacc51ae8e6eff58943d17d78d35a3b27ad88632b8a85f7f87ef5e1986", + "mode": "100644", + "physical_lines": 56, + "bytes_and_mode_equal": true + }, + { + "path": "src/openai/_models.py", + "sha256": "7e09adbab7d24de1c41a69e35bd80901bbfe9cfc3d29493571c8d24daceaf56c", + "mode": "100644", + "physical_lines": 1035, + "bytes_and_mode_equal": true + }, + { + "path": "tests/test_union_parsing.py", + "sha256": "f1b06fb97023a96843ae8e2edacf37251b7718a885273802edc7acef8d9edec8", + "mode": "100644", + "physical_lines": 64, + "bytes_and_mode_equal": true + }, + { + "path": ".release-please-manifest.json", + "sha256": "0066daf14f84fa9acba051b9353ada8f2c694d7b9fc4f39e200d05f6d63844ea", + "mode": "100644", + "physical_lines": 3, + "bytes_and_mode_equal": true + }, + { + "path": "CHANGELOG.md", + "sha256": "2fd2cb159a9de92ce59c9582d158f4bc66774c361326a8fe74b2cf44496052d6", + "mode": "100644", + "physical_lines": 5049, + "bytes_and_mode_equal": true + }, + { + "path": "pyproject.toml", + "sha256": "313aed18e64b72242c791327aec53e894ff501a13261c579f96869422722981d", + "mode": "100644", + "physical_lines": 331, + "bytes_and_mode_equal": true + }, + { + "path": "src/openai/_version.py", + "sha256": "79285c9c3bc4d27b0184b8924f418c3b312ee02c321d748c07a88c0affd9e8c9", + "mode": "100644", + "physical_lines": 2, + "bytes_and_mode_equal": true + }, + { + "path": "uv.lock", + "sha256": "b23bae66a4ef28755110f0da323f14bf38294cbcd9f96e7586544a392532bbcf", + "mode": "100644", + "physical_lines": 2919, + "bytes_and_mode_equal": true + }, + { + "path": "src/openai/types/__init__.py", + "sha256": "98f15c4ee123816edfbbd0b758a67ca30b80f80939d158e8bed9a1b518d7c6ae", + "mode": "100644", + "physical_lines": 157, + "bytes_and_mode_equal": true + }, + { + "path": "src/openai/types/decision_input_message_param.py", + "sha256": "22aff1b989c87ee05e3041c1caa9f3dd8540cbb381d96281ee75237b508297aa", + "mode": "100644", + "physical_lines": 95, + "bytes_and_mode_equal": true + } + ] + }, + "restored_helper_documentation": [ + "videos.create_and_poll", + "videos.poll", + "beta.threads.create_and_run_poll", + "beta.threads.create_and_run_stream", + "beta.threads.runs.create_and_poll", + "beta.threads.runs.create_and_stream", + "beta.threads.runs.poll", + "beta.threads.runs.stream", + "beta.threads.runs.submit_tool_outputs_and_poll", + "beta.threads.runs.submit_tool_outputs_stream" + ], + "validation": { + "full_sdk_lint": "passed: Ruff, Pyright, Mypy (2064 files), import checks", + "runtime": { + "passed": 337, + "skipped": 2, + "skip_reason": "Expected API-shape cases: creation streams do not expose until_done; follow-up drain and creation getter/iteration cases are exercised", + "skipped_cases": [ + "tests/lib/streaming/agents/test_results.py::test_result_selects_ordered_final_messages[sync-creation-drain]", + "tests/lib/streaming/agents/test_results.py::test_result_selects_ordered_final_messages[async-creation-drain]" + ] + }, + "reporting_tests": { + "passed": 220, + "existing_skip": 1, + "scope": "Merged compiler reporting templates; existing compiler-contract skip retained" + }, + "compiler_tests": { + "transform_passed": 190, + "renderer_passed": 823, + "raw_python_files_byte_identical_to_reviewed_output": 1966 + }, + "hosted_checks": "pending", + "public_delivery": "pending", + "default_activation": "pending" + }, + "report_counts": { + "after": 0, + "baseline_changed": 31, + "before": 41, + "removed": 10 + }, + "report_sha256": "2ba7a7d830aa514b27b8465152aeb691786614be8247c22b37312b20e31d97b8", + "python_policy_sha256": "b95498283ae38ecdfc3bc605670a5603fa204b1d710f4821bde792c8997d345f", + "python_policy_unchanged": true + }, + "candidate_audit_history": [ + { + "sdk_commit": "e320019182d287cb7470500ab6a69bb95bd5ce54", + "codegen_commit": "9293fea304d8c31dc1b24e3d9970db82752819ff", + "generation_id": "489447d0-e0d6-455f-a82b-f7c6d8c41bbd", + "compiler_commit": "babbc1ed17987d1c6017b20236058550a2e37cec", + "mixed_files": 0, + "custom_patch_lines": 0, + "removed_customizations": 27, + "generated_manifest_entries_verified": 1972, + "delivered": false, + "status": "measured private candidate: full SDK lint, focused reporting tests and independent ownership audit passed; public delivery and default activation tracked separately", + "files": [ + { + "path": "src/openai/resources/beta/threads/runs/runs.py", + "track": "T4", + "before": { + "path": "src/openai/resources/beta/threads/runs/runs.py", + "added": 1290, + "removed": 181, + "patch": 1471, + "present": true, + "current_lines": 3161, + "generated_lines": 2052, + "track": "T4" + }, + "candidate_physical_lines": 2889, + "checkpoint_physical_lines": 2889, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "93deafc3f96f62df958e5fce8836c9498e1d907c", + "sha256": "a430fe262b62d81ac6e1d8161e2e4d23a16c01c4bcdcc98a900392db5327d97c", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/responses/responses.py", + "track": "T5", + "before": { + "path": "src/openai/resources/responses/responses.py", + "added": 920, + "removed": 74, + "patch": 994, + "present": true, + "current_lines": 5181, + "generated_lines": 4335, + "track": "T5" + }, + "candidate_physical_lines": 4866, + "checkpoint_physical_lines": 4866, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "ca4fdc10028ce2319257c571b7cf33521a55d40f", + "sha256": "0fd253db414975a83148d3ff07b4e5c9c5329a1a6c33c4f2ca99294ef286d7ec", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/audio/transcriptions.py", + "track": "T3", + "before": { + "path": "src/openai/resources/audio/transcriptions.py", + "added": 287, + "removed": 601, + "patch": 888, + "present": true, + "current_lines": 1089, + "generated_lines": 1403, + "track": "T3" + }, + "candidate_physical_lines": 1061, + "checkpoint_physical_lines": 1061, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "584545eca564f26fa7b508d93def5f98d9b23f9d", + "sha256": "8bfaa74f7b00a61984e2ee801357dbf29fdda7f7388d245df7458f0c20820ea9", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/_client.py", + "track": "T2", + "before": { + "path": "src/openai/_client.py", + "added": 759, + "removed": 60, + "patch": 819, + "present": true, + "current_lines": 2440, + "generated_lines": 1741, + "track": "T2" + }, + "candidate_physical_lines": 1912, + "checkpoint_physical_lines": 1912, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "ae17365e6d4585559f66ad6124be34f76f0a60d6", + "sha256": "616e33f35af68a15cd312d9dc68414f41080a7c6275aa61f56cc1c34dad4fa29", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/chat/completions/completions.py", + "track": "T6", + "before": { + "path": "src/openai/resources/chat/completions/completions.py", + "added": 603, + "removed": 1, + "patch": 604, + "present": true, + "current_lines": 3456, + "generated_lines": 2854, + "track": "T6" + }, + "candidate_physical_lines": 3363, + "checkpoint_physical_lines": 3363, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "1b987cf3dfac8da6a152c76cb7186026a949e6d0", + "sha256": "0a0d3dfc38a12dff71afba526d745792f33db3e720c04ba755bb966c726f8a16", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/beta/threads/threads.py", + "track": "T4", + "before": { + "path": "src/openai/resources/beta/threads/threads.py", + "added": 390, + "removed": 7, + "patch": 397, + "present": true, + "current_lines": 2009, + "generated_lines": 1626, + "track": "T4" + }, + "candidate_physical_lines": 1951, + "checkpoint_physical_lines": 1951, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "d2c1ca7fd8e73381de0f075c170295b8127e5b7d", + "sha256": "1a49dcdeb83ce84b16160b2e17cc4cbfee76c8f93267fe22a727bc420dd92c3c", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/__init__.py", + "track": "T2", + "before": { + "path": "src/openai/__init__.py", + "added": 185, + "removed": 1, + "patch": 186, + "present": true, + "current_lines": 505, + "generated_lines": 321, + "track": "T2" + }, + "candidate_physical_lines": 470, + "checkpoint_physical_lines": 470, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "74f675aca6773e81b6ea2b075b266f5b46b82503", + "sha256": "a410d257a94f2c4f6cd0951a178131e8d883e21a69ef6bea245271db215ba196", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "api.md", + "track": "T16", + "before": { + "path": "api.md", + "added": 119, + "removed": 8, + "patch": 127, + "present": true, + "current_lines": 2090, + "generated_lines": 1979, + "track": "T16" + }, + "candidate_physical_lines": 1978, + "checkpoint_physical_lines": 1978, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "d2c27e44d5a5f2d2d6ec36a4123d98c899591d6f", + "sha256": "ba13780c85fce61ebce3514abfe5ce488870cc28ade379a2b4cce4a0390ae92f", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/beta/agents/sessions/sessions.py", + "track": "T9", + "before": { + "path": "src/openai/resources/beta/agents/sessions/sessions.py", + "added": 113, + "removed": 11, + "patch": 124, + "present": true, + "current_lines": 1304, + "generated_lines": 1202, + "track": "T9" + }, + "candidate_physical_lines": 1271, + "checkpoint_physical_lines": 1271, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "6a9658a501c0ac97f5aaf5aef6f760f539df8e00", + "sha256": "53987b64fb0b775e755a727f91d4486228dc2735947a3935e0073ca132c5a612", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/audio/translations.py", + "track": "T3", + "before": { + "path": "src/openai/resources/audio/translations.py", + "added": 40, + "removed": 75, + "patch": 115, + "present": true, + "current_lines": 374, + "generated_lines": 409, + "track": "T3" + }, + "candidate_physical_lines": 360, + "checkpoint_physical_lines": 360, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "f05c9bccd9fa871f580e0e3fcfec3d3a09600e6a", + "sha256": "e9e2fc5ac27461366225c8e6ca352a8db67b789c816bb0074b21c078e99afc05", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/beta/responses/responses.py", + "track": "T5", + "before": { + "path": "src/openai/resources/beta/responses/responses.py", + "added": 55, + "removed": 55, + "patch": 110, + "present": true, + "current_lines": 5575, + "generated_lines": 5575, + "track": "T5" + }, + "candidate_physical_lines": 5575, + "checkpoint_physical_lines": 5575, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "c4befe5e5eb8c6fba1bd296a41f4eadb5c3804ce", + "sha256": "aca7b656abd01d90bc23030690562738aaa0921bb33de8016e379e6aec34d294", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/webhooks/webhooks.py", + "track": "T12", + "before": { + "path": "src/openai/resources/webhooks/webhooks.py", + "added": 89, + "removed": 4, + "patch": 93, + "present": true, + "current_lines": 1146, + "generated_lines": 1061, + "track": "T12" + }, + "candidate_physical_lines": 1146, + "checkpoint_physical_lines": 1146, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "7de8f39ff86442f6c405ce6170c17bc8f67e9d7a", + "sha256": "f730653c5937f4c723f5519c7e86823fcba0ee088c270ecbd8e02fd10e49c654", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/webhooks/__init__.py", + "track": "T12", + "before": { + "path": "src/openai/resources/webhooks/__init__.py", + "added": 24, + "removed": 55, + "patch": 79, + "present": true, + "current_lines": 42, + "generated_lines": 73, + "track": "T12" + }, + "candidate_physical_lines": 43, + "checkpoint_physical_lines": 43, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "a4f5fbdcf1dc3b493883029ba3ecd8174b36c058", + "sha256": "c1e56f4b97470de30bd95fc65ab4bc5f6c3f2ea37224c3ce4e6dd6db9e2bbb3c", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/realtime/translations/translations.py", + "track": "T11", + "before": { + "path": "src/openai/resources/realtime/translations/translations.py", + "added": 34, + "removed": 18, + "patch": 52, + "present": true, + "current_lines": 1152, + "generated_lines": 1136, + "track": "T11" + }, + "candidate_physical_lines": 1152, + "checkpoint_physical_lines": 1152, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "7d2ce9c9f7d1de42b3b6af015b384eb584181400", + "sha256": "805923247278589582e4d4db869aa55f37a18130178d6c467f1c59b67825259c", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/embeddings.py", + "track": "T13", + "before": { + "path": "src/openai/resources/embeddings.py", + "added": 27, + "removed": 21, + "patch": 48, + "present": true, + "current_lines": 263, + "generated_lines": 257, + "track": "T13" + }, + "candidate_physical_lines": 264, + "checkpoint_physical_lines": 264, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "8a316896719cc34b84dc2e70e444b02747d1036e", + "sha256": "88aed80308e7ca8280fa29ebbf093a533f4dc327a902ace6467c5b8f52e4b387", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/realtime/api.md", + "track": "T16", + "before": { + "path": "src/openai/resources/realtime/api.md", + "added": 0, + "removed": 20, + "patch": 20, + "present": true, + "current_lines": 149, + "generated_lines": 169, + "track": "T16" + }, + "candidate_physical_lines": 164, + "checkpoint_physical_lines": 164, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "b4874ea22615e93351043a1d6a84616895598f7a", + "sha256": "f977c3b6d8ac7161eeb7025492ac820fc4a17465a54dbec223359b8d72ec6346", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/resources/realtime/realtime.py", + "track": "T11", + "before": { + "path": "src/openai/resources/realtime/realtime.py", + "added": 9, + "removed": 3, + "patch": 12, + "present": true, + "current_lines": 1660, + "generated_lines": 1654, + "track": "T11" + }, + "candidate_physical_lines": 1660, + "checkpoint_physical_lines": 1660, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "27fc71a5babd6f21c2fe44d871d508b65b26dace", + "sha256": "0285ac5edb9b8b17f296b2d513068cb35c3413895e35424221765cc4a15c8592", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/types/decision_input_image_param.py", + "track": "T1", + "before": { + "path": "src/openai/types/decision_input_image_param.py", + "added": 11, + "removed": 1, + "patch": 12, + "present": true, + "current_lines": 47, + "generated_lines": 37, + "track": "T1" + }, + "candidate_physical_lines": 47, + "checkpoint_physical_lines": 47, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "54fc06ea5613845d7e45dfb5abf94c166e5dca7d", + "sha256": "347c633b1312fb62ecceffc147d0a250954392a09c07fa680d585dd41a3f8c60", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/types/decision_input_text_param.py", + "track": "T1", + "before": { + "path": "src/openai/types/decision_input_text_param.py", + "added": 11, + "removed": 1, + "patch": 12, + "present": true, + "current_lines": 33, + "generated_lines": 23, + "track": "T1" + }, + "candidate_physical_lines": 33, + "checkpoint_physical_lines": 33, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "7398dfcfeb00d238a5b6c4520bd7f19cf4d6d1f0", + "sha256": "416864cec231da63e0324268e57192de9902399cf0119d061467c6117a3090c7", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/types/webhooks/__init__.py", + "track": "T1", + "before": { + "path": "src/openai/types/webhooks/__init__.py", + "added": 3, + "removed": 1, + "patch": 4, + "present": true, + "current_lines": 73, + "generated_lines": 71, + "track": "T1" + }, + "candidate_physical_lines": 73, + "checkpoint_physical_lines": 73, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "cffb48f9677e2d8039298f17041964aee5e40c0c", + "sha256": "a5774cf8e193a6d9299039fa823adb236b2dbb5cce675ccc9efbed2a613b1aa2", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/types/webhooks/unwrap_webhook_event.py", + "track": "T1", + "before": { + "path": "src/openai/types/webhooks/unwrap_webhook_event.py", + "added": 2, + "removed": 2, + "patch": 4, + "present": true, + "current_lines": 74, + "generated_lines": 74, + "track": "T1" + }, + "candidate_physical_lines": 74, + "checkpoint_physical_lines": 74, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "a5429f046b818109dc900b878ffe6af2c6fa6557", + "sha256": "f1285c95b535b806614b4da2b35607fb1c8759dd23520d3b79c2506ef1e69a70", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/types/responses/tool.py", + "track": "T1", + "before": { + "path": "src/openai/types/responses/tool.py", + "added": 1, + "removed": 2, + "patch": 3, + "present": true, + "current_lines": 398, + "generated_lines": 399, + "track": "T1" + }, + "candidate_physical_lines": 398, + "checkpoint_physical_lines": 398, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "497756bd3c75403e7f8192ed3cf1c1b2f40f4c66", + "sha256": "3ef27c46ab304c48f7ea43b9bb6df6cdb9e842268536415cd9ef0fcaea02e51f", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/types/responses/tool_param.py", + "track": "T1", + "before": { + "path": "src/openai/types/responses/tool_param.py", + "added": 3, + "removed": 0, + "patch": 3, + "present": true, + "current_lines": 398, + "generated_lines": 395, + "track": "T1" + }, + "candidate_physical_lines": 399, + "checkpoint_physical_lines": 399, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "0f49f868836e9bd4003e4baac91c070af8330c36", + "sha256": "3c9a6d95a41c4870e190679eb6d1c9b7a16440bb32001a3954cbae6ab6e541f5", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/types/webhooks/live_call_incoming_webhook_event.py", + "track": "T1", + "before": { + "path": "src/openai/types/webhooks/live_call_incoming_webhook_event.py", + "added": 2, + "removed": 1, + "patch": 3, + "present": true, + "current_lines": 71, + "generated_lines": 70, + "track": "T1" + }, + "candidate_physical_lines": 71, + "checkpoint_physical_lines": 71, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "2caba71d3e4a071b17ffcae1520dc4a3920e7688", + "sha256": "2dd3bc70a9df1f12e5333983946e1df037a69f712ce104c75324768034df5c24", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/types/fine_tuning/fine_tuning_job_integration.py", + "track": "T1", + "before": { + "path": "src/openai/types/fine_tuning/fine_tuning_job_integration.py", + "added": 0, + "removed": 2, + "patch": 2, + "present": true, + "current_lines": 5, + "generated_lines": 7, + "track": "T1" + }, + "candidate_physical_lines": 5, + "checkpoint_physical_lines": 5, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "8fefe8a90268ff12a1f010b6e3855d87f78bb47b", + "sha256": "add6f540e2493ce1c9e4b18c451669acd44443f84ffc364d03ba5299134c3f85", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py", + "track": "T1", + "before": { + "path": "src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py", + "added": 1, + "removed": 1, + "patch": 2, + "present": true, + "current_lines": 40, + "generated_lines": 40, + "track": "T1" + }, + "candidate_physical_lines": 40, + "checkpoint_physical_lines": 40, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "6e528de19d16490a4e1e76337aa324192c6bc916", + "sha256": "e0e955bb2d08d25f9217a2b3f48b2ac0a20328af2ac1765bfb94f616de9c1e7f", + "bytes_equal": true, + "modes_equal": true + }, + { + "path": "src/openai/types/realtime/realtime_error_event.py", + "track": "T1", + "before": { + "path": "src/openai/types/realtime/realtime_error_event.py", + "added": 1, + "removed": 1, + "patch": 2, + "present": true, + "current_lines": 25, + "generated_lines": 25, + "track": "T1" + }, + "candidate_physical_lines": 25, + "checkpoint_physical_lines": 25, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_mode": "100644", + "checkpoint_mode": "100644", + "git_blob_oid": "1cd8636d0b65cd465b24ce8b35493ed0c377f445", + "sha256": "3caef26e78cca8e07685e5c40b8f12f7cc3416f473407eb254b6c7a38b6b52fe", + "bytes_equal": true, + "modes_equal": true + } + ], + "totals": { + "mixed_files": 0, + "added_lines": 0, + "deleted_lines": 0, + "custom_patch_lines": 0, + "former_mixed_files": 27, + "former_mixed_physical_lines": 31290, + "legacy_reporter_owned_paths": 1971, + "strict_manifest_entries": 1972, + "checkpoint_total_paths_including_excluded_workflows": 1979, + "excluded_checkpoint_workflow_paths": 8, + "all_generated_owned_paths_bytes_and_modes_match": true, + "handwritten_runtime_files": 9, + "handwritten_runtime_physical_lines": 3427, + "handwritten_runtime_baseline_physical_lines": 39 + }, + "ownership_definitions": { + "strict_generated_manifest": { + "path": "scripts/castiron/generated-files.json", + "entries": 1972, + "sha256": "b443288567fea005555354848817bc0815c7a9979b98b1917ab5042a290d30c6", + "includes_workflows": true, + "includes_itself": false + }, + "legacy_reporter": { + "counted_paths": 1971, + "exclusions": [ + ".github/actions/", + ".github/workflows/" + ], + "includes_manifest_itself": true + }, + "checkpoint": { + "total_paths": 1979 + }, + "manifest_only_vs_reporter": [ + ".github/workflows/castiron-custom-code-comment.yml", + ".github/workflows/castiron-custom-code.yml" + ], + "reporter_only_vs_manifest": [ + "scripts/castiron/generated-files.json" + ], + "checkpoint_only_vs_manifest": [ + ".github/workflows/ci.yml", + ".github/workflows/codeql.yml", + ".github/workflows/create-releases.yml", + ".github/workflows/detect-breaking-changes.yml", + ".github/workflows/publish-pypi.yml", + ".github/workflows/python-version-review.yml", + "scripts/castiron/generated-files.json" + ], + "manifest_only_vs_checkpoint": [], + "interpretation": "These counts use different ownership definitions, not conflicting measurements. The strict manifest includes two generated reporter workflows and excludes its own file; the legacy reporter excludes all workflows and counts the manifest itself." + }, + "handwritten_runtime_files": [ + { + "path": "src/openai/lib/_assistants_runs.py", + "existing_in_public_baseline": false, + "baseline_physical_lines": 0, + "candidate_physical_lines": 768, + "sha256": "718ecdaf3ae5f33fb47968ba27c0e0d4e4a77cd2650000f4e99224eaebfc3524", + "delivery_sha256_matches": true, + "mode": "100644", + "generated_owned": false + }, + { + "path": "src/openai/lib/_assistants_threads.py", + "existing_in_public_baseline": false, + "baseline_physical_lines": 0, + "candidate_physical_lines": 287, + "sha256": "8c6d1b5a37b8eafad46074a3edfbb16961033d9d95ce9005a505d6cd05c18c11", + "delivery_sha256_matches": true, + "mode": "100644", + "generated_owned": false + }, + { + "path": "src/openai/lib/_chat_completion_resources.py", + "existing_in_public_baseline": false, + "baseline_physical_lines": 0, + "candidate_physical_lines": 490, + "sha256": "7db66232ade185e7aaadddf1db7685e7e5a9257939be8a2c09ad75e369513384", + "delivery_sha256_matches": true, + "mode": "100644", + "generated_owned": false + }, + { + "path": "src/openai/lib/_client_auth.py", + "existing_in_public_baseline": false, + "baseline_physical_lines": 0, + "candidate_physical_lines": 977, + "sha256": "62ca6e5f533e7fb1669cefcef4c0ee8c2505cd73c9c645eb60b8b1d7caede228", + "delivery_sha256_matches": true, + "mode": "100644", + "generated_owned": false + }, + { + "path": "src/openai/lib/_module_runtime.py", + "existing_in_public_baseline": false, + "baseline_physical_lines": 0, + "candidate_physical_lines": 120, + "sha256": "b7af072a96049c5db1cc39b4c6dfa0225e6cd6f0f208f19c590f0f04e046b776", + "delivery_sha256_matches": true, + "mode": "100644", + "generated_owned": false + }, + { + "path": "src/openai/lib/_request_context.py", + "existing_in_public_baseline": false, + "baseline_physical_lines": 0, + "candidate_physical_lines": 13, + "sha256": "0d60fd8534d5fe6aa622d1109f88227c8807a606f4a964f2b1fa13c64cb6109b", + "delivery_sha256_matches": true, + "mode": "100644", + "generated_owned": false + }, + { + "path": "src/openai/lib/_response_resources.py", + "existing_in_public_baseline": false, + "baseline_physical_lines": 0, + "candidate_physical_lines": 643, + "sha256": "c0667c3afe27befe99d3d22aca0b6bd7b9d9ef58da9c45c8ac9919d04fc5ee62", + "delivery_sha256_matches": true, + "mode": "100644", + "generated_owned": false + }, + { + "path": "src/openai/lib/beta/agents/_create.py", + "existing_in_public_baseline": false, + "baseline_physical_lines": 0, + "candidate_physical_lines": 69, + "sha256": "0acd65b368d84c7594ed0a7874e6bc782306dd13dbf98e184833d926c8d20eef", + "delivery_sha256_matches": true, + "mode": "100644", + "generated_owned": false + }, + { + "path": "src/openai/lib/_parsing/_embeddings.py", + "existing_in_public_baseline": true, + "baseline_physical_lines": 39, + "candidate_physical_lines": 60, + "sha256": "021e7bd918772dfee03869b52e6de1e1c79892a957b12a89504b111238d7a789", + "delivery_sha256_matches": true, + "mode": "100644", + "generated_owned": false + } + ], + "validation": { + "full_sdk_lint": "passed (Ruff, Pyright, Mypy over 2056 files, import check)", + "focused_reporting_tests": { + "run": 102, + "passed": 101, + "skipped": 1, + "seconds": 68.186, + "skip": "existing compiler-contract test" + }, + "public_promotion": "existing public PR4157; measured candidate requires repromotion", + "default_configuration_activation": "pending", + "additive_alias": "CreateChatCompletionRequestMessageParam is the same runtime union object as ChatCompletionMessageParam, with identical six members and ordering; existing declarations unchanged" + }, + "codegen_hash": "5f1421c905f7f97582e04beddc7179ec77da6a8a69cf59656f2ed282e9345d49" + }, + { + "sdk_commit": "c5519111ba3b907ba226175f2401ec8b18c18fad", + "baseline_sdk_commit": "fa9a1616fd175e68dec9960d33dee2caefc0e8d3", + "codegen_commit": "da5d06c76d64ed28f1ec415c924323d16900cadc", + "generation_id": "9c4bdbf4-d266-4211-a6ce-6e39bbee056d", + "codegen_hash": "5dce7cd552b6d28818b7660de7c735877fd738f233f253a34f602cf8939a537b", + "compiler_commit": "babbc1ed17987d1c6017b20236058550a2e37cec", + "mixed_files": 2, + "custom_patch_lines": 12, + "patch_additions": 10, + "patch_deletions": 2, + "former_mixed_files_now_matching": 39, + "generated_manifest_entries_verified": 1980, + "generated_manifest_entries_matching": 1978, + "delivered": false, + "status": "intermediate candidate; two released Decisions compatibility patches retained pending canonical compiler support; measured against fa9, not yet regenerated against e8 release baseline", + "files": [ + { + "path": "api.md", + "baseline_physical_lines": 2094, + "baseline_added": 126, + "baseline_removed": 8, + "baseline_patch": 134, + "candidate_physical_lines": 1982, + "checkpoint_physical_lines": 1982, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "5be23e56a8b2a77fb568670f9cbeb3b1243c64ecdfc992b6280924a54b300b8e", + "bytes_equal": true + }, + { + "path": "scripts/castiron/CUSTOM_CODE.md", + "baseline_physical_lines": 217, + "baseline_added": 0, + "baseline_removed": 40, + "baseline_patch": 40, + "candidate_physical_lines": 257, + "checkpoint_physical_lines": 257, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "7f8812395e9ff0022e82ea84d56f0db5e6901ea7c429e69e4d5f13366c7fb2b4", + "bytes_equal": true + }, + { + "path": "scripts/castiron/custom_code_budget.py", + "baseline_physical_lines": 702, + "baseline_added": 1, + "baseline_removed": 4, + "baseline_patch": 5, + "candidate_physical_lines": 705, + "checkpoint_physical_lines": 705, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "ba44999ef44cd80cf1afb9db4013c29969013d6fc8cf78f81220752175d3a539", + "bytes_equal": true + }, + { + "path": "scripts/castiron/custom_code_policy.py", + "baseline_physical_lines": 481, + "baseline_added": 1, + "baseline_removed": 4, + "baseline_patch": 5, + "candidate_physical_lines": 484, + "checkpoint_physical_lines": 484, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "c9c37ea2de771502bc806370362ebf5486e950e50f6fc11ec90b85eb48802c08", + "bytes_equal": true + }, + { + "path": "scripts/castiron/custom_code_report.py", + "baseline_physical_lines": 1250, + "baseline_added": 19, + "baseline_removed": 254, + "baseline_patch": 273, + "candidate_physical_lines": 1485, + "checkpoint_physical_lines": 1485, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "a52a1aabf4047f4c20e96d1d788c25a4aa78c1987b4fa73a13d22c92ef213a3f", + "bytes_equal": true + }, + { + "path": "scripts/castiron/fixtures/github_publisher.cjs", + "baseline_physical_lines": 69, + "baseline_added": 10, + "baseline_removed": 63, + "baseline_patch": 73, + "candidate_physical_lines": 122, + "checkpoint_physical_lines": 122, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "062eb3da63eb0a25cf8de21e9aa0025afb1322eaf4ae59d5d6b3515b3dabda4b", + "bytes_equal": true + }, + { + "path": "scripts/castiron/test_custom_code_policy.py", + "baseline_physical_lines": 587, + "baseline_added": 0, + "baseline_removed": 4, + "baseline_patch": 4, + "candidate_physical_lines": 591, + "checkpoint_physical_lines": 591, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "ce6516a7121a494d9e108de2ceddf3344e3835926d70f256c78d25d6c7825e72", + "bytes_equal": true + }, + { + "path": "scripts/castiron/test_custom_code_publication.py", + "baseline_physical_lines": 873, + "baseline_added": 4, + "baseline_removed": 600, + "baseline_patch": 604, + "candidate_physical_lines": 1469, + "checkpoint_physical_lines": 1469, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "38d723f458225eb2d4745aca4044f6e4283f10b2a9d3a5257349b04ecd1c8c75", + "bytes_equal": true + }, + { + "path": "scripts/castiron/test_custom_code_report.py", + "baseline_physical_lines": 1084, + "baseline_added": 0, + "baseline_removed": 489, + "baseline_patch": 489, + "candidate_physical_lines": 1573, + "checkpoint_physical_lines": 1573, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "f4d90fd38b17aac59096488391b65ef9da5493d2049d4c038d5926a960257ca7", + "bytes_equal": true + }, + { + "path": "src/openai/__init__.py", + "baseline_physical_lines": 505, + "baseline_added": 185, + "baseline_removed": 1, + "baseline_patch": 186, + "candidate_physical_lines": 470, + "checkpoint_physical_lines": 470, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "a410d257a94f2c4f6cd0951a178131e8d883e21a69ef6bea245271db215ba196", + "bytes_equal": true + }, + { + "path": "src/openai/_client.py", + "baseline_physical_lines": 2440, + "baseline_added": 759, + "baseline_removed": 60, + "baseline_patch": 819, + "candidate_physical_lines": 1912, + "checkpoint_physical_lines": 1912, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "616e33f35af68a15cd312d9dc68414f41080a7c6275aa61f56cc1c34dad4fa29", + "bytes_equal": true + }, + { + "path": "src/openai/resources/audio/transcriptions.py", + "baseline_physical_lines": 1089, + "baseline_added": 149, + "baseline_removed": 453, + "baseline_patch": 602, + "candidate_physical_lines": 1061, + "checkpoint_physical_lines": 1061, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "8bfaa74f7b00a61984e2ee801357dbf29fdda7f7388d245df7458f0c20820ea9", + "bytes_equal": true + }, + { + "path": "src/openai/resources/audio/translations.py", + "baseline_physical_lines": 374, + "baseline_added": 21, + "baseline_removed": 46, + "baseline_patch": 67, + "candidate_physical_lines": 360, + "checkpoint_physical_lines": 360, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "e9e2fc5ac27461366225c8e6ca352a8db67b789c816bb0074b21c078e99afc05", + "bytes_equal": true + }, + { + "path": "src/openai/resources/beta/agents/environments/files.py", + "baseline_physical_lines": 604, + "baseline_added": 6, + "baseline_removed": 6, + "baseline_patch": 12, + "candidate_physical_lines": 604, + "checkpoint_physical_lines": 604, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "80794856126bd21246130cd64f3935f5a07b0e2d5fa5ae3a2de1afbf80d8eee0", + "bytes_equal": true + }, + { + "path": "src/openai/resources/beta/agents/sessions/artifacts.py", + "baseline_physical_lines": 573, + "baseline_added": 2, + "baseline_removed": 2, + "baseline_patch": 4, + "candidate_physical_lines": 573, + "checkpoint_physical_lines": 573, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "d551e527bbfd700bb85608226ffd942117bc2bf8a0c33e9a8456a08fdc6470ed", + "bytes_equal": true + }, + { + "path": "src/openai/resources/beta/agents/sessions/sessions.py", + "baseline_physical_lines": 1304, + "baseline_added": 113, + "baseline_removed": 11, + "baseline_patch": 124, + "candidate_physical_lines": 1271, + "checkpoint_physical_lines": 1271, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "53987b64fb0b775e755a727f91d4486228dc2735947a3935e0073ca132c5a612", + "bytes_equal": true + }, + { + "path": "src/openai/resources/beta/responses/responses.py", + "baseline_physical_lines": 5575, + "baseline_added": 55, + "baseline_removed": 55, + "baseline_patch": 110, + "candidate_physical_lines": 5575, + "checkpoint_physical_lines": 5575, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "aca7b656abd01d90bc23030690562738aaa0921bb33de8016e379e6aec34d294", + "bytes_equal": true + }, + { + "path": "src/openai/resources/beta/threads/runs/runs.py", + "baseline_physical_lines": 3161, + "baseline_added": 1290, + "baseline_removed": 181, + "baseline_patch": 1471, + "candidate_physical_lines": 2889, + "checkpoint_physical_lines": 2889, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "a430fe262b62d81ac6e1d8161e2e4d23a16c01c4bcdcc98a900392db5327d97c", + "bytes_equal": true + }, + { + "path": "src/openai/resources/beta/threads/threads.py", + "baseline_physical_lines": 2009, + "baseline_added": 390, + "baseline_removed": 7, + "baseline_patch": 397, + "candidate_physical_lines": 1951, + "checkpoint_physical_lines": 1951, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "1a49dcdeb83ce84b16160b2e17cc4cbfee76c8f93267fe22a727bc420dd92c3c", + "bytes_equal": true + }, + { + "path": "src/openai/resources/chat/completions/completions.py", + "baseline_physical_lines": 3456, + "baseline_added": 603, + "baseline_removed": 1, + "baseline_patch": 604, + "candidate_physical_lines": 3363, + "checkpoint_physical_lines": 3363, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "0a0d3dfc38a12dff71afba526d745792f33db3e720c04ba755bb966c726f8a16", + "bytes_equal": true + }, + { + "path": "src/openai/resources/conversations/api.md", + "baseline_physical_lines": 42, + "baseline_added": 1, + "baseline_removed": 0, + "baseline_patch": 1, + "candidate_physical_lines": 41, + "checkpoint_physical_lines": 41, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "97ed38af759c8c47e8df79f2335a0cd9d2871cfaaea7423d4ae4eaea5a63a1b1", + "bytes_equal": true + }, + { + "path": "src/openai/resources/embeddings.py", + "baseline_physical_lines": 263, + "baseline_added": 27, + "baseline_removed": 21, + "baseline_patch": 48, + "candidate_physical_lines": 264, + "checkpoint_physical_lines": 264, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "88aed80308e7ca8280fa29ebbf093a533f4dc327a902ace6467c5b8f52e4b387", + "bytes_equal": true + }, + { + "path": "src/openai/resources/realtime/api.md", + "baseline_physical_lines": 149, + "baseline_added": 0, + "baseline_removed": 15, + "baseline_patch": 15, + "candidate_physical_lines": 164, + "checkpoint_physical_lines": 164, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "f977c3b6d8ac7161eeb7025492ac820fc4a17465a54dbec223359b8d72ec6346", + "bytes_equal": true + }, + { + "path": "src/openai/resources/realtime/realtime.py", + "baseline_physical_lines": 1660, + "baseline_added": 9, + "baseline_removed": 3, + "baseline_patch": 12, + "candidate_physical_lines": 1660, + "checkpoint_physical_lines": 1660, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "0285ac5edb9b8b17f296b2d513068cb35c3413895e35424221765cc4a15c8592", + "bytes_equal": true + }, + { + "path": "src/openai/resources/realtime/translations/translations.py", + "baseline_physical_lines": 1152, + "baseline_added": 34, + "baseline_removed": 18, + "baseline_patch": 52, + "candidate_physical_lines": 1152, + "checkpoint_physical_lines": 1152, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "805923247278589582e4d4db869aa55f37a18130178d6c467f1c59b67825259c", + "bytes_equal": true + }, + { + "path": "src/openai/resources/responses/api.md", + "baseline_physical_lines": 208, + "baseline_added": 2, + "baseline_removed": 0, + "baseline_patch": 2, + "candidate_physical_lines": 206, + "checkpoint_physical_lines": 206, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "5e100d14c416564c1c1ba4e9e0d9dbd3e33fab271405473ba77038f645aeb86e", + "bytes_equal": true + }, + { + "path": "src/openai/resources/responses/responses.py", + "baseline_physical_lines": 5181, + "baseline_added": 920, + "baseline_removed": 74, + "baseline_patch": 994, + "candidate_physical_lines": 4866, + "checkpoint_physical_lines": 4866, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "0fd253db414975a83148d3ff07b4e5c9c5329a1a6c33c4f2ca99294ef286d7ec", + "bytes_equal": true + }, + { + "path": "src/openai/resources/uploads/uploads.py", + "baseline_physical_lines": 652, + "baseline_added": 6, + "baseline_removed": 28, + "baseline_patch": 34, + "candidate_physical_lines": 674, + "checkpoint_physical_lines": 674, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "83658b3e4ff7d6c273673b46bc7b34a9302b4b138a89c24ab63da07d6e44a8dd", + "bytes_equal": true + }, + { + "path": "src/openai/resources/webhooks/__init__.py", + "baseline_physical_lines": 42, + "baseline_added": 24, + "baseline_removed": 55, + "baseline_patch": 79, + "candidate_physical_lines": 43, + "checkpoint_physical_lines": 43, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "c1e56f4b97470de30bd95fc65ab4bc5f6c3f2ea37224c3ce4e6dd6db9e2bbb3c", + "bytes_equal": true + }, + { + "path": "src/openai/resources/webhooks/webhooks.py", + "baseline_physical_lines": 1146, + "baseline_added": 89, + "baseline_removed": 4, + "baseline_patch": 93, + "candidate_physical_lines": 1146, + "checkpoint_physical_lines": 1146, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "f730653c5937f4c723f5519c7e86823fcba0ee088c270ecbd8e02fd10e49c654", + "bytes_equal": true + }, + { + "path": "src/openai/types/__init__.py", + "baseline_physical_lines": 157, + "baseline_added": 4, + "baseline_removed": 1, + "baseline_patch": 5, + "candidate_physical_lines": 157, + "checkpoint_physical_lines": 154, + "candidate_added": 4, + "candidate_removed": 1, + "candidate_patch": 5, + "candidate_sha256": "98f15c4ee123816edfbbd0b758a67ca30b80f80939d158e8bed9a1b518d7c6ae", + "bytes_equal": false + }, + { + "path": "src/openai/types/decision_input_image_param.py", + "baseline_physical_lines": 47, + "baseline_added": 11, + "baseline_removed": 1, + "baseline_patch": 12, + "candidate_physical_lines": 47, + "checkpoint_physical_lines": 47, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "347c633b1312fb62ecceffc147d0a250954392a09c07fa680d585dd41a3f8c60", + "bytes_equal": true + }, + { + "path": "src/openai/types/decision_input_message_param.py", + "baseline_physical_lines": 95, + "baseline_added": 6, + "baseline_removed": 1, + "baseline_patch": 7, + "candidate_physical_lines": 95, + "checkpoint_physical_lines": 90, + "candidate_added": 6, + "candidate_removed": 1, + "candidate_patch": 7, + "candidate_sha256": "22aff1b989c87ee05e3041c1caa9f3dd8540cbb381d96281ee75237b508297aa", + "bytes_equal": false + }, + { + "path": "src/openai/types/decision_input_text_param.py", + "baseline_physical_lines": 33, + "baseline_added": 11, + "baseline_removed": 1, + "baseline_patch": 12, + "candidate_physical_lines": 33, + "checkpoint_physical_lines": 33, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "416864cec231da63e0324268e57192de9902399cf0119d061467c6117a3090c7", + "bytes_equal": true + }, + { + "path": "src/openai/types/fine_tuning/fine_tuning_job_integration.py", + "baseline_physical_lines": 5, + "baseline_added": 0, + "baseline_removed": 2, + "baseline_patch": 2, + "candidate_physical_lines": 5, + "checkpoint_physical_lines": 5, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "add6f540e2493ce1c9e4b18c451669acd44443f84ffc364d03ba5299134c3f85", + "bytes_equal": true + }, + { + "path": "src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py", + "baseline_physical_lines": 40, + "baseline_added": 1, + "baseline_removed": 1, + "baseline_patch": 2, + "candidate_physical_lines": 40, + "checkpoint_physical_lines": 40, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "cc0ebd7e315fd30a64e9df0ff1e8940fc06d2d3d0fdf887dba749759d4d418f9", + "bytes_equal": true + }, + { + "path": "src/openai/types/realtime/realtime_error_event.py", + "baseline_physical_lines": 25, + "baseline_added": 1, + "baseline_removed": 1, + "baseline_patch": 2, + "candidate_physical_lines": 25, + "checkpoint_physical_lines": 25, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "7bfb5d08c6e790a4edb5b85a4cb765f34faa7e43e4992ba6a7691fe579749f3c", + "bytes_equal": true + }, + { + "path": "src/openai/types/responses/tool_param.py", + "baseline_physical_lines": 398, + "baseline_added": 3, + "baseline_removed": 0, + "baseline_patch": 3, + "candidate_physical_lines": 399, + "checkpoint_physical_lines": 399, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "3c9a6d95a41c4870e190679eb6d1c9b7a16440bb32001a3954cbae6ab6e541f5", + "bytes_equal": true + }, + { + "path": "src/openai/types/webhooks/__init__.py", + "baseline_physical_lines": 73, + "baseline_added": 3, + "baseline_removed": 1, + "baseline_patch": 4, + "candidate_physical_lines": 73, + "checkpoint_physical_lines": 73, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "a5774cf8e193a6d9299039fa823adb236b2dbb5cce675ccc9efbed2a613b1aa2", + "bytes_equal": true + }, + { + "path": "src/openai/types/webhooks/live_call_incoming_webhook_event.py", + "baseline_physical_lines": 71, + "baseline_added": 2, + "baseline_removed": 1, + "baseline_patch": 3, + "candidate_physical_lines": 71, + "checkpoint_physical_lines": 71, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "2dd3bc70a9df1f12e5333983946e1df037a69f712ce104c75324768034df5c24", + "bytes_equal": true + }, + { + "path": "src/openai/types/webhooks/unwrap_webhook_event.py", + "baseline_physical_lines": 74, + "baseline_added": 2, + "baseline_removed": 2, + "baseline_patch": 4, + "candidate_physical_lines": 74, + "checkpoint_physical_lines": 74, + "candidate_added": 0, + "candidate_removed": 0, + "candidate_patch": 0, + "candidate_sha256": "f1285c95b535b806614b4da2b35607fb1c8759dd23520d3b79c2506ef1e69a70", + "bytes_equal": true + } + ], + "manifest_verification": { + "candidate": "c5519111ba3b907ba226175f2401ec8b18c18fad", + "generated_checkpoint": "da5d06c76d64ed28f1ec415c924323d16900cadc", + "entries_verified": 1980, + "matching_entries": 1978, + "residual_mixed_files": [ + "src/openai/types/__init__.py", + "src/openai/types/decision_input_message_param.py" + ], + "all_generated_hashes_and_modes_authentic": true + }, + "public_preservation": { + "baseline": "fa9a1616fd175e68dec9960d33dee2caefc0e8d3", + "candidate": "c5519111ba3b907ba226175f2401ec8b18c18fad", + "files": [ + { + "path": ".castiron.stats.yml", + "baseline_sha256": "596cc1e65a14ed2b3e44e7b82282c82685538931b8578cb2f174b494d938a771", + "candidate_sha256": "920b6a70f04ccf985d9dc6cf0161fc44a23e585fb3f95f87dfebc07572165bf9", + "identical": false + }, + { + "path": ".github/workflows/ci.yml", + "baseline_sha256": "7d7930c2d05d9e2cbb0d4c676aca15302228791e55ae8dbf5457ab08f1c4f254", + "candidate_sha256": "7d7930c2d05d9e2cbb0d4c676aca15302228791e55ae8dbf5457ab08f1c4f254", + "identical": true + }, + { + "path": ".github/workflows/create-releases.yml", + "baseline_sha256": "b8d07645aaa697d45881bcb80779a9f89b7956b46ed7cabb03f4dd234d0d8cdd", + "candidate_sha256": "b8d07645aaa697d45881bcb80779a9f89b7956b46ed7cabb03f4dd234d0d8cdd", + "identical": true + }, + { + "path": ".github/workflows/detect-breaking-changes.yml", + "baseline_sha256": "86c89064a99fa2031785537fcb47d76459cc937d945ebdebb490bcaa66195f7d", + "candidate_sha256": "86c89064a99fa2031785537fcb47d76459cc937d945ebdebb490bcaa66195f7d", + "identical": true + }, + { + "path": ".github/workflows/publish-pypi.yml", + "baseline_sha256": "a06455002aa790f3fd45cb5d8b92e006941d6bfe5333128456ba35bd3b7fae89", + "candidate_sha256": "a06455002aa790f3fd45cb5d8b92e006941d6bfe5333128456ba35bd3b7fae89", + "identical": true + }, + { + "path": "api.md", + "baseline_sha256": "668a9076e479f27645ff1a53816b7d983c2767ad0a2520bbb08ccdf319e55f1a", + "candidate_sha256": "5be23e56a8b2a77fb568670f9cbeb3b1243c64ecdfc992b6280924a54b300b8e", + "identical": false + }, + { + "path": "api_reference/openapi.transformed.yml", + "baseline_sha256": "0a373e9664956c79e35bace5fdc5460630f359fb17edcdba000950eb94a9ecfc", + "candidate_sha256": "0a373e9664956c79e35bace5fdc5460630f359fb17edcdba000950eb94a9ecfc", + "identical": true + }, + { + "path": "examples/browser-use-demo/runner.py", + "baseline_sha256": "26a5b95f4fd572e08a4cf2b37fddc8b34948ddf6a57f816c3d7c51d19078fc7d", + "candidate_sha256": "26a5b95f4fd572e08a4cf2b37fddc8b34948ddf6a57f816c3d7c51d19078fc7d", + "identical": true + }, + { + "path": "scripts/castiron/generated-files.json", + "baseline_sha256": "0a89908606a99b00ce44c83d7fdc4dbab656cf4716a5aeb7344dd07e3a422d28", + "candidate_sha256": "41007d6c39616deabf384d067049292d0f2641b7ac0fa5a667ccd24f1afe1893", + "identical": false + }, + { + "path": "src/openai/lib/decisions_tool_runner.py", + "baseline_sha256": "eb652bb0d7af24c33e65cd6a1294bd7c320d833b1d1685afe5a0d62cd444ef94", + "candidate_sha256": "eb652bb0d7af24c33e65cd6a1294bd7c320d833b1d1685afe5a0d62cd444ef94", + "identical": true + }, + { + "path": "src/openai/resources/decisions.py", + "baseline_sha256": "611c66fb31858d0f37e61e7dbd7c9e5dd7140bdd23aceef70bf449b6658972d2", + "candidate_sha256": "611c66fb31858d0f37e61e7dbd7c9e5dd7140bdd23aceef70bf449b6658972d2", + "identical": true + }, + { + "path": "src/openai/types/__init__.py", + "baseline_sha256": "98f15c4ee123816edfbbd0b758a67ca30b80f80939d158e8bed9a1b518d7c6ae", + "candidate_sha256": "98f15c4ee123816edfbbd0b758a67ca30b80f80939d158e8bed9a1b518d7c6ae", + "identical": true + }, + { + "path": "src/openai/types/chat/__init__.py", + "baseline_sha256": "7aca686d72d200640a382e2c46d44f1e121297117b1571cf748d6899083cd327", + "candidate_sha256": "7aca686d72d200640a382e2c46d44f1e121297117b1571cf748d6899083cd327", + "identical": true + }, + { + "path": "src/openai/types/chat/create_chat_completion_request_message_param.py", + "baseline_sha256": "32f5e315583537d10a70a6d0f1f543bcbe4e5f36c1cc53ebf093fba6b9333c7a", + "candidate_sha256": "32f5e315583537d10a70a6d0f1f543bcbe4e5f36c1cc53ebf093fba6b9333c7a", + "identical": true + }, + { + "path": "src/openai/types/decision_create_params.py", + "baseline_sha256": "6803b3841127d2226aea298eab2c802c60040752fdfb2626e1a5ff1f43a010a4", + "candidate_sha256": "6803b3841127d2226aea298eab2c802c60040752fdfb2626e1a5ff1f43a010a4", + "identical": true + }, + { + "path": "src/openai/types/decision_input_message_param.py", + "baseline_sha256": "22aff1b989c87ee05e3041c1caa9f3dd8540cbb381d96281ee75237b508297aa", + "candidate_sha256": "22aff1b989c87ee05e3041c1caa9f3dd8540cbb381d96281ee75237b508297aa", + "identical": true + }, + { + "path": "src/openai/types/decision_input_part_union_param.py", + "baseline_sha256": "bfc84a78cb14e74ba38c942f2079fc9f07d0d5cfc63108cff18b1d9675d0320e", + "candidate_sha256": "bfc84a78cb14e74ba38c942f2079fc9f07d0d5cfc63108cff18b1d9675d0320e", + "identical": true + }, + { + "path": "src/openai/types/live/decision_trigger.py", + "baseline_sha256": "72b3fa55c82067c5377ef4c2ec8061f411363eec077cd2393780830fd3fb952b", + "candidate_sha256": "72b3fa55c82067c5377ef4c2ec8061f411363eec077cd2393780830fd3fb952b", + "identical": true + }, + { + "path": "src/openai/types/live/decision_trigger_param.py", + "baseline_sha256": "9d7338a7f85df1e4e3d2c0184bc73dd35d48ac21584d6c0a11ba02b87397c94b", + "candidate_sha256": "9d7338a7f85df1e4e3d2c0184bc73dd35d48ac21584d6c0a11ba02b87397c94b", + "identical": true + }, + { + "path": "src/openai/types/live/decisions_delegation_config.py", + "baseline_sha256": "6ad6442388302b3a8bfd4f18b771fe7934396b51fbb9a4b7f1a606303183fea6", + "candidate_sha256": "6ad6442388302b3a8bfd4f18b771fe7934396b51fbb9a4b7f1a606303183fea6", + "identical": true + }, + { + "path": "src/openai/types/live/decisions_delegation_config_param.py", + "baseline_sha256": "a2633c443288cb1962cae2968c43def5053ecc3f541d3f12d690ab05f6faa55b", + "candidate_sha256": "a2633c443288cb1962cae2968c43def5053ecc3f541d3f12d690ab05f6faa55b", + "identical": true + }, + { + "path": "src/openai/types/live/decisions_delegation_update_config.py", + "baseline_sha256": "1ca4177a723c322ef38ae7861e94328b4cb2fd4baa8caa12ef286b115e44d9a7", + "candidate_sha256": "1ca4177a723c322ef38ae7861e94328b4cb2fd4baa8caa12ef286b115e44d9a7", + "identical": true + }, + { + "path": "src/openai/types/live/decisions_delegation_update_config_param.py", + "baseline_sha256": "e71336c1bccd68614cac5916a8de7de14bc1d68a3e7d68ac9283a9ef3c595e09", + "candidate_sha256": "e71336c1bccd68614cac5916a8de7de14bc1d68a3e7d68ac9283a9ef3c595e09", + "identical": true + }, + { + "path": "src/openai/types/shared/__init__.py", + "baseline_sha256": "4253079b27893ed9bb808e43b61a0060c319e09678c5e49e26c77ea712d45cf0", + "candidate_sha256": "4253079b27893ed9bb808e43b61a0060c319e09678c5e49e26c77ea712d45cf0", + "identical": true + }, + { + "path": "src/openai/types/shared/decision_input_json.py", + "baseline_sha256": "161cecb4ba9d08ee9c4890609360b596901f4913eaf5464876b60236fa0cbb99", + "candidate_sha256": "161cecb4ba9d08ee9c4890609360b596901f4913eaf5464876b60236fa0cbb99", + "identical": true + }, + { + "path": "src/openai/types/shared/decision_question_input_image.py", + "baseline_sha256": "f1a7edd0fa6488dde6c1645db967dbc00a8b475d275b3e6aa63a7e6729a66c25", + "candidate_sha256": "f1a7edd0fa6488dde6c1645db967dbc00a8b475d275b3e6aa63a7e6729a66c25", + "identical": true + }, + { + "path": "src/openai/types/shared/decision_question_input_part.py", + "baseline_sha256": "35a97d935a32845e1bd3c1735e34d4c836087f0aa963afe51e77a1ee8cb5adf4", + "candidate_sha256": "35a97d935a32845e1bd3c1735e34d4c836087f0aa963afe51e77a1ee8cb5adf4", + "identical": true + }, + { + "path": "src/openai/types/shared/decision_question_input_text.py", + "baseline_sha256": "897e60c596bbe57d8966737973b33c9ccf2e46404e0414c44597e223f61cbf68", + "candidate_sha256": "897e60c596bbe57d8966737973b33c9ccf2e46404e0414c44597e223f61cbf68", + "identical": true + }, + { + "path": "src/openai/types/shared_params/__init__.py", + "baseline_sha256": "96f88f5ab552d204a4ee1d9c0864cc105835de612818c5bf1398b245670b26c7", + "candidate_sha256": "96f88f5ab552d204a4ee1d9c0864cc105835de612818c5bf1398b245670b26c7", + "identical": true + }, + { + "path": "src/openai/types/shared_params/decision_input_json.py", + "baseline_sha256": "0b050fd3ade1cdee2714406a4eaf6a9603dcdf2490ad7b82f531b354a068f7d0", + "candidate_sha256": "0b050fd3ade1cdee2714406a4eaf6a9603dcdf2490ad7b82f531b354a068f7d0", + "identical": true + }, + { + "path": "src/openai/types/shared_params/decision_question_input_image.py", + "baseline_sha256": "8f593680c8d02a715791da449dd13b44a75177da5a0af5b5a9ef89f6fe3fd189", + "candidate_sha256": "8f593680c8d02a715791da449dd13b44a75177da5a0af5b5a9ef89f6fe3fd189", + "identical": true + }, + { + "path": "src/openai/types/shared_params/decision_question_input_part.py", + "baseline_sha256": "23f40974b8ac08554af64fafe2bd783ef96af1cc7d04c2cdc0af2a7839008db9", + "candidate_sha256": "23f40974b8ac08554af64fafe2bd783ef96af1cc7d04c2cdc0af2a7839008db9", + "identical": true + }, + { + "path": "src/openai/types/shared_params/decision_question_input_text.py", + "baseline_sha256": "61cb46efe862e01326b4b00c9e4652a09c945bbc84d73c346cf2953e5cedf445", + "candidate_sha256": "61cb46efe862e01326b4b00c9e4652a09c945bbc84d73c346cf2953e5cedf445", + "identical": true + }, + { + "path": "tests/api_resources/live/test_sessions.py", + "baseline_sha256": "2610c853c6b96655b18b54b770c11843a774b4d1f8dbd5eeb9f9672e158040e8", + "candidate_sha256": "2610c853c6b96655b18b54b770c11843a774b4d1f8dbd5eeb9f9672e158040e8", + "identical": true + }, + { + "path": "tests/api_resources/test_decisions.py", + "baseline_sha256": "1a8f859704d9399c4f94da8fd48f5007ee3224f286a7f6688ad125ac4e566970", + "candidate_sha256": "1a8f859704d9399c4f94da8fd48f5007ee3224f286a7f6688ad125ac4e566970", + "identical": true + }, + { + "path": "tests/api_resources/test_live.py", + "baseline_sha256": "a770d729eb27749e1ee1aa62c51cd37e2fc995a95540bcfa1e579efde492072d", + "candidate_sha256": "a770d729eb27749e1ee1aa62c51cd37e2fc995a95540bcfa1e579efde492072d", + "identical": true + }, + { + "path": "tests/lib/test_decisions_tool_runner.py", + "baseline_sha256": "45858c78fa2a5f6cf95291619c881cbf8e2e62151283d73f06b2532af84386b5", + "candidate_sha256": "45858c78fa2a5f6cf95291619c881cbf8e2e62151283d73f06b2532af84386b5", + "identical": true + } + ] + }, + "validation": { + "full_sdk_lint": "passed; Mypy 2064 source files, Pyright, Ruff and import checks", + "runtime_tests_passed": 166, + "runtime_tests_seconds": 2.41, + "scope": "local checks at immutable c551; does not establish final regenerated output, public delivery or current-head hosted approval" + }, + "required_next_public_baseline": "e8a0fd28bbdc1699c97ff0f602b1af3dd378382d" + }, + { + "sdk_commit": "b77cc45e9f9c67e37ec533ddbd3974268b1632a1", + "baseline_sdk_commit": "e8a0fd28bbdc1699c97ff0f602b1af3dd378382d", + "codegen_commit": "6bfbad3227b8aec1e0ca8fcdb8430133abca07da", + "generation_id": "17ef66c0-d43e-4e42-8625-87458f262ad0", + "codegen_hash": "fb0da70eaf358e49d9c3822bcef29a4fe9c50674a3492eb69a1a353db2e0596e", + "compiler_commit": "b32461570107d2f81a5412178ba2c195e9216f74", + "mixed_files": 0, + "custom_patch_lines": 0, + "patch_additions": 0, + "patch_deletions": 0, + "former_mixed_files_now_matching": 41, + "generated_manifest_entries_verified": 1980, + "generated_manifest_entries_matching": 1980, + "delivered": false, + "status": "Measured final candidate: zero mixed files and zero custom patch lines; public delivery and default activation pending.", + "manifest_verification": { + "candidate": "e45e6603bdd5ec631cbf85f08eba668087d0fa8b", + "generated_checkpoint": "91e9a9626634d945190d53801a2e3aaa4ffd57c9", + "entries_verified": 1980, + "matching_entries": 1980, + "residual_mixed_files": [], + "all_generated_hashes_and_modes_authentic": true + }, + "validation": { + "runtime": { + "passed": 166, + "seconds": 2.73 + }, + "full_sdk_lint": "passed: Ruff, Pyright, Mypy (2064 source files), imports", + "reporting_tests": { + "run": 217, + "passed": 216, + "skipped": 1, + "seconds": 142.665 + }, + "hosted_checks": "pending; separate Ruby source post-merge failure under investigation", + "public_delivery": "pending", + "default_activation": "pending" + }, + "physical_lines_in_former_mixed_files": 40039 + }, + { + "sdk_commit": "e45e6603bdd5ec631cbf85f08eba668087d0fa8b", + "baseline_sdk_commit": "05dfea0a1cb41a3e6ae1cc3bd13145b57cf3ffb8", + "codegen_commit": "91e9a9626634d945190d53801a2e3aaa4ffd57c9", + "generation_id": "5c0f9c89-1281-4037-ab5b-b87eae619152", + "codegen_hash": "fb0da70eaf358e49d9c3822bcef29a4fe9c50674a3492eb69a1a353db2e0596e", + "compiler_commit": "b32461570107d2f81a5412178ba2c195e9216f74", + "mixed_files": 0, + "custom_patch_lines": 0, + "patch_additions": 0, + "patch_deletions": 0, + "former_mixed_files_now_matching": 41, + "generated_manifest_entries_verified": 1980, + "generated_manifest_entries_matching": 1980, + "delivered": false, + "status": "Historical e45 measured candidate; superseded by b53 before public delivery.", + "manifest_verification": { + "candidate": "e45e6603bdd5ec631cbf85f08eba668087d0fa8b", + "generated_checkpoint": "91e9a9626634d945190d53801a2e3aaa4ffd57c9", + "entries_verified": 1980, + "matching_entries": 1980, + "residual_mixed_files": [], + "all_generated_hashes_and_modes_authentic": true + }, + "validation": { + "runtime": { + "passed": 233, + "seconds": 2.91 + }, + "full_sdk_lint": "passed: Ruff, Pyright, Mypy2064, imports", + "reporting_tests": { + "carried_from": "b77cc45e9f9c67e37ec533ddbd3974268b1632a1", + "tree_identical": true, + "tests": 217, + "passed": 216, + "existing_skip": 1 + }, + "hosted_checks": "pending; separate Ruby source repair validation in progress", + "public_delivery": "pending", + "default_activation": "pending" + }, + "physical_lines_in_former_mixed_files": 40039, + "released_e8_preservation": { + "baseline": "e8a0fd28bbdc1699c97ff0f602b1af3dd378382d", + "candidate": "b77cc45e9f9c67e37ec533ddbd3974268b1632a1", + "files": [ + { + "path": ".release-please-manifest.json", + "sha256": "0066daf14f84fa9acba051b9353ada8f2c694d7b9fc4f39e200d05f6d63844ea", + "mode": "100644", + "identical": true + }, + { + "path": "CHANGELOG.md", + "sha256": "2fd2cb159a9de92ce59c9582d158f4bc66774c361326a8fe74b2cf44496052d6", + "mode": "100644", + "identical": true + }, + { + "path": "pyproject.toml", + "sha256": "313aed18e64b72242c791327aec53e894ff501a13261c579f96869422722981d", + "mode": "100644", + "identical": true + }, + { + "path": "src/openai/_version.py", + "sha256": "79285c9c3bc4d27b0184b8924f418c3b312ee02c321d748c07a88c0affd9e8c9", + "mode": "100644", + "identical": true + }, + { + "path": "uv.lock", + "sha256": "b23bae66a4ef28755110f0da323f14bf38294cbcd9f96e7586544a392532bbcf", + "mode": "100644", + "identical": true + } + ] + }, + "released_05_preservation": { + "baseline": "05dfea0a1cb41a3e6ae1cc3bd13145b57cf3ffb8", + "candidate": "e45e6603bdd5ec631cbf85f08eba668087d0fa8b", + "files": [ + { + "path": "src/openai/_models.py", + "sha256": "7e09adbab7d24de1c41a69e35bd80901bbfe9cfc3d29493571c8d24daceaf56c", + "mode": "100644", + "identical": true + }, + { + "path": "tests/test_union_parsing.py", + "sha256": "f1b06fb97023a96843ae8e2edacf37251b7718a885273802edc7acef8d9edec8", + "mode": "100644", + "identical": true + } + ] + } + } + ], + "current_transition_mechanisms": { + "baseline_sdk_commit": "cfa0a79b6395bb0561b28879122a9f90a87cdada", + "coverage": "All 41 current baseline mixed files, exactly once; historical lane counts are separate.", + "status": "All 41 baseline files match generation in measured b53; public delivery and default activation pending.", + "groups": [ + { + "area": "Shared reporting and policy templates", + "files": [ + "scripts/castiron/CUSTOM_CODE.md", + "scripts/castiron/custom_code_budget.py", + "scripts/castiron/custom_code_policy.py", + "scripts/castiron/custom_code_report.py", + "scripts/castiron/fixtures/github_publisher.cjs", + "scripts/castiron/test_custom_code_policy.py", + "scripts/castiron/test_custom_code_publication.py", + "scripts/castiron/test_custom_code_report.py" + ], + "mechanism_and_preservation": "Regenerate the shared documentation, reporter, policy, publisher fixture and regression tests from canonical templates. Template fixes preserve pagination, large identifier handling, setup calls and strict typing. The shared Go ratchet remains opt-in; Python ownership, exclusions and budget are unchanged. Updated shared next-policy selection compares effective policy and checker ancestry; policy-only updates and required authorization remain enforced. Python policy bytes, ceiling, ownership and counting are unchanged.", + "candidate_status": "Generated checkpoint and integrated candidate match exactly; public delivery pending." + }, + { + "area": "Client and package declarations", + "files": [ + "src/openai/_client.py", + "src/openai/__init__.py" + ], + "mechanism_and_preservation": "Typed client/package metadata generates signatures, imports, defaults, exports and delegation to handwritten authentication and module-runtime code. Explicit import scopes, binding validation and native member placement preserve original-module state, subclass dispatch, monkeypatching and sync/async behavior.", + "candidate_status": "Generated checkpoint and integrated candidate match exactly; public delivery pending." + }, + { + "area": "Parsing and request facades", + "files": [ + "src/openai/resources/audio/transcriptions.py", + "src/openai/resources/audio/translations.py", + "src/openai/resources/chat/completions/completions.py", + "src/openai/resources/responses/responses.py" + ], + "mechanism_and_preservation": "Typed helper signatures, overloads, request context, parser bindings and stream factories generate public facades. Existing handwritten parsing and request implementations retain sentinel handling, payload construction, stream lifecycle and response types; generated wrappers do not duplicate that runtime logic.", + "candidate_status": "Generated checkpoint and integrated candidate match exactly; public delivery pending." + }, + { + "area": "Assistants and Sessions facades", + "files": [ + "src/openai/resources/beta/threads/runs/runs.py", + "src/openai/resources/beta/threads/threads.py", + "src/openai/resources/beta/agents/sessions/sessions.py" + ], + "mechanism_and_preservation": "Typed resource helpers, positional request options, method ordering and deprecation metadata generate the existing public entry points. Handwritten Assistants and Sessions implementations retain polling, streaming, warning timing, tool dispatch and cleanup semantics.", + "candidate_status": "Generated checkpoint and integrated candidate match exactly; public delivery pending." + }, + { + "area": "Previously released upload and file helpers", + "files": [ + "src/openai/resources/beta/agents/environments/files.py", + "src/openai/resources/beta/agents/sessions/artifacts.py", + "src/openai/resources/uploads/uploads.py" + ], + "mechanism_and_preservation": "Retain released helper and upload bindings in the composed configuration and regenerate their facades. Existing handwritten implementations continue to own file processing and uploads, including synchronous factories on async resources, subclass hooks, lazy defaults/logger timing and payload limits.", + "candidate_status": "Generated checkpoint and integrated candidate match exactly; public delivery pending." + }, + { + "area": "Embedding parsing", + "files": [ + "src/openai/resources/embeddings.py" + ], + "mechanism_and_preservation": "The typed runtime hook delegates embedding response handling to the existing handwritten parser while retaining request defaults, encoding behavior and response transformation semantics.", + "candidate_status": "Generated checkpoint and integrated candidate match exactly; public delivery pending." + }, + { + "area": "Realtime and legacy response connections", + "files": [ + "src/openai/resources/realtime/realtime.py", + "src/openai/resources/realtime/translations/translations.py", + "src/openai/resources/beta/responses/responses.py" + ], + "mechanism_and_preservation": "Typed WebSocket runtime bindings and event-method ordering generate connection facades. Existing transport code retains query composition, client properties, older-runtime fallback and sync/async behavior; the managed Beta projection keeps its released declaration order.", + "candidate_status": "Generated checkpoint and integrated candidate match exactly; public delivery pending." + }, + { + "area": "Webhook runtime and compatibility exports", + "files": [ + "src/openai/resources/webhooks/__init__.py", + "src/openai/resources/webhooks/webhooks.py", + "src/openai/types/webhooks/__init__.py", + "src/openai/types/webhooks/live_call_incoming_webhook_event.py", + "src/openai/types/webhooks/unwrap_webhook_event.py" + ], + "mechanism_and_preservation": "Typed runtime bindings, package exports, compatibility aliases and deprecation annotations generate the wrappers and imports. Existing webhook verification remains handwritten; event import paths and deprecated class names remain available without changing verification behavior.", + "candidate_status": "Generated checkpoint and integrated candidate match exactly; public delivery pending." + }, + { + "area": "Existing compatibility type declarations", + "files": [ + "src/openai/types/decision_input_image_param.py", + "src/openai/types/decision_input_text_param.py", + "src/openai/types/fine_tuning/fine_tuning_job_integration.py", + "src/openai/types/responses/tool_param.py" + ], + "mechanism_and_preservation": "Typed compatibility declarations retain PromptCacheBreakpoint, the established fine-tuning module export shape and ParseableToolParam union membership. Generate these declarations directly rather than preserving handwritten additions in generated type modules.", + "candidate_status": "Generated checkpoint and integrated candidate match exactly; public delivery pending." + }, + { + "area": "Newly released Decisions constructor and union", + "files": [ + "src/openai/types/decision_input_message_param.py", + "src/openai/types/__init__.py" + ], + "mechanism_and_preservation": "Merged typed legacy TypedDict/union capability emits the released callable DecisionInputMessageParam subclass and separate DecisionInputMessageUnionParam export. Both generated modules are byte-identical to released public files; the two historical c551 residual patches are eliminated in b53.", + "candidate_status": "Generated checkpoint and integrated candidate match exactly; public delivery pending." + }, + { + "area": "API documentation", + "files": [ + "api.md", + "src/openai/resources/conversations/api.md", + "src/openai/resources/realtime/api.md", + "src/openai/resources/responses/api.md" + ], + "mechanism_and_preservation": "Canonical documentation rendering combines typed helper signatures, compatibility facts and external-resource references with the released schema. Regeneration removes stale generated documentation edits while retaining the public helper and resource documentation surface. Merged rendering restores all ten supported Videos/Assistants helper listings directly from typed signatures, deduplicates existing entries and resolves standalone relative links.", + "candidate_status": "Generated checkpoint and integrated candidate match exactly; public delivery pending." + }, + { + "area": "Realtime schema descriptions", + "files": [ + "src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py", + "src/openai/types/realtime/realtime_error_event.py" + ], + "mechanism_and_preservation": "Propagate the authoritative schema wording through ordinary model rendering, eliminating stale description edits without changing fields, event types or runtime behavior.", + "candidate_status": "Generated checkpoint and integrated candidate match exactly; public delivery pending." + } + ] + } } diff --git a/scripts/castiron/CUSTOM_CODE.md b/scripts/castiron/CUSTOM_CODE.md index 5ee61c8bf2..1809ea4788 100644 --- a/scripts/castiron/CUSTOM_CODE.md +++ b/scripts/castiron/CUSTOM_CODE.md @@ -9,6 +9,24 @@ and human review, and [AGENTS.md](../../AGENTS.md#custom-code-budget) for agent ## What is counted +Repositories may separately activate `"ratchet": "go"` in their policy after +this checker reaches trusted main. Activation must be a human-reviewed, +policy-only PR; SDK promotion does not carry policy edits. Keep the existing +`max_custom_patch_lines` ceiling. + +The Go ratchet rejects net growth in generated-owned custom patch additions +plus deletions **and physical lines of handwritten Go** compared with the +captured live target. It includes helpers, tests, comments, blank lines, nested +modules, and SDK-owned CI Go retained in private checkpoints. Non-text and +nonregular handwritten Go fail validation. Both revisions use their own verified +generated snapshots, read as Git objects without executing candidate code. + +Stale PRs compare with the live target, not their historical merge base. Merge +queues check the complete combined candidate. Every landed reduction becomes +the next baseline automatically. An activated ratchet cannot be disabled; +raising the independent patch-only ceiling does not restore retired headroom. +Existing policy isolation, human review, and snapshot ownership rules still apply. + The budget is **added lines plus deleted lines in the entire current custom patch** against the candidate's verified pure-generated snapshot. It is not the PR's diff against main, and additions and deletions never cancel. Replacing 500 generated @@ -188,14 +206,33 @@ PR targeting next; preserve the existing budget/schema fields: The SHA must belong to that same repository's authenticated main first-parent history and support this pin protocol. Selection reads the protected target -base, never the candidate. Updates require an exact-head human approving review -from a repository owner, member, or collaborator, in addition to ordinary -CODEOWNERS and required reviews. Bot reviews, review-body markers, stale reviews, -and dismissed approvals do not authorize activation. +base, never the candidate. Pin-only updates and initial pin adoption use ordinary +CODEOWNERS and required reviews when the pin does not roll back to an older main +checker, the effective pinned budget stays the same or decreases, and its schema, +strictness and ratchet mode stay unchanged. Initial adoption compares checker age +with authenticated bootstrap main; later updates compare with the prior pin. A +checker rollback, an effective pinned-budget increase or mode transition, a local numeric budget increase, or Go +ratchet activation still requires an exact-head human approving review from a +repository owner, member, or collaborator. Bot reviews, stale reviews and dismissed +approvals do not authorize those changes. + +For those human-required changes, the approving reviewer must explicitly attest that +they personally performed the human review. The entire review body must be the +following sentence, substituting the exact full current head SHA: + +```text +I personally reviewed and approve this policy change as a human: <40-character head SHA> +``` + +Blank reviews, ordinary code-review text, quoted attestations, and attestations for +an earlier head do not qualify. Agents must never submit this attestation, even +through a human account. This is an explicit reviewer attestation, not a claim that +GitHub account type or team membership proves who operated the account. Ordinary +SDK code reviews continue to accept automation under their normal repository gates. For initial adoption only, a base with no pin may accept an exact policy-only pin-initialization PR under authenticated current-main checker/policy, with the -same human review requirement. The proposed pin is validated but never executed +same effective-budget and mode review requirements. The proposed pin is validated but never executed for its own initialization. A malformed base pin cannot enter this path; other changes without a base pin fail closed. Ordinary pin updates execute under the previous approved version and cannot include SDK/tooling changes. @@ -215,3 +252,28 @@ immediately before publication. Dismissed, replaced, stale, automated or missing approvals fail closed even if ordinary automated approvals keep the queue live. The publisher consumes the selector's versioned review rules, with cross-runtime predicate fixtures; candidate artifacts never supply authorization or outcomes. + +### Review changes after publication + +The unprivileged reporter workflow signals submitted, edited and dismissed reviews. +For a current next PR changing the policy file, the trusted main handler marks its +required budget statuses and affected live queue statuses pending, then requests +normal reporter reruns. Only the ordinary authenticated evaluator may restore +success; the review signal never selects executable code, approval rules or an +arbitrary status target. Fork runs with empty PR lists use authenticated commit +associations and open-PR lookup, retaining live head/merge binding. The handler +marks each authenticated snapshot pending before slow run pagination, then retries +discovery if the PR or queue changes. Pending statuses on superseded affected +SHAs cannot authorize a merge; observed successes stay revoked even if a later +lookup fails. Reporter reruns must resolve to this same PR under the canonical +main-preference rule, not merely share its head SHA. Missing or ambiguous reporter +runs remain pending. Status writers share a serialization group. + +Retain native required approving reviews and CODEOWNERS at merge time. Review-event +delivery and reevaluation are asynchronous; this mechanism is not an atomic human +approval check at merge. In particular, automated reviews using human accounts do +not become human policy authorization. Do not weaken native gates or treat this +handler as their replacement. A failed invalidation or rerun must be investigated; +missing reporter runs remain pending until the normal trusted run is requested. +Install and validate the review signal on the protected target before activating +next policy checks, including actual PR and queue revocation after dismissal. diff --git a/scripts/castiron/custom_code_budget.py b/scripts/castiron/custom_code_budget.py index 9ead30bc43..e1dd34e072 100755 --- a/scripts/castiron/custom_code_budget.py +++ b/scripts/castiron/custom_code_budget.py @@ -50,6 +50,10 @@ def strict_policy(repo: Path, revision: str) -> bool: return cast(int, read_policy(repo, revision)["schema_version"]) == 2 +def go_ratchet(repo: Path, revision: str) -> bool: + return cast(bool, read_policy(repo, revision).get("ratchet") == "go") + + MANIFEST = b"scripts/castiron/generated-files.json" # Accept the relocated tooling only from the verified checkpoint. Deploy this # reader before changing the producer so trusted-main checks can validate moves. @@ -266,6 +270,8 @@ def check_isolation(repo: Path, base: str, head: str) -> int | None: return None if strict_policy(repo, base) and not strict_policy(repo, head): raise ValueError("strict generated-file ownership cannot be downgraded") + if go_ratchet(repo, base) and not go_ratchet(repo, head): + raise ValueError("Go customization ratchet cannot be disabled") if paths != {POLICY.encode()}: raise ValueError(ISOLATION_MESSAGE) return proposed @@ -290,6 +296,40 @@ def outcome(state: str, description: str) -> dict[str, str]: return {"state": state, "description": description} +def go_customization(repo: Path, revision: str, measured: dict[str, Any]) -> dict[str, Any]: + """Count patches plus physical handwritten Go, using only verified Git objects.""" + added, removed = count_custom_lines(measured) + # Use the reporter's ownership view. Private snapshots also retain SDK-owned + # CI files excluded from that view; those Go files are still handwritten. + generated = report.tree_entries(repo, measured["after"]["commit"]) + handwritten = { + path: entry + for path, entry in report.tree_entries(repo, revision, include_workflows=True).items() + if path.endswith(b".go") and path not in generated + } + for path, entry in handwritten.items(): + if entry.mode not in {b"100644", b"100755"}: + raise ValueError(f"nonregular handwritten Go: {path!r}") + blobs = report.read_blobs(repo, handwritten) + files: list[dict[str, Any]] = [] + for path, entry in sorted(handwritten.items()): + content = blobs[entry.oid] + content.decode("utf-8") + if b"\0" in content: + raise ValueError(f"non-text handwritten Go: {path!r}") + lines = content.count(b"\n") + int(bool(content) and not content.endswith(b"\n")) + files.append({"path": path.decode("utf-8"), "lines": lines}) + handwritten_lines = sum(file["lines"] for file in files) + return { + "revision": revision, + "generated": measured["after"], + "patch_lines": added + removed, + "handwritten_lines": handwritten_lines, + "handwritten_files": files, + "total": added + removed + handwritten_lines, + } + + def evaluate( repo: Path, base: str, @@ -362,11 +402,38 @@ def evaluate( result["checked_limit"] = checked_limit state = "success" if total <= checked_limit else "failure" description = f"+{added} / -{removed} = {total} custom lines; limit {checked_limit}." + if any(go_ratchet(repo, revision) for revision in (policy, base, head)): + # The report's historical merge base is for presentation only. Measure + # the captured live target against its own hash-verified snapshot. + baseline, _ = report.build_report( + repo, + base, + base, + public=public, + fetch=fetch, + require_head_hash=True, + target_branch=target_branch, + ) + before = go_customization(repo, base, baseline) + after = go_customization(repo, head, measured) + delta = after["total"] - before["total"] + result["ratchet"] = {"kind": "go", "before": before, "after": after, "delta": delta} + description += ( + f" Go customization {before['total']} -> {after['total']} (delta {delta:+d})." + ) + if delta > 0: + state = "failure" + if go_ratchet(repo, base) and not go_ratchet(repo, head): + state = "failure" + description += " Go customization ratchet cannot be disabled." # Main's strict policy applies even if the candidate tries to remove it. # A proposed activation must itself satisfy the stricter contract. if strict_policy(repo, policy) or strict_policy(repo, base) or strict_policy(repo, head): violations = verify_generated( - repo, measured["after"]["commit"], head, previous=measured["before"]["commit"] + repo, + measured["after"]["commit"], + head, + previous=(measured["before"] or measured["adoption_baseline"])["commit"], ) result["strict_violations"] = violations if violations: diff --git a/scripts/castiron/custom_code_policy.py b/scripts/castiron/custom_code_policy.py index e2c1288f47..f4e8ad99bd 100755 --- a/scripts/castiron/custom_code_policy.py +++ b/scripts/castiron/custom_code_policy.py @@ -59,6 +59,10 @@ def read_policy(repo: Path, revision: str) -> dict[str, Any]: if type(version) is not int or version not in (1, 2): raise ValueError("unsupported budget schema_version") keys = {"schema_version", "max_custom_patch_lines"} + if "ratchet" in value: + keys.add("ratchet") + if value["ratchet"] != "go": + raise ValueError("ratchet must be go") if version == 2: keys.add("generated_files") if "next_policy" in value: @@ -133,10 +137,12 @@ class HumanReviewRules(TypedDict): version: int associations: list[str] negative_pattern: str + attestation_prefix: str HUMAN_REVIEW_RULES: HumanReviewRules = { - "version": 1, + "version": 2, + "attestation_prefix": "I personally reviewed and approve this policy change as a human: ", "associations": ["OWNER", "MEMBER", "COLLABORATOR"], "negative_pattern": ( r"automated[\s_:-]*(?:source[\s_:-]*)?review|source[- ]only review|" @@ -161,6 +167,7 @@ def has_human_approval(reviews: list[dict[str, Any]], author: str, head: str) -> and review.get("author_association") in HUMAN_REVIEW_RULES["associations"] and not review["user"]["login"].endswith("[bot]") and review["user"]["login"] != author + and (review.get("body") or "").strip() == HUMAN_REVIEW_RULES["attestation_prefix"] + head and not re.search( HUMAN_REVIEW_RULES["negative_pattern"], review.get("body") or "", re.IGNORECASE ) @@ -249,7 +256,8 @@ def select_policy( raise ValueError("next evaluation has no verified constituent PR") for candidate in candidates: candidate_policy = read_policy(repo, candidate) - candidate_base_policy = read_policy(repo, merge_base(repo, base, candidate)) + candidate_base = merge_base(repo, base, candidate) + candidate_base_policy = read_policy(repo, candidate_base) pin_changed = candidate_policy.get("next_policy") != candidate_base_policy.get( "next_policy" ) @@ -257,9 +265,50 @@ def select_policy( candidate_policy["max_custom_patch_lines"] > candidate_base_policy["max_custom_patch_lines"] ) - if pin_changed or budget_increased: - if changed_paths(repo, base, candidate) != {POLICY.encode()}: - raise ValueError("each next policy activation or increase must be policy-only") + ratchet_activated = ( + candidate_policy.get("ratchet") == "go" and candidate_base_policy.get("ratchet") != "go" + ) + if (pin_changed or budget_increased or ratchet_activated) and changed_paths( + repo, base, candidate + ) != {POLICY.encode()}: + raise ValueError("each next policy activation or increase must be policy-only") + pin_requires_human = False + if pin_changed: + proposed = read_policy( + repo, next_policy_sha(repo, repository, candidate, authenticated_main) + ) + previous = ( + read_policy( + repo, next_policy_sha(repo, repository, candidate_base, authenticated_main) + ) + if "next_policy" in candidate_base_policy + else candidate_base_policy + ) + # Bootstrap executes authenticated main; initial adoption must not silently + # replace it with older checker code, even when policy fields match. + previous_checker = ( + candidate_base_policy["next_policy"]["main_sha"] + if "next_policy" in candidate_base_policy + else authenticated_main + ) + rolled_back = bool( + report.git( + repo, + "rev-list", + "--first-parent", + f"{candidate_policy['next_policy']['main_sha']}..{previous_checker}", + ).strip() + ) + # Compare effective policies, not the candidate's unchanged local ceiling. + # Schema/strictness or ratchet transitions also retain human authorization. + pin_requires_human = ( + rolled_back + or proposed["max_custom_patch_lines"] > previous["max_custom_patch_lines"] + or proposed["schema_version"] != previous["schema_version"] + or proposed.get("generated_files") != previous.get("generated_files") + or proposed.get("ratchet") != previous.get("ratchet") + ) + if budget_increased or ratchet_activated or pin_requires_human: number = require_human_policy_review(repository, candidate) policy_reviews.append({"number": number, "head_sha": candidate}) return { diff --git a/scripts/castiron/custom_code_report.py b/scripts/castiron/custom_code_report.py index f9e22afb46..88c897bbf1 100755 --- a/scripts/castiron/custom_code_report.py +++ b/scripts/castiron/custom_code_report.py @@ -48,6 +48,7 @@ "newly_customized": "Newly customized", "removed": "Customization removed", "existing_changed": "Existing customization changed", + "first_adoption": "Current customization; legacy baseline unavailable", "baseline_changed": "Generated baseline changed", "unchanged": "Existing customization unchanged", "no_longer_generated": "No longer generated", @@ -201,6 +202,14 @@ def public_metadata(repo: Path, commit: str) -> dict[str, str]: if not fields["Branch"].startswith("codegen/"): raise ReportError("invalid public codegen branch") git(repo, "check-ref-format", f"refs/heads/{fields['Branch']}") + for key in ("Adoption-Base", "Adoption-Source"): + values = re.findall(rf"^Castiron-Public-Codegen-{key}: (.+)$", message, re.MULTILINE) + if values: + if len(values) != 1: + raise ReportError("duplicate adoption metadata") + fields[key] = require_sha(values[0]) + if ("Adoption-Base" in fields) != ("Adoption-Source" in fields): + raise ReportError("incomplete adoption metadata") return fields @@ -392,6 +401,7 @@ def create_public_snapshot( content_hash: str, branch: str, parent: str | None, + adoption: tuple[str, str] | None = None, ) -> str: target = ( "next" @@ -406,6 +416,11 @@ def create_public_snapshot( f"Castiron-Public-Codegen-Branch: {branch}\n" f"Castiron-Public-Codegen-Target: {target}\n" ) + if adoption is not None: + if target != "next": + raise ReportError("adoption snapshot must target next") + message += f"Castiron-Public-Codegen-Adoption-Base: {require_sha(adoption[0])}\n" + message += f"Castiron-Public-Codegen-Adoption-Source: {require_sha(adoption[1])}\n" parents = ["-p", require_sha(parent)] if parent is not None else [] snapshot = ( git( @@ -429,6 +444,124 @@ def create_public_snapshot( return snapshot +def first_adoption_source(repo: Path, base: str, *, target_branch: str, fetch: bool) -> str: + """Authority is the protected target base policy, never a candidate receipt.""" + base = require_sha(base) + if target_branch != "next": + raise ReportError("first adoption is next-only") + if git(repo, "ls-tree", base, "--", ".castiron.stats.yml"): + raise ReportError("existing/corrupt Castiron stats require ordinary recovery") + if git(repo, "log", "--full-history", "--format=%H", base, "--", ".castiron.stats.yml").strip(): + raise ReportError("deleted Castiron stats cannot authorize first adoption") + if not git(repo, "ls-tree", base, "--", ".stats.yml").startswith(b"100644 blob "): + raise ReportError("first adoption requires a legacy SDK target") + if not git(repo, "ls-tree", base, "--", ".castiron-ratchet.json").startswith(b"100644 blob "): + raise ReportError("first adoption requires an approved target policy") + + def unique(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ReportError("duplicate policy field") + result[key] = value + return result + + policy = json.loads( + git(repo, "show", f"{base}:.castiron-ratchet.json"), object_pairs_hook=unique + ) + pin = policy.get("next_policy") + if not isinstance(pin, dict) or set(cast(dict[str, Any], pin)) != {"repository", "main_sha"}: + raise ReportError("first adoption requires the protected next-policy pin") + pin = cast(dict[str, Any], pin) + repository = pin["repository"] + 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", + } + if remote not in allowed: + raise ReportError("adoption policy repository does not match SDK origin") + source = require_sha(pin["main_sha"]) + main_ref = "refs/castiron/adoption-main" if fetch else "refs/remotes/origin/main" + if fetch: + git( + repo, + "fetch", + "--quiet", + "--no-tags", + f"https://github.com/{repository}.git", + f"+refs/heads/main:{main_ref}", + ) + if source not in git(repo, "rev-list", "--first-parent", main_ref).decode().splitlines(): + raise ReportError("adoption source is not on authenticated main history") + source_policy = json.loads( + git(repo, "show", f"{source}:.castiron-ratchet.json"), object_pairs_hook=unique + ) + if {key: value for key, value in policy.items() if key != "next_policy"} != source_policy: + raise ReportError("first adoption must preserve approved policy limits") + if not read_stats(repo, source).get("public_codegen_sha"): + raise ReportError( + "approved main must have a genuine public checkpoint before first adoption" + ) + return source + + +def adoption_head( + repo: Path, + base: str, + source: str, + head: str, + after: dict[str, Any], + *, + public: bool, +) -> None: + git(repo, "merge-base", "--is-ancestor", base, head) + if not public: + # Private initialization retains the authenticated source parent. Normal + # promotion deliberately publishes a single-parent target-branch commit. + git(repo, "merge-base", "--is-ancestor", source, head) + if git(repo, "show", f"{base}:.castiron-ratchet.json") != git( + repo, "show", f"{head}:.castiron-ratchet.json" + ): + raise ReportError("first adoption must preserve protected target policy") + original, current = read_stats(repo, source), read_stats(repo, head) + if original["generation_id"] == current["generation_id"]: + raise ReportError("first adoption requires a new generation") + for key in ("config_hash", "openapi_spec_hash"): + if not original.get(key) or current.get(key) != original[key]: + raise ReportError("first adoption must preserve authenticated main inputs") + if public: + # Public authorization comes from the protected pin and verified public + # snapshot ancestry, never from a candidate-supplied adoption receipt. + git(repo, "merge-base", "--is-ancestor", original["public_codegen_sha"], after["commit"]) + metadata = public_metadata(repo, after["commit"]) + if ( + metadata.get("Target") != "next" + or metadata.get("Adoption-Base") != base + or metadata.get("Adoption-Source") != source + ): + raise ReportError("snapshot does not bind the authorized first adoption") + else: + message = git(repo, "show", "-s", "--format=%B", current["codegen_sha"]).decode() + bindings = re.findall(r"^Generation metadata: ([A-Za-z0-9+/=]+)$", message, re.MULTILINE) + if len(bindings) != 1: + raise ReportError("invalid adoption generation binding") + envelope = json.loads(base64.b64decode(bindings[0], validate=True)) + record = envelope.get("record", {}) + if envelope.get("release_target") != "next" or record.get("public_baseline_commit") != base: + raise ReportError("new checkpoint must bind the old target base") + pin = json.loads(git(repo, "show", f"{base}:.castiron-ratchet.json"))["next_policy"] + sdk = pin["repository"].split("/")[1] + if record.get("target") != sdk or record.get("language") != sdk.removeprefix("openai-"): + raise ReportError("adoption generation SDK mismatch") + + def prepare_public_snapshot( source: Path, source_base: str, @@ -438,15 +571,25 @@ def prepare_public_snapshot( branch: str, ) -> dict[str, Any]: """Prepare public-only generated history; never push anything.""" - read_stats(source, source_base) after = read_stats(source, source_head) - public = read_stats(destination, public_base) target_branch = ( "next" if branch == "castiron/next" or branch.startswith("castiron/next/") else "main" ) + adoption_source = None + if not git(destination, "ls-tree", public_base, "--", ".castiron.stats.yml"): + adoption_source = first_adoption_source( + destination, public_base, target_branch=target_branch, fetch=True + ) + else: + read_stats(source, source_base) + public = read_stats(destination, adoption_source or public_base) baseline = resolve_checkpoint( source, after, fetch=True, require_hash=True, target_branch=target_branch ) + if adoption_source: + # Public source commit is already fetched into the private promotion clone. + adoption_head(source, public_base, adoption_source, source_head, baseline, public=False) + codegen_branch = f"codegen/{branch}" ref = f"refs/heads/{codegen_branch}" git(destination, "check-ref-format", ref) @@ -477,7 +620,11 @@ def prepare_public_snapshot( base_snapshot = public.get("public_codegen_sha") if base_snapshot: base_snapshot = resolve_public_baseline( - destination, public_base, fetch=True, require_hash=False, target_branch=target_branch + destination, + adoption_source or public_base, + fetch=True, + require_hash=False, + target_branch="main" if adoption_source else target_branch, )["commit"] elif previous: try: @@ -515,6 +662,13 @@ def prepare_public_snapshot( if ( not upgrade_legacy_parent and metadata["Generation-ID"] == after["generation_id"] + and ( + not adoption_source + or ( + metadata.get("Adoption-Base") == public_base + and metadata.get("Adoption-Source") == adoption_source + ) + ) and metadata["Hash"] == baseline["hash"] and git(destination, "rev-parse", f"{previous}^{{tree}}").decode().strip() == tree and base_snapshot in ancestors @@ -522,7 +676,13 @@ def prepare_public_snapshot( snapshot = previous else: snapshot = create_public_snapshot( - destination, tree, after["generation_id"], baseline["hash"], codegen_branch, parent + destination, + tree, + after["generation_id"], + baseline["hash"], + codegen_branch, + parent, + (public_base, adoption_source) if adoption_source else None, ) elif ( public_metadata(destination, base_snapshot)["Generation-ID"] == after["generation_id"] @@ -531,7 +691,13 @@ def prepare_public_snapshot( snapshot = base_snapshot else: snapshot = create_public_snapshot( - destination, tree, after["generation_id"], baseline["hash"], codegen_branch, parent + destination, + tree, + after["generation_id"], + baseline["hash"], + codegen_branch, + parent, + (public_base, adoption_source) if adoption_source else None, ) stats_path = destination / ".castiron.stats.yml" current = git(source, "show", f"{require_sha(source_head)}:.castiron.stats.yml").decode("utf-8") @@ -678,26 +844,53 @@ def build_report( ) -> tuple[dict[str, Any], bytes]: base, head = require_sha(base), require_sha(head) comparison_base = git(repo, "merge-base", base, head).decode().strip() + adoption_source = None + if not git(repo, "ls-tree", comparison_base, "--", ".castiron.stats.yml"): + if comparison_base != base: + raise ReportError( + "first-adoption target advanced; regenerate against the protected base" + ) + adoption_source = first_adoption_source( + repo, comparison_base, target_branch=target_branch, fetch=fetch + ) if public: after = resolve_public_baseline( - repo, head, fetch=fetch, require_hash=require_head_hash, target_branch=target_branch + repo, + head, + fetch=fetch, + require_hash=require_head_hash, + target_branch=target_branch, ) before = resolve_public_baseline( repo, - comparison_base, + adoption_source or comparison_base, fetch=fetch, require_hash=False, lineage=after["commit"], - target_branch=target_branch, + target_branch="main" if adoption_source else target_branch, ) else: before = resolve_baseline( - repo, comparison_base, fetch=fetch, require_hash=False, target_branch=target_branch + repo, + adoption_source or comparison_base, + fetch=fetch, + require_hash=False, + target_branch="main" if adoption_source else target_branch, ) after = resolve_baseline( - repo, head, fetch=fetch, require_hash=require_head_hash, target_branch=target_branch + repo, + head, + fetch=fetch, + require_hash=require_head_hash, + target_branch=target_branch, ) - g0, i0 = tree_entries(repo, before["commit"]), tree_entries(repo, comparison_base) + if adoption_source: + adoption_head(repo, comparison_base, adoption_source, head, after, public=public) + g0, i0 = ( + ({}, {}) + if adoption_source + else (tree_entries(repo, before["commit"]), tree_entries(repo, comparison_base)) + ) g1, i1 = tree_entries(repo, after["commit"]), tree_entries(repo, head) stats = numstats(repo, after["commit"], head) files: list[dict[str, Any]] = [] @@ -712,8 +905,12 @@ def build_report( files.append( { "path": path.decode("utf-8"), - "category": category, - "custom_before": custom_before, + "category": ( + ("first_adoption" if custom_after else "unchanged") + if adoption_source + else category + ), + "custom_before": None if adoption_source else custom_before, "custom_after": custom_after, "added": added, "removed": removed, @@ -745,11 +942,14 @@ def build_report( "target_base_sha": base, "base_sha": comparison_base, "head_sha": head, - "before": before, + "before": None if adoption_source else before, + "adoption_baseline": before if adoption_source else None, + "adoption_source": adoption_source, + "before_status": "legacy_generation_unavailable" if adoption_source else "verified", "after": after, "files": files, "counts": { - "before": sum(f["custom_before"] for f in files), + "before": None if adoption_source else sum(f["custom_before"] for f in files), "after": sum(f["custom_after"] for f in files), **dict(collections.Counter(f["category"] for f in files)), }, @@ -794,7 +994,15 @@ def render_report( and file["category"] != "no_longer_generated" for file in files ) - if newly_customized == 0: + if report.get("before_status") == "legacy_generation_unavailable": + lines.extend( + [ + "Legacy before-generation unavailable; no historical customization claim.", + "", + f"Current mixed files: {after}. Approved main reference: `{require_sha(report['adoption_source'])}`.", + ] + ) + elif newly_customized == 0: remaining = ( f"{after} mixed file{'s' if after != 1 else ''} remain{'s' if after == 1 else ''}" ) @@ -822,8 +1030,16 @@ def render_report( ), ] ) - lines.extend(["", f"Compared `{base[:12]}` → `{head[:12]}`. Generated baselines verified."]) - if not report["before"]["hash_recorded"] or not report["after"]["hash_recorded"]: + verification = ( + "Current generation and approved main reference verified." + if report["before"] is None + else "Generated baselines verified." + ) + lines.extend(["", f"Compared `{base[:12]}` → `{head[:12]}`. {verification}"]) + if ( + not (report["before"] or report["adoption_baseline"])["hash_recorded"] + or not report["after"]["hash_recorded"] + ): lines.extend( [ "", @@ -1175,6 +1391,10 @@ def main() -> int: trusted.add_argument("--run-attempt", type=int, required=True) trusted.add_argument("--out", type=Path, required=True) trusted.add_argument("--base", required=True, help="immutable main SHA of the trusted checkout") + selecting = commands.add_parser("promotion-base") + selecting.add_argument("--repo", type=Path, required=True) + selecting.add_argument("--base", required=True) + selecting.add_argument("--branch", required=True) preparing = commands.add_parser("prepare-public") preparing.add_argument("--source-repo", type=Path, required=True) preparing.add_argument("--source-base", required=True) @@ -1194,6 +1414,16 @@ def main() -> int: try: if args.command == "hash": sys.stdout.write(hash_codegen_commit(args.repo, args.commit) + "\n") + elif args.command == "promotion-base": + base = require_sha(args.base) + if not git(args.repo, "ls-tree", base, "--", ".castiron.stats.yml"): + target = ( + "next" + if args.branch == "castiron/next" or args.branch.startswith("castiron/next/") + else "main" + ) + base = first_adoption_source(args.repo, base, target_branch=target, fetch=True) + sys.stdout.write(base + "\n") elif args.command == "prepare-public": sys.stdout.write( json.dumps( @@ -1210,7 +1440,12 @@ def main() -> int: ) elif args.command == "trusted-report": trusted_report( - args.repo, args.repository, args.run_id, args.run_attempt, args.out, base=args.base + args.repo, + args.repository, + args.run_id, + args.run_attempt, + args.out, + base=args.base, ) elif args.command == "comment": if args.report.stat().st_size > 5_000_000: diff --git a/scripts/castiron/fixtures/github_publisher.cjs b/scripts/castiron/fixtures/github_publisher.cjs index 0c4d7484bc..dc9106c378 100644 --- a/scripts/castiron/fixtures/github_publisher.cjs +++ b/scripts/castiron/fixtures/github_publisher.cjs @@ -6,8 +6,22 @@ const writes = []; const github = { rest: { actions: { - getWorkflowRun: async () => ({ data: data.run }), - getWorkflow: async () => ({ data: { id: 456, path: '.github/workflows/castiron-custom-code.yml' } }), + getWorkflowRun: async ({ run_id }) => { + if (data.run_errors?.[run_id]) + throw Object.assign(new Error('Workflow run lookup failed'), { status: data.run_errors[run_id] }); + return { data: data.runs_by_id?.[run_id] || data.run }; + }, + getWorkflow: async ({ workflow_id }) => ({ + data: { + id: workflow_id === 'castiron-custom-code-comment.yml' ? 789 : 456, + path: `.github/workflows/${workflow_id}`, + }, + }), + listWorkflowRuns: 'workflow-runs', + reRunWorkflow: async (value) => { + if (data.fail_rerun) throw new Error('Simulated dispatch failure'); + writes.push({ operation: 'rerun', ...value }); + }, }, pulls: { get: async ({ pull_number }) => ({ @@ -15,16 +29,31 @@ const github = { }), list: 'pulls', listReviews: 'reviews', + listFiles: 'files', }, git: { getRef: async ({ ref }) => ({ data: { - object: { sha: ref === 'heads/main' && data.main_sha ? data.main_sha : data.current.base.sha }, + object: { + sha: ref.startsWith('pull/') + ? data.merge_sha + : ref === 'heads/main' && data.main_sha + ? data.main_sha + : data.current.base.sha, + }, }, }), }, repos: { - createCommitStatus: async (value) => writes.push(value), + getCommit: async () => ({ data: data.merge_commit }), + createCommitStatus: async (value) => { + writes.push(value); + data.statuses_by_sha ||= {}; + data.statuses_by_sha[value.sha] = [ + { ...value, creator: { login: 'github-actions[bot]' } }, + ...(data.statuses_by_sha[value.sha] || data.previous_statuses || []), + ]; + }, listCommitStatusesForRef: 'statuses', listPullRequestsAssociatedWithCommit: 'associations', }, @@ -34,15 +63,31 @@ const github = { updateComment: async (value) => writes.push({ operation: 'update', ...value }), }, }, - graphql: async (_query, { branch, cursor }) => { - if (branch !== data.current.base.ref) throw new Error('Wrong queue target'); + graphql: async (_query, { branch, cursor, number }) => { + if (_query.includes('reviews(first:100')) { + const page = data.review_pages?.shift(); + if (page && page.expected_cursor !== cursor) throw new Error('Wrong review cursor'); + return { + repository: { + pullRequest: { + reviews: page || { + nodes: data.review_nodes_by_number?.[number] || data.review_nodes || [], + pageInfo: { hasNextPage: false, endCursor: null }, + }, + }, + }, + }; + } + if (data.forbid_queue) throw new Error('Unexpected queue discovery'); + if ((branch || 'next') !== data.current.base.ref) throw new Error('Wrong queue target'); const page = data.queue_pages?.shift(); if (page && page.expected_cursor !== cursor) throw new Error('Wrong queue cursor'); return { repository: { mergeQueue: { entries: page || { - nodes: data.queue_absent ? [] : [{ headCommit: { oid: data.run.head_sha } }], + nodes: + data.queue_nodes || (data.queue_absent ? [] : [{ headCommit: { oid: data.run.head_sha } }]), pageInfo: { hasNextPage: false, endCursor: null }, }, }, @@ -50,10 +95,12 @@ const github = { }; }, paginate: async (method, params) => { + if (method === 'files') return data.files || [{ filename: '.castiron-ratchet.json' }]; + if (method === 'workflow-runs') return data.runs_by_sha?.[params.head_sha] || []; if (method === 'reviews') return data.reviews_by_number?.[params.pull_number] || data.reviews || []; - if (method === 'statuses') return data.previous_statuses || []; + if (method === 'statuses') return data.statuses_by_sha?.[params.ref] || data.previous_statuses || []; if (method === 'comments') return data.comments || []; - if (method === 'associations') return []; + if (method === 'associations') return data.associated_pulls || []; if (method === 'pulls' && params.head === 'contributor:sdk' && params.state === 'open') { return data.fallback_pulls || []; } @@ -61,9 +108,15 @@ const github = { }, }; const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; -new AsyncFunction('github', 'context', 'process', data.script)(github, data.context, { env: data.env || {} }) +new AsyncFunction('github', 'context', 'process', 'require', data.script)( + github, + data.context, + { env: data.env || {} }, + require, +) .then(() => process.stdout.write(JSON.stringify(writes))) .catch((error) => { + if (data.capture_writes) fs.writeFileSync(data.capture_writes, JSON.stringify(writes)); console.error(error); process.exitCode = 1; }); diff --git a/scripts/castiron/generated-files.json b/scripts/castiron/generated-files.json index 2f6bc33b6c..6e37e91568 100644 --- a/scripts/castiron/generated-files.json +++ b/scripts/castiron/generated-files.json @@ -11,11 +11,11 @@ }, "api.md": { "mode": "100644", - "sha256": "d89180163de347db8284b9c5dba87c230cfbdc2312985239ac4d3a054b72eaa3" + "sha256": "f58148943cc440a8f82d8b78a88ce875a4826cf9ce68c9c8894af806337cb66e" }, "scripts/castiron/CUSTOM_CODE.md": { "mode": "100644", - "sha256": "7f8812395e9ff0022e82ea84d56f0db5e6901ea7c429e69e4d5f13366c7fb2b4" + "sha256": "c80c8e834f55f3cfbf690f6bffb70d7b0a10317261a407b46686d810e07f6a61" }, "scripts/castiron/README.md": { "mode": "100644", @@ -23,11 +23,11 @@ }, "scripts/castiron/custom_code_budget.py": { "mode": "100755", - "sha256": "ba44999ef44cd80cf1afb9db4013c29969013d6fc8cf78f81220752175d3a539" + "sha256": "3c96300546afb5e57fbfb596d4e1bde90821928e44904c4aeb91e2d7e4e27ff5" }, "scripts/castiron/custom_code_policy.py": { "mode": "100755", - "sha256": "c9c37ea2de771502bc806370362ebf5486e950e50f6fc11ec90b85eb48802c08" + "sha256": "f2d21cf2f66b1907ea177930270b6f161951b67af59bc34724f62743d6561a67" }, "scripts/castiron/custom_code_report.py": { "mode": "100755", @@ -47,7 +47,7 @@ }, "scripts/castiron/test_custom_code_budget.py": { "mode": "100644", - "sha256": "71527f2e594989bb3f7f94a723322714bf9bd2669aa861719f29f462b925a4b6" + "sha256": "feaf35f7f30deecb89d95b31b8a325033180c64650e1d965cbdee486ab11f16d" }, "scripts/castiron/test_custom_code_github.py": { "mode": "100644", @@ -55,7 +55,7 @@ }, "scripts/castiron/test_custom_code_policy.py": { "mode": "100644", - "sha256": "ce6516a7121a494d9e108de2ceddf3344e3835926d70f256c78d25d6c7825e72" + "sha256": "eb49e1d2f55ea369010c1ea1e074d822de687abf439bdc8b4c2c29e7ce38f4db" }, "scripts/castiron/test_custom_code_publication.py": { "mode": "100644", @@ -63,15 +63,15 @@ }, "scripts/castiron/test_custom_code_report.py": { "mode": "100644", - "sha256": "4954ba9306286802302aa1ce7b787e272d9d033ffb5d85012908e13a494c58ef" + "sha256": "f4d90fd38b17aac59096488391b65ef9da5493d2049d4c038d5926a960257ca7" }, "src/openai/__init__.py": { "mode": "100644", - "sha256": "6b4376340c25a5a435e1599a5efdf7f7550b2e7c7b4a836e79da5179a5ecd5d9" + "sha256": "a410d257a94f2c4f6cd0951a178131e8d883e21a69ef6bea245271db215ba196" }, "src/openai/_client.py": { "mode": "100644", - "sha256": "4169b69ca58d5d53bfeb0f9aa8c230bd6893973d6710d6b9badc65e089e4b055" + "sha256": "616e33f35af68a15cd312d9dc68414f41080a7c6275aa61f56cc1c34dad4fa29" }, "src/openai/_data_residency.py": { "mode": "100644", @@ -267,11 +267,11 @@ }, "src/openai/resources/audio/transcriptions.py": { "mode": "100644", - "sha256": "fe8ead64db9643bfb90db794e5cfadda03120eab49210ace7086deef6f1fbb39" + "sha256": "8bfaa74f7b00a61984e2ee801357dbf29fdda7f7388d245df7458f0c20820ea9" }, "src/openai/resources/audio/translations.py": { "mode": "100644", - "sha256": "a2c33a084d2af8fcf46644f536cb6fc5855490b96d3dd37756d7653e2cca2ac6" + "sha256": "e9e2fc5ac27461366225c8e6ca352a8db67b789c816bb0074b21c078e99afc05" }, "src/openai/resources/audio/voice_consents.py": { "mode": "100644", @@ -331,7 +331,7 @@ }, "src/openai/resources/beta/agents/sessions/sessions.py": { "mode": "100644", - "sha256": "6b3c6da39e9b5b091f7189f10e850489f3eac854eeb8e7489a6016c61f06a765" + "sha256": "53987b64fb0b775e755a727f91d4486228dc2735947a3935e0073ca132c5a612" }, "src/openai/resources/beta/agents/sessions/subagents/__init__.py": { "mode": "100644", @@ -423,7 +423,7 @@ }, "src/openai/resources/beta/responses/responses.py": { "mode": "100644", - "sha256": "e02479e8641d6e5cbf70086381400eb9f96195fe3fbc08e2ba0466e08315079f" + "sha256": "aca7b656abd01d90bc23030690562738aaa0921bb33de8016e379e6aec34d294" }, "src/openai/resources/beta/threads/__init__.py": { "mode": "100644", @@ -439,7 +439,7 @@ }, "src/openai/resources/beta/threads/runs/runs.py": { "mode": "100644", - "sha256": "7ba7720d81a72c95a377b62b6454954f9259278b3a18f430f1ae039e729a4288" + "sha256": "a430fe262b62d81ac6e1d8161e2e4d23a16c01c4bcdcc98a900392db5327d97c" }, "src/openai/resources/beta/threads/runs/steps.py": { "mode": "100644", @@ -447,7 +447,7 @@ }, "src/openai/resources/beta/threads/threads.py": { "mode": "100644", - "sha256": "d0b5ae764f84c0933d098fb68d6038f52390630fd0f134f0140e3d7027a96b35" + "sha256": "1a49dcdeb83ce84b16160b2e17cc4cbfee76c8f93267fe22a727bc420dd92c3c" }, "src/openai/resources/chat/__init__.py": { "mode": "100644", @@ -463,7 +463,7 @@ }, "src/openai/resources/chat/completions/completions.py": { "mode": "100644", - "sha256": "398f77d7d2552b44400cd954ed233cee861839d68e4ab73a95574d2398a07a22" + "sha256": "0a0d3dfc38a12dff71afba526d745792f33db3e720c04ba755bb966c726f8a16" }, "src/openai/resources/chat/completions/messages.py": { "mode": "100644", @@ -519,7 +519,7 @@ }, "src/openai/resources/embeddings.py": { "mode": "100644", - "sha256": "871fd8d2332be2661c114e8fd135543000f0f3ba586faae742af16be5570bb34" + "sha256": "88aed80308e7ca8280fa29ebbf093a533f4dc327a902ace6467c5b8f52e4b387" }, "src/openai/resources/evals/__init__.py": { "mode": "100644", @@ -643,7 +643,7 @@ }, "src/openai/resources/realtime/realtime.py": { "mode": "100644", - "sha256": "a4b57db70118e3d9c468c8f7528a6b4db979fc3b0d5665ef47d7622998f509ca" + "sha256": "0285ac5edb9b8b17f296b2d513068cb35c3413895e35424221765cc4a15c8592" }, "src/openai/resources/realtime/translations/__init__.py": { "mode": "100644", @@ -655,7 +655,7 @@ }, "src/openai/resources/realtime/translations/translations.py": { "mode": "100644", - "sha256": "6d8eb9eb1962bd24af7eab0273ed01be77d31012a34c4b99f0f6f381253cb475" + "sha256": "805923247278589582e4d4db869aa55f37a18130178d6c467f1c59b67825259c" }, "src/openai/resources/responses/__init__.py": { "mode": "100644", @@ -663,7 +663,7 @@ }, "src/openai/resources/responses/api.md": { "mode": "100644", - "sha256": "5e100d14c416564c1c1ba4e9e0d9dbd3e33fab271405473ba77038f645aeb86e" + "sha256": "840e8dc7c7440209ec84924cc82fa028934956e63dbbea5059885d90812da2a3" }, "src/openai/resources/responses/input_items.py": { "mode": "100644", @@ -675,7 +675,7 @@ }, "src/openai/resources/responses/responses.py": { "mode": "100644", - "sha256": "730d34b7ec479169c91f15d021b3c3f03de91c52fb4984cc4058b0bc60b9be2b" + "sha256": "0fd253db414975a83148d3ff07b4e5c9c5329a1a6c33c4f2ca99294ef286d7ec" }, "src/openai/resources/safety/__init__.py": { "mode": "100644", @@ -751,11 +751,11 @@ }, "src/openai/resources/webhooks/__init__.py": { "mode": "100644", - "sha256": "482d8a3740c763b8206f00d09f0fdd5178089ab3beb57d2d3db0c3cc5cc45b65" + "sha256": "c1e56f4b97470de30bd95fc65ab4bc5f6c3f2ea37224c3ce4e6dd6db9e2bbb3c" }, "src/openai/resources/webhooks/api.md": { "mode": "100644", - "sha256": "8f4e49369b4f9cce4968bc67402ffa2b107b0cfb57b8fd81041119e82b8d1ed4" + "sha256": "c9525180e201503bb8a958a2768a08b26655106c716569e8f40bb9f15349c365" }, "src/openai/resources/webhooks/event_types.py": { "mode": "100644", @@ -763,11 +763,11 @@ }, "src/openai/resources/webhooks/webhooks.py": { "mode": "100644", - "sha256": "d5214ec6cfdae16f687397dac4e90222c52ee33f4edd209c1f6e614c43014be8" + "sha256": "f730653c5937f4c723f5519c7e86823fcba0ee088c270ecbd8e02fd10e49c654" }, "src/openai/types/__init__.py": { "mode": "100644", - "sha256": "75443908fc973e8fd3bdb80f814ce3496e3da89ae7cacef4311235716a57e4da" + "sha256": "98f15c4ee123816edfbbd0b758a67ca30b80f80939d158e8bed9a1b518d7c6ae" }, "src/openai/types/admin/__init__.py": { "mode": "100644", @@ -4103,11 +4103,11 @@ }, "src/openai/types/decision_input_image_param.py": { "mode": "100644", - "sha256": "ea10f5cc6ca0126d19b4f5a0b2acd06386d09fe44515ceda473fe920bdc0211d" + "sha256": "347c633b1312fb62ecceffc147d0a250954392a09c07fa680d585dd41a3f8c60" }, "src/openai/types/decision_input_message_param.py": { "mode": "100644", - "sha256": "e9ade7d34bef710df6793c812f0855896d1cafb0b835a69de0e0ae4370f6658f" + "sha256": "22aff1b989c87ee05e3041c1caa9f3dd8540cbb381d96281ee75237b508297aa" }, "src/openai/types/decision_input_part_union_param.py": { "mode": "100644", @@ -4115,7 +4115,7 @@ }, "src/openai/types/decision_input_text_param.py": { "mode": "100644", - "sha256": "96810356758e0082d22d20bc76e9dab61360ca37be3f2f9ad15ae01c473b470f" + "sha256": "416864cec231da63e0324268e57192de9902399cf0119d061467c6117a3090c7" }, "src/openai/types/decision_prompt_cache_breakpoint_param.py": { "mode": "100644", @@ -4359,7 +4359,7 @@ }, "src/openai/types/fine_tuning/fine_tuning_job_integration.py": { "mode": "100644", - "sha256": "01be0fb6a90b729e6d8c700539af334344baf908dfa30caec223651109cbf541" + "sha256": "add6f540e2493ce1c9e4b18c451669acd44443f84ffc364d03ba5299134c3f85" }, "src/openai/types/fine_tuning/fine_tuning_job_wandb_integration.py": { "mode": "100644", @@ -6683,7 +6683,7 @@ }, "src/openai/types/responses/tool_param.py": { "mode": "100644", - "sha256": "d548d6c62e21b015052324ce6fdb97cbbae290ff8dd4a87efbdcfca14815c339" + "sha256": "3c9a6d95a41c4870e190679eb6d1c9b7a16440bb32001a3954cbae6ab6e541f5" }, "src/openai/types/responses/tool_search_output_namespace_tool.py": { "mode": "100644", @@ -7119,7 +7119,7 @@ }, "src/openai/types/webhooks/__init__.py": { "mode": "100644", - "sha256": "6cd86f449fccb260a2e03c56defd59fbf890063734171c8a9fc0405cf9bb2b7d" + "sha256": "a5774cf8e193a6d9299039fa823adb236b2dbb5cce675ccc9efbed2a613b1aa2" }, "src/openai/types/webhooks/agent_environment_expired_webhook_event.py": { "mode": "100644", @@ -7203,7 +7203,7 @@ }, "src/openai/types/webhooks/live_call_incoming_webhook_event.py": { "mode": "100644", - "sha256": "98c0194038495649ea8f61edad0fffe95e1ce9b100ce6303b96a2ca37e7fab98" + "sha256": "2dd3bc70a9df1f12e5333983946e1df037a69f712ce104c75324768034df5c24" }, "src/openai/types/webhooks/live_transport_incoming_webhook_event.py": { "mode": "100644", @@ -7247,7 +7247,7 @@ }, "src/openai/types/webhooks/unwrap_webhook_event.py": { "mode": "100644", - "sha256": "ab0f7c3a5b26ee9df72b53f7084da93dd86e7e38ce25efe13fbcfe9dfcbe4c69" + "sha256": "f1285c95b535b806614b4da2b35607fb1c8759dd23520d3b79c2506ef1e69a70" }, "src/openai/types/webhooks/webhook_create_params.py": { "mode": "100644", diff --git a/scripts/castiron/test_custom_code_budget.py b/scripts/castiron/test_custom_code_budget.py index 602ade2c82..b35e8fad44 100644 --- a/scripts/castiron/test_custom_code_budget.py +++ b/scripts/castiron/test_custom_code_budget.py @@ -16,6 +16,152 @@ from custom_code_test_support import GitTestCase +class GoRatchetTests(GitTestCase): + def prepare(self, *, inherited_ci: bool = False) -> str: + if inherited_ci: + self.write(".github/actions/check/main.go", "package main\n") + self.baseline() + self.policy() + return self.commit("activate Go ratchet") + + def policy(self, *, active: bool = True, limit: int = 2000) -> None: + value: dict[str, Any] = {"schema_version": 1, "max_custom_patch_lines": limit} + if active: + value["ratchet"] = "go" + self.write(budget.POLICY, json.dumps(value) + "\n") + + def evaluate(self, base: str, head: str, **kwargs: Any) -> dict[str, Any]: + return budget.evaluate(self.repo, base, head, public=False, **kwargs)[0] + + def test_handwritten_helpers_tests_comments_and_nested_modules_count(self) -> None: + base = self.prepare() + for path in ( + "helper.go", + "helper_test.go", + "nested/module/helper.go", + ".github/workflows/check.go", + ): + with self.subTest(path=path): + self.git("checkout", "--detach", "-q", base) + self.write(path, "// comment\n\npackage example\n// final line") + result = self.evaluate(base, self.commit()) + self.assertEqual(result["checks"]["budget"]["state"], "failure") + self.assertEqual(result["ratchet"]["delta"], 4) + self.assertEqual( + result["ratchet"]["after"]["handwritten_files"], [{"path": path, "lines": 4}] + ) + + def test_inherited_private_checkpoint_ci_is_handwritten(self) -> None: + base = self.prepare(inherited_ci=True) + self.write(".github/actions/check/main.go", "package main\n// added\n") + result = self.evaluate(base, self.commit()) + self.assertEqual(result["total"], 0) + self.assertEqual(result["ratchet"]["before"]["handwritten_lines"], 1) + self.assertEqual(result["ratchet"]["delta"], 1) + self.assertEqual( + result["ratchet"]["after"]["handwritten_files"], + [{"path": ".github/actions/check/main.go", "lines": 2}], + ) + self.assertEqual(result["checks"]["budget"]["state"], "failure") + + def test_patch_reduction_can_pay_for_handwritten_lines(self) -> None: + self.prepare() + self.write("generated.py", "generated\ncustom\ncustom\n") + base = self.commit() + self.write("generated.py", "generated\n") + self.write("helper.go", "package example\n") + result = self.evaluate(base, self.commit()) + self.assertEqual(result["ratchet"]["delta"], -1) + self.assertEqual(result["checks"]["budget"]["state"], "success") + + def test_stale_branch_cannot_reclaim_landed_reduction(self) -> None: + self.prepare() + self.write("helper.go", "package example\n// old allowance\n") + old = self.commit() + self.git("checkout", "-q", "-b", "stale") + head = self.commit() + self.git("checkout", "-q", "main") + self.write("helper.go", "package example\n") + base = self.commit() + self.assertEqual(self.git("merge-base", base, head), old) + result = self.evaluate(base, head) + self.assertEqual(result["ratchet"]["before"]["revision"], base) + self.assertEqual(result["ratchet"]["delta"], 1) + self.assertEqual(result["checks"]["budget"]["state"], "failure") + + def test_complete_merge_queue_candidate_is_counted(self) -> None: + base = self.prepare() + constituent = self.commit("empty constituent") + self.write("helper.go", "package example\n") + result = self.evaluate(base, self.commit("combined queue"), pull_heads=[constituent]) + self.assertEqual(result["checks"]["isolation"]["state"], "success") + self.assertEqual(result["checks"]["budget"]["state"], "failure") + + def test_ratchet_cannot_be_disabled(self) -> None: + base = self.prepare() + self.policy(active=False) + result = self.evaluate(base, self.commit()) + for name in ("budget", "isolation"): + self.assertEqual(result["checks"][name]["state"], "failure") + self.assertIn("cannot be disabled", result["checks"][name]["description"]) + + def test_old_ceiling_increase_cannot_restore_ratchet_headroom(self) -> None: + self.prepare() + self.policy(limit=3000) + base = self.commit("separately reviewed ceiling increase") + self.write("helper.go", "package example\n") + result = self.evaluate(base, self.commit()) + self.assertEqual(result["limit"], 3000) + self.assertEqual(result["checks"]["budget"]["state"], "failure") + + def test_fixed_patch_ceiling_remains_independent(self) -> None: + self.prepare() + self.policy(limit=1) + self.write("helper.go", "package example\n// two\n// three\n") + base = self.commit() + (self.repo / "helper.go").unlink() + self.write("generated.py", "generated\ncustom\ncustom\n") + result = self.evaluate(base, self.commit()) + self.assertEqual(result["ratchet"]["delta"], -1) + self.assertEqual(result["checks"]["budget"]["state"], "failure") + + def test_nontext_and_symlink_go_fail_closed(self) -> None: + base = self.prepare() + for content in (b"package example\0", b"\xff", None): + with self.subTest(content=content): + self.git("checkout", "--detach", "-q", base) + path = self.repo / "helper.go" + if content is None: + path.symlink_to("generated.py") + else: + path.write_bytes(content) + result = self.evaluate(base, self.commit()) + self.assertEqual(result["checks"]["budget"]["state"], "failure") + + def test_live_target_snapshot_hash_is_verified(self) -> None: + self.prepare() + self.git("checkout", "-q", "-b", "candidate") + head = self.commit() + self.git("checkout", "-q", "main") + stats = self.repo / ".castiron.stats.yml" + stats.write_text(stats.read_text().replace("codegen_hash: ", "codegen_hash: bad")) + result = self.evaluate(self.commit(), head) + self.assertEqual(result["checks"]["budget"]["state"], "failure") + + def test_activation_must_be_policy_only(self) -> None: + self.prepare() + self.policy(active=False) + base = self.commit() + self.policy() + head = self.commit() + result = self.evaluate(base, head) + self.assertTrue(all(check["state"] == "success" for check in result["checks"].values())) + self.write("helper.go", "package example\n") + result = self.evaluate(base, self.commit()) + self.assertEqual(result["checks"]["isolation"]["state"], "failure") + self.assertEqual(result["checks"]["budget"]["state"], "failure") + + class BudgetTests(GitTestCase): def setUp(self) -> None: # pyright: ignore[reportImplicitOverride] super().setUp() diff --git a/scripts/castiron/test_custom_code_policy.py b/scripts/castiron/test_custom_code_policy.py index 68c715440b..be49c70665 100644 --- a/scripts/castiron/test_custom_code_policy.py +++ b/scripts/castiron/test_custom_code_policy.py @@ -77,6 +77,7 @@ def approved(self, **kwargs: Any) -> dict[str, Any]: "commit_id": self.head, "user": {"type": "User", "login": "reviewer"}, "author_association": "MEMBER", + "body": selector.HUMAN_REVIEW_RULES["attestation_prefix"] + self.head, **kwargs, } @@ -136,21 +137,15 @@ def test_malformed_base_pin_cannot_bootstrap(self) -> None: with self.assertRaisesRegex(ValueError, "exactly"): self.select(base=base, head=self.base) - def test_bootstrap_requires_exact_policy_only_and_human(self) -> None: + def test_bootstrap_requires_exact_policy_only_with_native_review(self) -> None: self.head = self.base - with mock.patch.object( - selector.report, "api", side_effect=self.review_api([self.approved()]) - ): + with mock.patch.object(selector, "require_human_policy_review") as review: result = self.select(base=self.main) self.assertTrue(result["bootstrap"]) - self.assertEqual(result["policy_reviews"], [{"number": 3, "head_sha": self.head}]) + self.assertEqual(result["policy_reviews"], []) self.assertEqual(result["human_review_rules"], selector.HUMAN_REVIEW_RULES) self.assertEqual(result["policy_sha"], self.main) - with ( - mock.patch.object(selector.report, "api", side_effect=self.review_api([])), - self.assertRaisesRegex(ValueError, "human"), - ): - self.select(base=self.main) + review.assert_not_called() with self.assertRaisesRegex(ValueError, "policy-only"): self.select(base=self.main, head=self.git("rev-parse", "next")) @@ -164,15 +159,8 @@ def test_queue_bootstrap_isolated_per_constituent(self) -> None: result = self.select(base=self.main, head=group, pull_heads=[self.base, sdk_head]) self.assertTrue(result["bootstrap"]) self.assertEqual(result["policy_sha"], self.main) - self.assertEqual(result["policy_reviews"], [{"number": 3, "head_sha": self.base}]) - review.assert_called_once_with(REPO, self.base) - with ( - mock.patch.object( - selector, "require_human_policy_review", side_effect=ValueError("human required") - ), - self.assertRaisesRegex(ValueError, "human"), - ): - self.select(base=self.main, head=group, pull_heads=[self.base, sdk_head]) + self.assertEqual(result["policy_reviews"], []) + review.assert_not_called() for heads in (None, [group]): with self.subTest(heads=heads), self.assertRaisesRegex(ValueError, "policy-only"): self.select(base=self.main, head=group, pull_heads=heads) @@ -192,8 +180,8 @@ def test_queue_pin_update_isolated_per_constituent(self) -> None: with mock.patch.object(selector, "require_human_policy_review", return_value=3) as review: result = self.select(head=group, main=later, pull_heads=[pin_head, self.head]) self.assertEqual(result["policy_sha"], self.main) - self.assertEqual(result["policy_reviews"], [{"number": 3, "head_sha": pin_head}]) - review.assert_called_once_with(REPO, pin_head) + self.assertEqual(result["policy_reviews"], []) + review.assert_not_called() # A combined SDK/pin change in one PR is still forbidden. with self.assertRaisesRegex(ValueError, "policy-only"): self.select(head=group, main=later) @@ -227,22 +215,15 @@ def test_queue_stale_sdk_constituent_inherits_protected_pin(self) -> None: self.assertEqual(result["policy_reviews"], []) review.assert_not_called() - def test_stale_actual_pin_edit_still_requires_isolation_and_human(self) -> None: + def test_stale_actual_pin_edit_still_requires_isolation(self) -> None: later, base = self.advanced_pin_base() self.git("checkout", "-q", "--detach", self.base) self.write_policy({**self.policy, "next_policy": {"repository": REPO, "main_sha": later}}) edited = self.commit("PR independently edits pin") with mock.patch.object(selector, "require_human_policy_review", return_value=3) as review: result = self.select(base=base, head=edited, main=later) - self.assertEqual(result["policy_reviews"], [{"number": 3, "head_sha": edited}]) - review.assert_called_once_with(REPO, edited) - with ( - mock.patch.object( - selector, "require_human_policy_review", side_effect=ValueError("human required") - ), - self.assertRaisesRegex(ValueError, "human"), - ): - self.select(base=base, head=edited, main=later) + self.assertEqual(result["policy_reviews"], []) + review.assert_not_called() (self.repo / "mixed-sdk.txt").write_text("SDK change") mixed = self.commit("mix policy and SDK") with self.assertRaisesRegex(ValueError, "policy-only"): @@ -327,9 +308,20 @@ def test_selector_outputs_transport_review_receipt_as_json(self) -> None: self.assertEqual(json.loads(out.read_text())["schema_version"], 1) def test_forged_bot_stale_dismissed_untrusted_reviews_rejected(self) -> None: - self.head = self.base + self.git("checkout", "-q", "--detach", self.base) + self.write_policy( + { + **self.policy, + "next_policy": self.pin, + "max_custom_patch_lines": self.policy["max_custom_patch_lines"] + 1, + } + ) + self.head = self.commit("bootstrap pin with budget increase") cases = [ [], + [self.approved(body="")], + [self.approved(body="Looks good to me")], + [self.approved(body=selector.HUMAN_REVIEW_RULES["attestation_prefix"] + "f" * 40)], [self.approved(state="COMMENTED", body="APPROVED by a human")], [self.approved(user={"type": "Bot", "login": "reviewer[bot]"})], [self.approved(commit_id="a" * 40)], @@ -360,6 +352,28 @@ def test_forged_bot_stale_dismissed_untrusted_reviews_rejected(self) -> None: ): self.select(base=self.main) + def test_bootstrap_budget_increase_keeps_human_proof_for_pr_and_queue(self) -> None: + self.git("checkout", "-q", "--detach", self.base) + self.write_policy( + { + **self.policy, + "next_policy": self.pin, + "max_custom_patch_lines": self.policy["max_custom_patch_lines"] + 1, + } + ) + self.head = self.commit("bootstrap pin with budget increase") + for heads in (None, [self.head]): + with ( + mock.patch.object(selector.report, "api", side_effect=self.review_api([])), + self.assertRaisesRegex(ValueError, "human"), + ): + self.select(base=self.main, pull_heads=heads) + with mock.patch.object( + selector.report, "api", side_effect=self.review_api([self.approved()]) + ): + result = self.select(base=self.main, pull_heads=heads) + self.assertEqual(result["policy_reviews"], [{"number": 3, "head_sha": self.head}]) + def test_budget_increase_without_pin_change_requires_human(self) -> None: self.git("checkout", "-q", "--detach", self.base) policy = selector.read_policy(self.repo, self.base) @@ -383,6 +397,25 @@ def test_changed_authenticated_main_ref_rejected(self) -> None: with self.assertRaisesRegex(ValueError, "changed"): self.select() + def test_go_ratchet_activation_requires_human_for_pr_and_queue(self) -> None: + self.git("checkout", "-q", "--detach", self.base) + self.write_policy({**selector.read_policy(self.repo, self.base), "ratchet": "go"}) + self.head = self.commit("activate Go ratchet without changing pin or ceiling") + for pull_heads in (None, [self.head]): + for reviews in ([], [self.approved(body="Automated source review.")]): + with ( + self.subTest(pull_heads=pull_heads, reviews=reviews), + mock.patch.object(selector.report, "api", side_effect=self.review_api(reviews)), + self.assertRaisesRegex(ValueError, "human"), + ): + self.select(pull_heads=pull_heads) + with mock.patch.object( + selector.report, "api", side_effect=self.review_api([self.approved()]) + ): + selected = self.select(pull_heads=pull_heads) + self.assertEqual(selected["policy_sha"], self.main) + self.assertEqual(selected["policy_reviews"], [{"number": 3, "head_sha": self.head}]) + def test_wrong_origin_rejected(self) -> None: self.git("remote", "set-url", "origin", "https://github.com/attacker/example.git") with self.assertRaisesRegex(ValueError, "origin"): @@ -406,6 +439,106 @@ def test_policy_update_uses_old_base_checker_and_requires_human(self) -> None: ): self.select(main=newer) + def test_pin_effective_policy_review_for_pr_and_queue(self) -> None: + cases = ( + ({**self.policy, "max_custom_patch_lines": 4000}, False), + ({**self.policy, "max_custom_patch_lines": 3999}, False), + ({**self.policy, "max_custom_patch_lines": 5000}, True), + ({**self.policy, "ratchet": "go"}, True), + ({"schema_version": 2, "max_custom_patch_lines": 0, "generated_files": "strict"}, True), + ) + for proposed, requires_human in cases: + self.git("checkout", "-q", "--detach", self.main) + self.write_policy(proposed) + newer = self.commit("adopt proposed effective policy") + self.git("update-ref", "refs/remotes/origin/main", newer) + for bootstrap in (False, True): + base = self.main if bootstrap else self.base + self.git("checkout", "-q", "--detach", base) + self.write_policy( + {**self.policy, "next_policy": {"repository": REPO, "main_sha": newer}} + ) + self.head = self.commit("request pin without changing local budget") + for heads in (None, [self.head]): + with self.subTest(proposed=proposed, bootstrap=bootstrap, heads=heads): + with mock.patch.object( + selector, + "require_human_policy_review", + side_effect=ValueError("human review missing"), + ) as review: + if requires_human: + with self.assertRaisesRegex(ValueError, "human"): + self.select(base=base, main=newer, pull_heads=heads) + review.assert_called_once_with(REPO, self.head) + else: + result = self.select(base=base, main=newer, pull_heads=heads) + review.assert_not_called() + self.assertEqual(result["policy_reviews"], []) + if requires_human: + with mock.patch.object( + selector.report, + "api", + side_effect=self.review_api([self.approved()]), + ): + result = self.select(base=base, main=newer, pull_heads=heads) + self.assertEqual( + result["policy_reviews"], [{"number": 3, "head_sha": self.head}] + ) + + def test_rollback_pin_keeps_human_proof_for_pr_and_queue(self) -> None: + self.git("checkout", "-q", "--detach", self.main) + (self.repo / "checker-fix.txt").write_text("reviewed checker fix") + newer = self.commit("newer main checker with unchanged policy") + self.git("update-ref", "refs/remotes/origin/main", newer) + self.git("checkout", "-q", "--detach", self.base) + self.write_policy({**self.policy, "next_policy": {"repository": REPO, "main_sha": newer}}) + advanced_base = self.commit("adopt newer checker") + self.write_policy({**self.policy, "next_policy": self.pin}) + self.head = self.commit("roll back checker without changing budget") + for heads in (None, [self.head]): + with ( + mock.patch.object(selector.report, "api", side_effect=self.review_api([])), + self.assertRaisesRegex(ValueError, "human"), + ): + self.select(base=advanced_base, main=newer, pull_heads=heads) + with mock.patch.object( + selector.report, "api", side_effect=self.review_api([self.approved()]) + ): + result = self.select(base=advanced_base, main=newer, pull_heads=heads) + self.assertEqual(result["policy_sha"], newer) + self.assertEqual(result["policy_reviews"], [{"number": 3, "head_sha": self.head}]) + + def test_initial_older_pin_keeps_human_proof_for_pr_and_queue(self) -> None: + self.git("checkout", "-q", "--detach", self.main) + (self.repo / "checker-fix.txt").write_text("reviewed bootstrap checker fix") + newer = self.commit("newer main checker with unchanged policy") + self.git("update-ref", "refs/remotes/origin/main", newer) + for pin_sha in (self.main, newer): + self.git("checkout", "-q", "--detach", self.main) + self.write_policy( + {**self.policy, "next_policy": {"repository": REPO, "main_sha": pin_sha}} + ) + self.head = self.commit("initial checker pin") + for heads in (None, [self.head]): + with mock.patch.object(selector.report, "api", side_effect=self.review_api([])): + if pin_sha == self.main: + with self.assertRaisesRegex(ValueError, "human"): + self.select(base=self.main, main=newer, pull_heads=heads) + else: + result = self.select(base=self.main, main=newer, pull_heads=heads) + self.assertEqual(result["policy_reviews"], []) + self.assertEqual(result["policy_sha"], newer) + with mock.patch.object( + selector.report, "api", side_effect=self.review_api([self.approved()]) + ): + result = self.select(base=self.main, main=newer, pull_heads=heads) + self.assertEqual(result["policy_sha"], newer) + self.assertTrue(result["bootstrap"]) + self.assertEqual( + result["policy_reviews"], + [{"number": 3, "head_sha": self.head}] if pin_sha == self.main else [], + ) + def test_incapable_policy_is_not_executed(self) -> None: self.git("checkout", "-q", "--detach", self.main) (self.repo / "scripts/castiron/custom_code_budget.py").write_text( diff --git a/scripts/castiron/test_custom_code_publication.py b/scripts/castiron/test_custom_code_publication.py index eeb1331dcb..817f137548 100644 --- a/scripts/castiron/test_custom_code_publication.py +++ b/scripts/castiron/test_custom_code_publication.py @@ -7,6 +7,7 @@ import json import shutil import subprocess +import tempfile import unittest from pathlib import Path from typing import Any, cast @@ -455,8 +456,11 @@ def publish( 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) @@ -464,6 +468,23 @@ def publish( 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)), @@ -496,7 +517,9 @@ def publish( "POLICY_REVIEWS": json.dumps(policy_reviews or []), "HUMAN_REVIEW_RULES": "" if missing_review_contract - else json.dumps(policy.HUMAN_REVIEW_RULES), + else json.dumps( + policy.HUMAN_REVIEW_RULES if review_contract is None else review_contract + ), "SELECTED_BRANCH": branch, "SELECTED_BASE_SHA": base, "SELECTED_HEAD_SHA": head, @@ -536,7 +559,7 @@ def test_live_human_review_predicate_matches_selector(self) -> None: "commit_id": head, "user": {"login": "human", "type": "User"}, "author_association": "MEMBER", - "body": "", + "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + head, } cases = [ [approved], @@ -550,6 +573,10 @@ def test_live_human_review_predicate_matches_selector(self) -> None: [{**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.", @@ -567,6 +594,80 @@ 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_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] = { @@ -582,7 +683,7 @@ def test_queue_policy_approval_dismissal_after_selection_fails(self) -> None: "commit_id": candidate, "user": {"login": "human", "type": "User"}, "author_association": "MEMBER", - "body": "", + "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + candidate, } for state in ("APPROVED", "DISMISSED", "CHANGES_REQUESTED"): with self.subTest(state=state): @@ -631,7 +732,7 @@ def test_queue_policy_constituent_changes_fail_closed(self) -> None: "commit_id": candidate, "user": {"login": "human", "type": "User"}, "author_association": "MEMBER", - "body": "", + "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + candidate, } for changed in [ {**pull, "state": "closed"}, @@ -654,6 +755,20 @@ def test_missing_policy_review_contract_fails_closed(self) -> None: ["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): @@ -869,5 +984,486 @@ def test_publication_guard_allows_current_retry_and_ignores_unrelated_statuses(s self.assertTrue(all(len(r["description"]) <= 140 for r in results)) +@unittest.skipUnless( + shutil.which("node"), "Node is needed to execute the review-revocation fixture" +) +class ReviewRevocationTests(unittest.TestCase): + def payload(self) -> dict[str, Any]: + head, base, queue = "a" * 40, "b" * 40, "c" * 40 + pull = { + "number": 3, + "user": {"login": "author"}, + "state": "open", + "head": {"sha": head}, + "base": {"ref": "next", "sha": base, "repo": {"full_name": "openai/example"}}, + } + run = { + **source_run(head), + "id": 20, + "actor": {"login": "outsider", "type": "User"}, + "event": "pull_request_review", + "status": "completed", + "pull_requests": [{"number": 3, "head": {"sha": head}}], + } + + def original(sha: str, event: str, identity: int) -> dict[str, Any]: + return {**run, "head_sha": sha, "id": identity, "event": event} + + return { + "run": run, + "previous_statuses": [ + { + "context": name, + "state": "success", + "creator": {"login": "github-actions[bot]"}, + "target_url": "https://github.com/openai/example/actions/runs/19", + } + for name in ( + "Castiron next / budget-only change", + "Castiron next / custom-code budget", + ) + ], + "current": pull, + "queue_nodes": [ + {"headCommit": {"oid": queue}, "pullRequest": {"number": 3, "headRefOid": head}} + ], + "runs_by_sha": { + head: [original(head, "pull_request", 12)], + queue: [original(queue, "merge_group", 14)], + }, + "context": { + "payload": {"workflow_run": dict(run)}, + "runId": 21, + "repo": {"owner": "openai", "repo": "example"}, + "serverUrl": "https://github.com", + }, + } + + def invoke(self, payload: dict[str, Any]) -> list[dict[str, Any]]: + return run_publisher("Re-evaluate current PR and queue after review changes", payload) + + def test_revokes_pr_and_queue_before_requesting_normal_re_evaluation(self) -> None: + result = self.invoke(self.payload()) + self.assertEqual([item.get("state") for item in result[:4]], ["pending"] * 4) + self.assertEqual({item["sha"] for item in result[:4]}, {"a" * 40, "c" * 40}) + self.assertTrue(all(item["context"].startswith("Castiron next /") for item in result[:4])) + self.assertEqual([item["run_id"] for item in result[4:]], [12, 14]) + self.assertNotIn("success", [item.get("state") for item in result]) + + def approval(self) -> dict[str, Any]: + return { + "fullDatabaseId": "101", + "state": "APPROVED", + "body": policy.HUMAN_REVIEW_RULES["attestation_prefix"] + "a" * 40, + "author": {"login": "reviewer", "__typename": "User"}, + "authorAssociation": "MEMBER", + "commit": {"oid": "a" * 40}, + "updatedAt": "2026-10-22T01:00:00Z", + } + + def recovery(self) -> dict[str, Any]: + payload = self.payload() + payload["review_nodes"] = [self.approval()] + payload["run"]["actor"] = {"login": "reviewer", "type": "User"} + for status in payload["previous_statuses"]: + status["state"] = "failure" + return payload + + def history(self, payload: dict[str, Any], writes: list[dict[str, Any]]) -> None: + payload["statuses_by_sha"] = { + sha: [ + {**item, "creator": {"login": "github-actions[bot]"}} + for item in writes + if item.get("sha") == sha + ] + for sha in ("a" * 40, "c" * 40) + } + payload["runs_by_id"] = { + "21": { + "id": 21, + "workflow_id": 789, + "status": "completed", + "conclusion": "success", + "repository": {"full_name": "openai/example"}, + "event": "workflow_run", + } + } + + def test_unrelated_comment_cannot_invalidate_valid_approval_or_request_recovery(self) -> None: + for state in ("success", "failure", "pending"): + payload = self.recovery() + payload["run"]["actor"]["login"] = "outsider" + payload["review_nodes"].append( + { + **self.approval(), + "fullDatabaseId": "102", + "author": {"login": "outsider", "__typename": "User"}, + "state": "COMMENTED", + "body": "hello", + } + ) + for status in payload["previous_statuses"]: + status["state"] = state + payload["forbid_queue"] = True + self.assertEqual(self.invoke(payload), []) + + def test_ineligible_review_cannot_request_recovery(self) -> None: + for overrides in ( + {"state": "COMMENTED"}, + {"body": ""}, + {"authorAssociation": "CONTRIBUTOR"}, + {"commit": {"oid": "b" * 40}}, + {"author": {"login": "author", "__typename": "User"}}, + {"author": {"login": "reviewer", "__typename": "Bot"}}, + ): + with self.subTest(overrides=overrides): + payload = self.recovery() + payload["review_nodes"][0].update(overrides) + self.assertEqual(self.invoke(payload), []) + + def test_repeated_invalid_signals_do_not_rerun_already_revoked_heads(self) -> None: + payload = self.payload() + writes = self.invoke(payload) + self.history(payload, writes) + self.assertEqual(self.invoke(payload), []) + for statuses in payload["statuses_by_sha"].values(): + for status in statuses: + status["state"] = "failure" + self.assertEqual(self.invoke(payload), []) + + def test_approval_recovery_coalesces_and_edited_restoration_recovers_again(self) -> None: + payload = self.recovery() + writes = self.invoke(payload) + self.assertEqual(len(writes), 6) + self.assertTrue(all(len(item["description"]) <= 140 for item in writes[:4])) + self.history(payload, writes) + self.assertEqual(self.invoke(payload), []) + payload["review_nodes"][0]["updatedAt"] = "2026-10-22T01:02:00Z" + restored = self.invoke(payload) + self.assertEqual(len(restored), 6) + self.assertNotEqual(writes[0]["description"], restored[0]["description"]) + + def test_new_revocation_supersedes_old_recovery_even_with_equal_edit_timestamp(self) -> None: + payload = self.recovery() + approved = self.invoke(payload) + self.history(payload, approved) + for statuses in payload["statuses_by_sha"].values(): + statuses[:0] = [ + {**status, "state": "success", "description": "Successful evaluation"} + for status in statuses + ] + payload["review_nodes"][0]["body"] = "" + revoked = self.invoke(payload) + self.assertEqual(len(revoked), 6) + self.history(payload, revoked) + for sha, statuses in payload["statuses_by_sha"].items(): + statuses.extend( + {**item, "creator": {"login": "github-actions[bot]"}} + for item in approved + if item.get("sha") == sha + ) + payload["review_nodes"][0] = ( + self.approval() + ) # Same ID, body and second-precision update time. + self.assertEqual(len(self.invoke(payload)), 6) + + def test_full_review_id_handles_current_and_beyond_safe_integer_ids(self) -> None: + payload = self.recovery() + payload["review_nodes"][0].update(databaseId=None, fullDatabaseId="5614624241") + self.assertEqual(len(self.invoke(payload)), 6) + payload = self.payload() + payload["review_nodes"] = [ + {**self.approval(), "databaseId": None, "fullDatabaseId": "9007199254740992"}, + { + **self.approval(), + "databaseId": None, + "fullDatabaseId": "9007199254740993", + "state": "COMMENTED", + }, + ] + self.assertEqual(len(self.invoke(payload)), 6) + payload["review_nodes"][0]["fullDatabaseId"] = 9007199254740992 + with self.assertRaises(subprocess.CalledProcessError): + self.invoke(payload) + + def test_publisher_failure_supersedes_recovery_without_observed_revocation(self) -> None: + payload = self.recovery() + approved = self.invoke(payload) + self.history(payload, approved) + for statuses in payload["statuses_by_sha"].values(): + statuses[:0] = [ + {**status, "state": "failure", "description": "Approval missing during publication"} + for status in statuses + ] + # Publisher observed invalid body; review jobs only see its same-second restoration. + # No new review identity/timestamp and no review-handler revocation marker exists. + recovered = self.invoke(payload) + self.assertEqual(len(recovered), 6) + self.history(payload, recovered) + self.assertEqual(self.invoke(payload), []) + + def test_deleted_marker_writer_is_untrusted_but_service_errors_propagate(self) -> None: + payload = self.recovery() + approved = self.invoke(payload) + self.history(payload, approved) + payload["run_errors"] = {"21": 404} + self.assertEqual(len(self.invoke(payload)), 6) + for status in (403, 429, 500): + payload["run_errors"]["21"] = status + with self.subTest(status=status), self.assertRaises(subprocess.CalledProcessError): + self.invoke(payload) + + def test_failed_partial_dispatch_is_retried_before_signals_coalesce(self) -> None: + for authorized in (False, True): + for conclusion in ("failure", "cancelled", "timed_out"): + payload = self.recovery() if authorized else self.payload() + payload["fail_rerun"] = True + # The fixture persists actual pending writes before the simulated API error. + with tempfile.TemporaryDirectory() as directory: + capture = Path(directory) / "pending.json" + payload["capture_writes"] = str(capture) + with self.assertRaises(subprocess.CalledProcessError): + self.invoke(payload) + pending = json.loads(capture.read_text()) + self.assertEqual(len(pending), 4) + self.history(payload, pending) + payload.pop("capture_writes") + payload.pop("fail_rerun") + payload["runs_by_id"]["21"]["conclusion"] = conclusion + payload["context"]["runId"] = 22 + retried = self.invoke(payload) + self.assertEqual([item["run_id"] for item in retried if "run_id" in item], [12, 14]) + self.history(payload, retried) + payload["runs_by_id"]["22"] = {**payload["runs_by_id"]["21"], "id": 22} + self.assertEqual(self.invoke(payload), []) + + def test_untrusted_marker_cannot_suppress_recovery(self) -> None: + for field, value in ( + ("workflow_id", 999), + ("event", "pull_request"), + ("repository", {"full_name": "attacker/example"}), + ): + payload = self.recovery() + self.history(payload, self.invoke(payload)) + 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: + for changed in ( + {"state": "DISMISSED"}, + {"body": ""}, + {"state": "COMMENTED", "fullDatabaseId": "102"}, + ): + payload = self.payload() + payload["review_nodes"] = [{**self.approval(), **changed}] + self.assertEqual(len(self.invoke(payload)), 6) + payload = self.payload() + payload["review_pages"] = [ + { + "expected_cursor": None, + "nodes": [{**self.approval(), "fullDatabaseId": "102", "state": "COMMENTED"}], + "pageInfo": {"hasNextPage": True, "endCursor": "more"}, + }, + { + "expected_cursor": "more", + "nodes": [self.approval()], + "pageInfo": {"hasNextPage": False}, + }, + ] + self.assertEqual(len(self.invoke(payload)), 6) + + def test_missing_authoritative_edit_time_rejects_recovery(self) -> None: + payload = self.recovery() + payload["review_nodes"][0]["updatedAt"] = None + with self.assertRaises(subprocess.CalledProcessError): + self.invoke(payload) + + def test_review_signal_requires_canonical_workflow_identity_before_writes(self) -> None: + for identity in (None, "456", 999): + for path in ( + ".github/workflows/castiron-custom-code.yml", + ".github/workflows/castiron-custom-code.yml@evil.yml", + ): + with self.subTest(identity=identity, path=path): + payload = self.payload() + payload["run"].update({"workflow_id": identity, "path": path}) + payload["context"]["payload"]["workflow_run"] = dict(payload["run"]) + self.assertEqual(self.invoke(payload), []) + + def test_rejects_forged_or_superseded_review_signals(self) -> None: + for field, value in [ + ("path", ".github/workflows/other.yml"), + ("run_attempt", 9), + ("head_sha", "d" * 40), + ("event", "pull_request"), + ]: + with self.subTest(field=field): + payload = self.payload() + payload["run"][field] = value + with self.assertRaises(subprocess.CalledProcessError): + self.invoke(payload) + payload = self.payload() + payload["run"]["repository"] = {"full_name": "attacker/example"} + with self.assertRaises(subprocess.CalledProcessError): + self.invoke(payload) + + def test_head_change_keeps_observed_statuses_revoked_without_stale_reruns(self) -> None: + payload = self.payload() + changed = {**payload["current"], "head": {"sha": "d" * 40}} + payload["current_sequence"] = [payload["current"], changed] + payload["current"] = changed + result = self.invoke(payload) + self.assertEqual(len(result), 4) + self.assertTrue(all(item["state"] == "pending" for item in result)) + self.assertNotIn("d" * 40, {item["sha"] for item in result}) + + def test_queue_change_retries_and_invalidates_new_snapshot(self) -> None: + payload = self.payload() + old = list(payload["queue_nodes"]) + payload["queue_nodes"].append( + { + "headCommit": {"oid": "d" * 40}, + "pullRequest": {"number": 4, "headRefOid": "e" * 40}, + } + ) + payload["runs_by_sha"]["d" * 40] = [ + { + **payload["runs_by_sha"]["c" * 40][0], + "head_sha": "d" * 40, + "id": 16, + } + ] + payload["queue_pages"] = [ + { + "expected_cursor": None, + "nodes": old, + "pageInfo": {"hasNextPage": False}, + } + ] + result = self.invoke(payload) + pending = [item for item in result if item.get("state") == "pending"] + self.assertEqual({item["sha"] for item in pending}, {"a" * 40, "c" * 40, "d" * 40}) + self.assertEqual([item["run_id"] for item in result if "run_id" in item], [12, 14, 16]) + self.assertTrue(all(item.get("state") == "pending" for item in result[:-3])) + + def test_empty_review_pr_list_uses_authenticated_fork_discovery(self) -> None: + for associated in ([], [{"number": 3}]): + payload = self.payload() + payload["run"].update( + { + "pull_requests": [], + "head_repository": {"owner": {"login": "contributor"}}, + "head_branch": "sdk", + } + ) + payload["associated_pulls"] = associated + payload["fallback_pulls"] = [{"number": 3}] + self.assertEqual(len(self.invoke(payload)), 6) + payload["current"]["head"]["sha"] = "d" * 40 + payload["merge_sha"] = "e" * 40 + payload["merge_commit"] = {"parents": []} + self.assertEqual(self.invoke(payload), []) + + def test_merge_sha_signal_requires_exact_live_merge_and_parents(self) -> None: + payload = self.payload() + payload["run"]["head_sha"] = payload["context"]["payload"]["workflow_run"]["head_sha"] = ( + "e" * 40 + ) + payload["merge_sha"] = "e" * 40 + payload["merge_commit"] = {"parents": [{"sha": "b" * 40}, {"sha": "a" * 40}]} + self.assertEqual(len(self.invoke(payload)), 6) + payload["merge_commit"]["parents"].reverse() + self.assertEqual(self.invoke(payload), []) + + def test_closed_or_superseded_review_is_a_no_op(self) -> None: + for changed in ({"state": "closed"}, {"head": {"sha": "f" * 40}}): + payload = self.payload() + payload["current"].update(changed) + self.assertEqual(self.invoke(payload), []) + + def test_null_queue_association_does_not_block_revocation(self) -> None: + payload = self.payload() + payload["queue_nodes"].insert(0, {"headCommit": {"oid": "f" * 40}, "pullRequest": None}) + result = self.invoke(payload) + self.assertEqual(len(result), 6) + self.assertNotIn("f" * 40, {item.get("sha") for item in result}) + payload["queue_nodes"] = payload["queue_nodes"][:1] + self.assertEqual(len(self.invoke(payload)), 3) + + def test_rerun_requires_the_reviewed_pr_not_just_shared_head(self) -> None: + payload = self.payload() + correct = payload["runs_by_sha"]["a" * 40][0] + main = { + **payload["current"], + "number": 4, + "base": {**payload["current"]["base"], "ref": "main"}, + } + payload["pulls_by_number"] = {"4": main} + other: dict[str, Any] = { + **correct, + "id": 30, + "pull_requests": [{"number": 4, "head": {"sha": "a" * 40}}], + } + payload["runs_by_sha"]["a" * 40].append(other) + result = self.invoke(payload) + self.assertEqual([item["run_id"] for item in result if "run_id" in item], [12, 14]) + # Empty fork associations may rerun only if canonical discovery selects this next PR. + fork: dict[str, Any] = { + **correct, + "pull_requests": [], + "head_repository": {"owner": {"login": "contributor"}}, + "head_branch": "sdk", + } + payload["fallback_pulls"] = [{"number": 3}] + payload["runs_by_sha"]["a" * 40] = [fork, other] + result = self.invoke(payload) + self.assertEqual([item["run_id"] for item in result if "run_id" in item], [12, 14]) + association_cases: tuple[list[dict[str, Any]], ...] = ( + [], + [*correct["pull_requests"], *other["pull_requests"]], + ) + for associations in association_cases: + payload["associated_pulls"] = [{"number": 3}, {"number": 4}] + payload["runs_by_sha"]["a" * 40] = [{**fork, "pull_requests": associations}, other] + result = self.invoke(payload) + self.assertEqual([item["run_id"] for item in result if "run_id" in item], [14]) + self.assertTrue(all(item["state"] == "pending" for item in result[:4])) + + def test_unprepared_queue_entry_still_revokes_pr_statuses(self) -> None: + payload = self.payload() + payload["queue_nodes"][0]["headCommit"] = None + result = self.invoke(payload) + self.assertEqual([item.get("state") for item in result], ["pending", "pending", None]) + self.assertEqual(result[-1]["run_id"], 12) + + def test_unrelated_review_does_not_restart_reporter(self) -> None: + payload = self.payload() + payload["files"] = [{"filename": "src/client.ts"}] + self.assertEqual(self.invoke(payload), []) + + def test_no_success_or_foreign_run_can_be_published(self) -> None: + payload = self.payload() + payload["runs_by_sha"]["a" * 40][0]["repository"] = {"full_name": "other/repo"} + result = self.invoke(payload) + self.assertEqual( + [item["run_id"] for item in result if item.get("operation") == "rerun"], [14] + ) + self.assertTrue(all(item.get("state") != "success" for item in result)) + + 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.assertNotIn("checkout@", signal) + handler = WORKFLOW.read_text() + revoke = handler.split(" review-change:\n", 1)[1].split(" compute:\n", 1)[0] + self.assertNotIn("checkout@", revoke) + self.assertNotIn("download-artifact", revoke) + self.assertEqual( + handler.count("group: castiron-budget-writes-${{ github.repository_id }}"), 2 + ) + + if __name__ == "__main__": unittest.main() diff --git a/scripts/castiron/test_custom_code_report.py b/scripts/castiron/test_custom_code_report.py index c523765a10..6c42233e25 100644 --- a/scripts/castiron/test_custom_code_report.py +++ b/scripts/castiron/test_custom_code_report.py @@ -3,11 +3,14 @@ import base64 import hashlib +import io import json import os import struct import subprocess import unittest +from pathlib import Path +from typing import Any from unittest import mock import custom_code_report as report @@ -1080,5 +1083,491 @@ def test_public_snapshot_has_no_private_history_and_reports_without_private_remo self.assertEqual(merged_report["counts"]["newly_customized"], 1) +ADOPTION_GENERATION = "550e8400-e29b-41d4-a716-446655440001" + + +class FirstAdoptionTests(GitTestCase): + def setUp(self) -> None: # pyright: ignore[reportImplicitOverride] + super().setUp() + self.repository = "openai/openai-java" + self.write(".stats.yml", "configured_endpoints: 278\n") + self.root = self.commit("legacy root") + self.git("remote", "add", "origin", "https://github.com/openai/openai-java.git") + self.policy: dict[str, Any] = {"schema_version": 1, "max_custom_patch_lines": 3000} + self.write(".castiron-ratchet.json", json.dumps(self.policy) + "\n") + self.g0 = self.generated("source\n", GENERATION, False) + self.public0 = report.create_public_snapshot( + self.repo, + self.git("rev-parse", f"{self.g0}^{{tree}}"), + GENERATION, + report.hash_codegen_commit(self.repo, self.g0), + "codegen/main", + None, + ) + self.git("update-ref", "refs/remotes/origin/codegen/main", self.public0) + self.write("generated.py", "source\n") + self.stats(self.g0, self.public0, GENERATION) + self.main = self.commit("approved main") + self.git("update-ref", "refs/remotes/origin/main", self.main) + self.git("checkout", "--detach", self.root) + self.pin: dict[str, Any] = { + **self.policy, + "next_policy": {"repository": self.repository, "main_sha": self.main}, + } + self.write(".castiron-ratchet.json", json.dumps(self.pin) + "\n") + self.base = self.commit("separate approved next policy") + self.git("update-ref", "refs/remotes/origin/next", self.base) + + def generated(self, text: str, generation: str, next_target: bool) -> str: + blob = ( + report.git(self.repo, "hash-object", "-w", "--stdin", input_bytes=text.encode()) + .decode() + .strip() + ) + tree = ( + report.git( + self.repo, + "mktree", + input_bytes=f"100644 blob {blob}\tgenerated.py\n".encode(), + ) + .decode() + .strip() + ) + record: dict[str, Any] = { + "generation_id": generation, + "source_branch": "test", + "target": self.repository.split("/")[1], + "language": self.repository.split("-")[-1], + } + if next_target: + record["public_baseline_commit"] = self.base + record = { + "schema_version": "castiron-checkpoint/v2", + "release_target": "next", + "record": record, + } + encoded = base64.b64encode(json.dumps(record).encode()).decode() + commit = ( + report.git( + self.repo, + "commit-tree", + tree, + input_bytes=f"codegen\n\nGeneration metadata: {encoded}\n".encode(), + ) + .decode() + .strip() + ) + namespace = "castiron/next/" if next_target else "" + self.git("update-ref", f"refs/remotes/origin/codegen/{namespace}test", commit) + return commit + + def stats(self, private: str, public: str, generation: str) -> None: + self.write( + ".castiron.stats.yml", + f"schema_version: 1\ngeneration_id: {generation}\ncodegen_sha: {private}\npublic_codegen_sha: {public}\ncodegen_hash: {report.hash_codegen_commit(self.repo, private)}\nconfig_hash: source-config\nopenapi_spec_hash: source-spec\n", + ) + + def seed(self) -> str: + # Synthetic two-parent private seed; production worker initialization is + # covered separately in the SDK worker candidate-flow suite. + self.git("read-tree", "--reset", "-u", self.main) + self.write(".castiron-ratchet.json", json.dumps(self.pin) + "\n") + self.git("add", "-A") + return self.git( + "commit-tree", + self.git("write-tree"), + "-p", + self.base, + "-p", + self.main, + "-m", + "approved main adoption fixture", + ) + + def head(self, *, public_receipt: bool = True, customize: bool = False) -> str: + seed = self.seed() + self.git("checkout", "--detach", seed) + self.g1 = self.generated("next generated\n", ADOPTION_GENERATION, True) + snap = report.create_public_snapshot( + self.repo, + self.git("rev-parse", f"{self.g1}^{{tree}}"), + ADOPTION_GENERATION, + report.hash_codegen_commit(self.repo, self.g1), + "codegen/castiron/next/new", + self.public0, + (self.base, self.main) if public_receipt else None, + ) + self.git("update-ref", "refs/remotes/origin/codegen/castiron/next/new", snap) + self.write( + "generated.py", + "next generated\n" + ("# customization\n" if customize else ""), + ) + self.stats(self.g1, snap, ADOPTION_GENERATION) + return self.commit("next candidate") + + def test_java_public_and_private_report_have_no_fake_before_generation(self) -> None: + head = self.head(customize=True) + for public in (False, True): + result, patch = report.build_report( + self.repo, + self.base, + head, + public=public, + target_branch="next", + require_head_hash=True, + ) + self.assertIsNone(result["before"]) + self.assertEqual(result["adoption_baseline"]["generation_id"], GENERATION) + self.assertEqual(result["files"][0]["added"], "1") + self.assertIn(b"# customization", patch) + self.assertIn("Legacy before-generation unavailable", report.render_report(result)) + + def test_cli_uses_same_protected_pin_path(self) -> None: + self.repository = "openai/openai-cli" + self.git("remote", "set-url", "origin", "https://github.com/openai/openai-cli.git") + self.pin["next_policy"]["repository"] = self.repository + self.write(".castiron-ratchet.json", json.dumps(self.pin) + "\n") + self.base = self.commit("CLI approved policy fixture") + head = self.head() + result, _ = report.build_report( + self.repo, + self.base, + head, + public=False, + target_branch="next", + require_head_hash=True, + ) + self.assertIsNone(result["before"]) + + def test_promotion_base_cli_recognizes_exact_next_branch(self) -> None: + original = report.git + + def offline_git(repo: Path, *args: str, input_bytes: bytes | None = None) -> bytes: + if args[0] == "fetch": + source, destination = args[-1].lstrip("+").split(":", 1) + return original( + repo, + "update-ref", + destination, + source.replace("refs/heads/", "refs/remotes/origin/"), + ) + return original(repo, *args, input_bytes=input_bytes) + + for base, branch, expected in ( + (self.base, "castiron/next", self.main), + (self.base, "castiron/next/child", self.main), + (self.base, "castiron/nextish", None), + (self.base, "castiron/next-door", None), + (self.base, "castiron/promotions/main", None), + (self.main, "castiron/promotions/main", self.main), + ): + with self.subTest(base=base, branch=branch): + output, errors = io.StringIO(), io.StringIO() + with ( + mock.patch.object(report, "git", side_effect=offline_git), + mock.patch.object( + report.sys, + "argv", + [ + "custom_code_report.py", + "promotion-base", + "--repo", + str(self.repo), + "--base", + base, + "--branch", + branch, + ], + ), + mock.patch.object(report.sys, "stdout", output), + mock.patch.object(report.sys, "stderr", errors), + ): + result = report.main() + if expected is None: + self.assertEqual(result, 1) + self.assertEqual(output.getvalue(), "") + self.assertIn("first adoption is next-only", errors.getvalue()) + else: + self.assertEqual(result, 0, errors.getvalue()) + self.assertEqual(output.getvalue(), expected + "\n") + + def test_promotion_prepares_new_snapshot_from_real_main_snapshot(self) -> None: + head = self.head() + original = report.git + + def offline_git(repo: Path, *args: str, input_bytes: bytes | None = None) -> bytes: + if args[0] == "fetch": + for arg in args: + if arg.startswith("+refs/") and ":" in arg: + source, destination = arg.lstrip("+").split(":", 1) + original( + repo, + "update-ref", + destination, + source.replace("refs/heads/", "refs/remotes/origin/"), + ) + return b"" + if args[0] == "ls-remote": + return b"" + return original(repo, *args, input_bytes=input_bytes) + + with mock.patch.object(report, "git", side_effect=offline_git): + prepared = report.prepare_public_snapshot( + self.repo, self.base, head, self.repo, self.base, "castiron/next/fresh" + ) + metadata = report.public_metadata(self.repo, prepared["commit"]) + self.assertEqual(metadata["Target"], "next") + self.assertEqual(metadata["Adoption-Base"], self.base) + self.assertEqual(metadata["Adoption-Source"], self.main) + self.assertEqual(prepared["base_commit"], self.public0) + self.assertEqual(self.git("rev-parse", prepared["commit"] + "^"), self.public0) + + def test_missing_receipt_rejected(self) -> None: + head = self.head(public_receipt=False) + with self.assertRaisesRegex(report.ReportError, "authorized first adoption"): + report.build_report(self.repo, self.base, head, public=True, target_branch="next") + + def test_wrong_sdk_record_rejected(self) -> None: + head = self.head() + self.repository = "openai/openai-cli" + wrong = self.generated("next generated\n", ADOPTION_GENERATION, True) + self.stats( + wrong, report.read_stats(self.repo, head)["public_codegen_sha"], ADOPTION_GENERATION + ) + head = self.commit("wrong SDK binding") + with self.assertRaisesRegex(report.ReportError, "SDK mismatch"): + report.build_report(self.repo, self.base, head, public=False, target_branch="next") + + def test_config_change_rejected(self) -> None: + self.head() + path = self.repo / ".castiron.stats.yml" + path.write_text(path.read_text().replace("source-config", "wrong-config")) + head = self.commit() + with self.assertRaisesRegex(report.ReportError, "authenticated main inputs"): + report.build_report(self.repo, self.base, head, public=False, target_branch="next") + + def test_existing_corrupt_deleted_stats_rejected(self) -> None: + self.write(".castiron.stats.yml", "corrupt: true\n") + corrupt = self.commit() + with self.assertRaisesRegex(report.ReportError, "existing/corrupt"): + report.first_adoption_source(self.repo, corrupt, target_branch="next", fetch=False) + self.git("rm", ".castiron.stats.yml") + deleted = self.commit() + with self.assertRaisesRegex(report.ReportError, "deleted"): + report.first_adoption_source(self.repo, deleted, target_branch="next", fetch=False) + + def test_candidate_cannot_select_source_or_remove_policy(self) -> None: + self.head() + self.write(".castiron-ratchet.json", json.dumps(self.policy) + "\n") + head = self.commit() + with self.assertRaisesRegex(report.ReportError, "protected target policy"): + report.build_report(self.repo, self.base, head, public=True, target_branch="next") + + def test_current_count_and_strict_zero_still_enforced(self) -> None: + import custom_code_budget as budget + + head = self.head(customize=True) + measurement = report.build_report( + self.repo, self.base, head, public=True, target_branch="next" + ) + evaluated, _ = budget.evaluate( + self.repo, + self.base, + head, + public=True, + policy_sha=self.main, + target_branch="next", + measurement=measurement, + ) + self.assertEqual(evaluated["total"], 1) + self.assertEqual(evaluated["checks"]["budget"]["state"], "success", evaluated) + self.write( + ".castiron-ratchet.json", + '{"schema_version":2,"max_custom_patch_lines":0,"generated_files":"strict"}\n', + ) + strict = self.commit() + evaluated, _ = budget.evaluate( + self.repo, + self.base, + head, + public=True, + policy_sha=strict, + target_branch="next", + measurement=measurement, + ) + self.assertEqual(evaluated["limit"], 0) + self.assertEqual(evaluated["checks"]["budget"]["state"], "failure") + self.assertIn("manifest", evaluated["checks"]["budget"]["description"]) + + def test_unapproved_source_pin_rejected(self) -> None: + self.pin["next_policy"]["main_sha"] = self.base + self.write(".castiron-ratchet.json", json.dumps(self.pin) + "\n") + invalid = self.commit() + with self.assertRaisesRegex(report.ReportError, "authenticated main history"): + report.first_adoption_source(self.repo, invalid, target_branch="next", fetch=False) + + def test_advanced_target_requires_fresh_adoption(self) -> None: + head = self.head() + self.git("checkout", "--detach", self.base) + self.write("legacy-note.txt", "another approved target change\n") + advanced = self.commit() + with self.assertRaisesRegex(report.ReportError, "target advanced"): + report.build_report(self.repo, advanced, head, public=True, target_branch="next") + + def test_main_target_rejected(self) -> None: + with self.assertRaisesRegex(report.ReportError, "next-only"): + report.first_adoption_source(self.repo, self.base, target_branch="main", fetch=False) + + def test_normal_public_main_report_unchanged(self) -> None: + self.git("checkout", "--detach", self.main) + self.write("generated.py", "source\n# customization\n") + head = self.commit() + result, _ = report.build_report( + self.repo, self.main, head, public=True, target_branch="main" + ) + self.assertIsNotNone(result["before"]) + self.assertEqual(result["before_status"], "verified") + + def promoted_public_head(self) -> tuple[Path, str, dict[str, Any]]: + # Exercise the separate public checkout and the actual single-parent + # commit-tree shape in sdk/promote.rs, not the private merge seed. + import subprocess + + destination = self.repo / "public-checkout" + head = self.head(customize=True) + subprocess.run( + ["git", "clone", "-q", "--no-hardlinks", str(self.repo), str(destination)], check=True + ) + + def public_git(*args: str) -> str: + return report.git(destination, *args).decode().strip() + + public_git("config", "user.name", "Castiron test") + public_git("config", "user.email", "castiron@example.test") + public_git("remote", "set-url", "origin", "https://github.com/" + self.repository + ".git") + for ref, commit in [ + ("main", self.main), + ("next", self.base), + ("codegen/main", self.public0), + ]: + public_git("update-ref", "refs/remotes/origin/" + ref, commit) + public_git("checkout", "--detach", self.base) + # Normal promotion applies the reviewed private patch to this index. + public_git("read-tree", "--reset", "-u", head) + original = report.git + + def offline_git(repo: Path, *args: str, input_bytes: bytes | None = None) -> bytes: + if args[0] == "fetch": + for arg in args: + if arg.startswith("+refs/") and ":" in arg: + source, target = arg.lstrip("+").split(":", 1) + original( + repo, + "update-ref", + target, + source.replace("refs/heads/", "refs/remotes/origin/"), + ) + return b"" + if args[0] == "ls-remote": + return b"" + return original(repo, *args, input_bytes=input_bytes) + + with mock.patch.object(report, "git", side_effect=offline_git): + prepared = report.prepare_public_snapshot( + self.repo, self.base, head, destination, self.base, "castiron/next/fresh" + ) + public_git("update-ref", "refs/remotes/origin/" + prepared["branch"], prepared["commit"]) + promoted = public_git( + "commit-tree", + public_git("write-tree"), + "-p", + self.base, + "-m", + "normal public promotion", + ) + self.assertEqual( + public_git("rev-list", "--parents", "-n", "1", promoted), promoted + " " + self.base + ) + with self.assertRaises(report.ReportError): + public_git("merge-base", "--is-ancestor", self.main, promoted) + # Private authorization still retains both parents. + self.git("merge-base", "--is-ancestor", self.main, head) + self.git("merge-base", "--is-ancestor", self.base, head) + return destination, promoted, prepared + + def test_normal_single_parent_public_promotion_reports_first_adoption(self) -> None: + destination, promoted, _ = self.promoted_public_head() + result, patch = report.build_report( + destination, + self.base, + promoted, + public=True, + target_branch="next", + require_head_hash=True, + ) + self.assertIsNone(result["before"]) + self.assertEqual(result["adoption_baseline"]["generation_id"], GENERATION) + self.assertIn(b"# customization", patch) + + def test_public_adoption_rejects_disconnected_snapshot_with_valid_receipt(self) -> None: + destination, promoted, prepared = self.promoted_public_head() + # The right receipt/hash alone does not authenticate approved-main lineage. + detached = report.create_public_snapshot( + destination, + report.git(destination, "rev-parse", prepared["commit"] + "^{tree}").decode().strip(), + ADOPTION_GENERATION, + prepared["hash"], + prepared["branch"], + None, + (self.base, self.main), + ) + report.git(destination, "update-ref", "refs/remotes/origin/" + prepared["branch"], detached) + report.git(destination, "checkout", "--detach", promoted) + stats = destination / ".castiron.stats.yml" + stats.write_text(stats.read_text().replace(prepared["commit"], detached)) + report.git(destination, "add", ".castiron.stats.yml") + invalid = ( + report.git( + destination, + "commit-tree", + report.git(destination, "write-tree").decode().strip(), + "-p", + self.base, + "-m", + "disconnected snapshot", + ) + .decode() + .strip() + ) + with self.assertRaises(report.ReportError): + report.build_report( + destination, + self.base, + invalid, + public=True, + target_branch="next", + require_head_hash=True, + ) + + def test_cli_normal_single_parent_public_promotion_reports_first_adoption(self) -> None: + self.repository = "openai/openai-cli" + self.git("remote", "set-url", "origin", "https://github.com/openai/openai-cli.git") + self.pin["next_policy"]["repository"] = self.repository + self.write(".castiron-ratchet.json", json.dumps(self.pin) + "\n") + self.base = self.commit("CLI approved next policy") + self.git("update-ref", "refs/remotes/origin/next", self.base) + destination, promoted, _ = self.promoted_public_head() + result, _ = report.build_report( + destination, + self.base, + promoted, + public=True, + target_branch="next", + require_head_hash=True, + ) + self.assertIsNone(result["before"]) + self.assertEqual(result["adoption_baseline"]["generation_id"], GENERATION) + + if __name__ == "__main__": unittest.main() diff --git a/src/openai/__init__.py b/src/openai/__init__.py index accbd79f1b..74f675aca6 100644 --- a/src/openai/__init__.py +++ b/src/openai/__init__.py @@ -4,20 +4,25 @@ import os as _os import typing as _t +import typing_extensions as _te from typing_extensions import override from . import types from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given from ._utils import file_from_path from ._client import Client, OpenAI, Stream, Timeout, Transport, AsyncClient, AsyncOpenAI, AsyncStream, RequestOptions -from ._httpx2 import DefaultHttpx2Client, DefaultAsyncHttpx2Client, normalize_httpx_url as _normalize_httpx_url +from ._httpx2 import ( + DefaultHttpx2Client as DefaultHttpx2Client, + DefaultAsyncHttpx2Client as DefaultAsyncHttpx2Client, + normalize_httpx_url as _normalize_httpx_url, +) from ._models import BaseModel from ._version import __title__, __version__ from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS from ._exceptions import ( APIError, - OAuthError, + OAuthError as OAuthError, OpenAIError, ConflictError, NotFoundError, @@ -29,12 +34,12 @@ AuthenticationError, InternalServerError, PermissionDeniedError, - LengthFinishReasonError, + LengthFinishReasonError as LengthFinishReasonError, WebSocketQueueFullError, UnprocessableEntityError, APIResponseValidationError, - InvalidWebhookSignatureError, - ContentFilterFinishReasonError, + InvalidWebhookSignatureError as InvalidWebhookSignatureError, + ContentFilterFinishReasonError as ContentFilterFinishReasonError, WebSocketConnectionClosedError, ) from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient @@ -114,7 +119,16 @@ from .lib.bedrock import BedrockOpenAI as BedrockOpenAI, AsyncBedrockOpenAI as AsyncBedrockOpenAI from .lib._old_api import * -_COMPUTER_EXPORTS = ("ComputerToolset", "AsyncComputerToolset", "ComputerDriver", "AsyncComputerDriver") +_COMPUTER_EXPORTS = ( + "ComputerToolset", + "AsyncComputerToolset", + "ComputerDriver", + "AsyncComputerDriver", +) +_STREAMING_EXPORTS = ( + "AssistantEventHandler", + "AsyncAssistantEventHandler", +) if _t.TYPE_CHECKING: from .lib.computer import ( @@ -128,20 +142,24 @@ AsyncAssistantEventHandler as AsyncAssistantEventHandler, ) else: - _STREAMING_EXPORTS = ("AssistantEventHandler", "AsyncAssistantEventHandler") def __getattr__(name: str) -> _t.Any: - if name in _STREAMING_EXPORTS or name in _COMPUTER_EXPORTS: + if name in _COMPUTER_EXPORTS: + from importlib import import_module + + value = getattr(import_module(".lib.computer", __name__), name) + globals()[name] = value + return value + if name in _STREAMING_EXPORTS: from importlib import import_module - module = ".lib.computer" if name in _COMPUTER_EXPORTS else ".lib.streaming" - value = getattr(import_module(module, __name__), name) + value = getattr(import_module(".lib.streaming", __name__), name) globals()[name] = value return value raise AttributeError(f"module {__name__!r} has no attribute {name!r}") def __dir__() -> list[str]: - return sorted(set(globals()) | set(_STREAMING_EXPORTS) | set(_COMPUTER_EXPORTS)) + return sorted(set(globals()) | set(_COMPUTER_EXPORTS) | set(_STREAMING_EXPORTS)) _setup_logging() @@ -160,10 +178,8 @@ def __dir__() -> list[str]: pass __all__ += ["ComputerToolset", "AsyncComputerToolset", "ComputerDriver", "AsyncComputerDriver"] - # ------ Module level client ------ import typing as _t -import typing_extensions as _te import httpx2 as _httpx @@ -191,6 +207,7 @@ def __dir__() -> list[str]: http_client: _httpx.Client | None = None + _ApiType = _te.Literal["openai", "azure", "amazon-bedrock"] api_type: _ApiType | None = _t.cast(_ApiType, _os.environ.get("OPENAI_API_TYPE")) @@ -199,7 +216,6 @@ def __dir__() -> list[str]: azure_endpoint: str | None = _os.environ.get("AZURE_OPENAI_ENDPOINT") -# Keep explicit module configuration separate from Azure's environment fallback. azure_ad_token: str | None = None azure_ad_token_provider: _azure.AzureADTokenProvider | None = None @@ -343,130 +359,79 @@ class _AzureModuleClient(_ModuleClient, AzureOpenAI): # type: ignore class _BedrockModuleClient(_ModuleClient, BedrockOpenAI): # type: ignore @property # type: ignore @override - def api_key(self) -> str | None: - return api_key if api_key is not None else _bedrock_api_key + def api_key( + self, + ) -> str | None: + from .lib._module_runtime import BedrockModuleClient_api_key + + return BedrockModuleClient_api_key(self) @api_key.setter # type: ignore - def api_key(self, value: str | None) -> None: # type: ignore - global _bedrock_api_key + def api_key( + self, + value: str | None, + ) -> None: + from .lib._module_runtime import BedrockModuleClient_api_key_setter - _bedrock_api_key = value + return BedrockModuleClient_api_key_setter(self, value) @override - def _refresh_api_key(self) -> str: - if api_key is not None: - return api_key + def _refresh_api_key( + self, + ) -> str: + from typing import Any as _RuntimeAny, cast as _runtime_cast + + from .lib._module_runtime import BedrockModuleClient_refresh_api_key - return super()._refresh_api_key() + return BedrockModuleClient_refresh_api_key(_runtime_cast(_RuntimeAny, super()).__thisclass__, self) @override - def _legacy_auth_configuration(self) -> _bedrock._LegacyAuthConfiguration: - if api_key is not None: - return ("bearer", api_key) - return super()._legacy_auth_configuration() + def _legacy_auth_configuration( + self, + ) -> _bedrock._LegacyAuthConfiguration: + from typing import Any as _RuntimeAny, cast as _runtime_cast + from .lib._module_runtime import BedrockModuleClient_legacy_auth_configuration -class _AmbiguousModuleClientUsageError(OpenAIError): - def __init__(self) -> None: - super().__init__( - "Ambiguous use of module client; please set `openai.api_type` or the `OPENAI_API_TYPE` environment variable to `openai`, `azure`, or `amazon-bedrock`" - ) + return BedrockModuleClient_legacy_auth_configuration(_runtime_cast(_RuntimeAny, super()).__thisclass__, self) -def _has_openai_credentials() -> bool: - return _os.environ.get("OPENAI_API_KEY") is not None +class _AmbiguousModuleClientUsageError(OpenAIError): # type: ignore[reportUnusedClass] + def __init__( + self, + ) -> None: + from typing import Any as _RuntimeAny, cast as _runtime_cast + from .lib._module_runtime import AmbiguousModuleClientUsageError_init -def _has_azure_credentials() -> bool: - return azure_endpoint is not None or _os.environ.get("AZURE_OPENAI_API_KEY") is not None + return AmbiguousModuleClientUsageError_init(_runtime_cast(_RuntimeAny, super()).__thisclass__, self) -def _has_azure_ad_credentials() -> bool: - return ( - _os.environ.get("AZURE_OPENAI_AD_TOKEN") is not None - or azure_ad_token is not None - or azure_ad_token_provider is not None - ) +def _has_openai_credentials() -> bool: # type: ignore[reportUnusedFunction] + from .lib._module_runtime import has_openai_credentials + + return has_openai_credentials() + + +def _has_azure_credentials() -> bool: # type: ignore[reportUnusedFunction] + from .lib._module_runtime import has_azure_credentials + + return has_azure_credentials() + + +def _has_azure_ad_credentials() -> bool: # type: ignore[reportUnusedFunction] + from .lib._module_runtime import has_azure_ad_credentials + + return has_azure_ad_credentials() _client: OpenAI | None = None def _load_client() -> OpenAI: # type: ignore[reportUnusedFunction] - global _client + from .lib._module_runtime import load_client - if _client is None: - global api_type, azure_endpoint, api_version - - if azure_endpoint is None: - azure_endpoint = _os.environ.get("AZURE_OPENAI_ENDPOINT") - - if api_version is None: - api_version = _os.environ.get("OPENAI_API_VERSION") - - if api_type is None: - has_openai = _has_openai_credentials() - has_azure = _has_azure_credentials() - has_azure_ad = _has_azure_ad_credentials() - - if has_openai and (has_azure or has_azure_ad): - raise _AmbiguousModuleClientUsageError() - - if has_azure or has_azure_ad: - api_type = "azure" - else: - api_type = "openai" - - if api_type == "azure": - _client = _AzureModuleClient( # type: ignore - api_version=api_version, - azure_endpoint=azure_endpoint, - api_key=api_key, - azure_ad_token=azure_ad_token, - azure_ad_token_provider=azure_ad_token_provider, - organization=organization, - base_url=base_url, - timeout=timeout, - max_retries=max_retries, - default_headers=default_headers, - default_query=default_query, - http_client=http_client, - ) - return _client - - if api_type == "amazon-bedrock": - _client = _BedrockModuleClient( # type: ignore - api_key=api_key, - bedrock_token_provider=bedrock_token_provider, - organization=organization, - project=project, - webhook_secret=webhook_secret, - base_url=base_url, - timeout=timeout, - max_retries=max_retries, - default_headers=default_headers, - default_query=default_query, - http_client=http_client, - ) - return _client - - _client = _ModuleClient( - api_key=api_key, - admin_api_key=admin_api_key, - organization=organization, - project=project, - webhook_secret=webhook_secret, - base_url=base_url, - timeout=timeout, - max_retries=max_retries, - default_headers=default_headers, - default_query=default_query, - http_client=http_client, - _enforce_credentials=False, - ) - return _client - - return _client + return load_client() def _reset_client() -> None: # type: ignore[reportUnusedFunction] diff --git a/src/openai/_client.py b/src/openai/_client.py index f656e00012..ae17365e6d 100644 --- a/src/openai/_client.py +++ b/src/openai/_client.py @@ -2,59 +2,81 @@ from __future__ import annotations -import os -from typing import TYPE_CHECKING, Any, Mapping, Callable, Iterable, Awaitable, cast -from typing_extensions import Self, Unpack, override +import os as os +from typing import ( + TYPE_CHECKING as TYPE_CHECKING, + Any as Any, + Mapping as Mapping, + Callable as Callable, + Iterable as Iterable, + Awaitable as Awaitable, + cast as cast, +) +from typing_extensions import Self as Self, Unpack as Unpack, override as override -import httpx2 +import httpx2 as httpx2 -from . import _exceptions -from ._qs import Querystring -from .auth import WorkloadIdentity, WorkloadIdentityAuth, X509WorkloadIdentity +from . import _exceptions as _exceptions +from ._qs import Querystring as Querystring +from .auth import ( + WorkloadIdentity as WorkloadIdentity, + WorkloadIdentityAuth as WorkloadIdentityAuth, + X509WorkloadIdentity as X509WorkloadIdentity, +) from ._types import ( - Omit, - Query, - Headers, - Timeout, - NotGiven, - Transport, - ProxiesTypes, - HttpxSendArgs, - RequestOptions, - omit, - not_given, + Omit as Omit, + Query as Query, + Headers as Headers, + Timeout as Timeout, + NotGiven as NotGiven, + Transport as Transport, + ProxiesTypes as ProxiesTypes, + HttpxSendArgs as HttpxSendArgs, + RequestOptions as RequestOptions, + omit as omit, + not_given as not_given, ) from ._utils import ( - is_given, - is_mapping, - is_mapping_t, - get_async_library, + is_given as is_given, + is_mapping as is_mapping, + is_mapping_t as is_mapping_t, + get_async_library as get_async_library, +) +from ._compat import cached_property as cached_property +from ._httpx2 import ( + normalize_httpx_url as normalize_httpx_url, + is_httpx2_sync_client as is_httpx2_sync_client, + is_httpx2_async_client as is_httpx2_async_client, +) +from ._models import SecurityOptions as SecurityOptions, FinalRequestOptions as FinalRequestOptions +from ._version import __version__ as __version__ +from ._provider import ( + _Provider as _Provider, + _provider_name as _provider_name, + _ProviderRuntime as _ProviderRuntime, + _configure_provider as _configure_provider, ) -from ._compat import cached_property -from ._httpx2 import normalize_httpx_url, is_httpx2_sync_client, is_httpx2_async_client -from ._models import SecurityOptions, FinalRequestOptions -from ._version import __version__ -from ._provider import _Provider, _provider_name, _ProviderRuntime, _configure_provider from ._streaming import Stream as Stream, AsyncStream as AsyncStream from .auth._x509 import ( - MTLS_API_BASE_URL, - SyncX509WorkloadIdentityAuth, - AsyncX509WorkloadIdentityAuth, - validate_x509_api_url, - is_x509_workload_identity, - x509_data_residency_base_url, - validate_x509_api_credentials, - x509_safe_environment_headers, - validate_x509_request_authority, + MTLS_API_BASE_URL as MTLS_API_BASE_URL, + SyncX509WorkloadIdentityAuth as SyncX509WorkloadIdentityAuth, + AsyncX509WorkloadIdentityAuth as AsyncX509WorkloadIdentityAuth, + validate_x509_api_url as validate_x509_api_url, + is_x509_workload_identity as is_x509_workload_identity, + x509_data_residency_base_url as x509_data_residency_base_url, + validate_x509_api_credentials as validate_x509_api_credentials, + x509_safe_environment_headers as x509_safe_environment_headers, + validate_x509_request_authority as validate_x509_request_authority, ) -from ._exceptions import OpenAIError, APIStatusError +from ._exceptions import OpenAIError as OpenAIError, APIStatusError as APIStatusError from ._base_client import ( - DEFAULT_MAX_RETRIES, - SyncAPIClient, - AsyncAPIClient, - _merge_mappings, + DEFAULT_MAX_RETRIES as DEFAULT_MAX_RETRIES, + SyncAPIClient as SyncAPIClient, + AsyncAPIClient as AsyncAPIClient, + _merge_mappings as _merge_mappings, ) -from ._data_residency import DataResidency, resolve_data_residency +from ._data_residency import DataResidency as DataResidency, resolve_data_residency as resolve_data_residency +from .lib._client_auth import _has_header as _has_header, _has_omitted_header as _has_omitted_header if TYPE_CHECKING: from .resources import ( @@ -117,30 +139,22 @@ WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER = "workload-identity-auth" -def _has_header(headers: Headers, header: str) -> bool: - header = header.lower() - return any(key.lower() == header for key in headers) - - -def _has_omitted_header(headers: Headers, header: str) -> bool: - header = header.lower() - return any(key.lower() == header and isinstance(value, Omit) for key, value in headers.items()) - - class OpenAI(SyncAPIClient): - # client options - api_key: str - admin_api_key: str | None workload_identity: WorkloadIdentity | X509WorkloadIdentity | None - organization: str | None - project: str | None - webhook_secret: str | None _workload_identity_auth: WorkloadIdentityAuth | SyncX509WorkloadIdentityAuth | None _provider: _Provider | None _provider_runtime: _ProviderRuntime | None _base_url_was_default: bool _data_residency: DataResidency | None _ambient_authorizations: frozenset[str] + _api_key_provider: Callable[[], str] | None + + # client options + api_key: str + admin_api_key: str | None + organization: str | None + project: str | None + webhook_secret: str | None websocket_base_url: str | httpx2.URL | None """Base URL for WebSocket connections. @@ -152,17 +166,21 @@ class OpenAI(SyncAPIClient): @property @override - def base_url(self) -> httpx2.URL: - return self._base_url + def base_url( + self, + ) -> httpx2.URL: + from .lib._client_auth import syncbase_url + + return syncbase_url(self) @base_url.setter - def base_url(self, url: httpx2.URL | str) -> None: - normalized_url = normalize_httpx_url(url) - if is_x509_workload_identity(self.workload_identity): - validate_x509_api_url(normalized_url) - self._base_url = self._enforce_trailing_slash(normalized_url) - self._base_url_was_default = False - self._data_residency = None + def base_url( + self, + url: httpx2.URL | str, + ) -> None: + from .lib._client_auth import syncbase_url_set + + return syncbase_url_set(self, url) def _websocket_query_params_fallback(self, url: httpx2.URL, extra_query: Query) -> httpx2.QueryParams: # Remove replaced logical defaults before encoding nested query values. @@ -199,18 +217,7 @@ def __init__( max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, - # Configure a custom httpx2 client. - # We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. - # See the [httpx2 documentation](https://httpx2.pydantic.dev/api/#client) for more details. http_client: httpx2.Client | None = None, - # Enable or disable schema validation for data returned by the API. - # When enabled an error APIResponseValidationError is raised - # if the API responds with invalid data for the expected schema. - # - # This parameter may be removed or changed in the future. - # If you rely on this feature, please open a GitHub issue - # outlining your use-case to help us decide if it should be - # part of our public interface in the future. _strict_response_validation: bool = False, _enforce_credentials: bool = True, ) -> None: @@ -227,157 +234,32 @@ def __init__( `data_residency` selects an OpenAI regional endpoint and cannot be combined with `base_url`, `websocket_base_url`, or `provider`. """ - base_url = resolve_data_residency( - data_residency, base_url, provider=provider, websocket_base_url=websocket_base_url - ) - base_url = x509_data_residency_base_url(base_url, data_residency, workload_identity) - provider_runtime: _ProviderRuntime | None = None - if provider is not None: - provider_name = _provider_name(provider) - conflicts = [ - name - for name, value in ( - ("api_key", api_key), - ("admin_api_key", admin_api_key), - ("workload_identity", workload_identity), - ("base_url", base_url), - ) - if value is not None - ] - if conflicts: - formatted = ", ".join(f"`{name}`" for name in conflicts) - raise OpenAIError( - f"`provider` cannot be combined with top-level {formatted}. " - f"Move provider authentication and routing options into `{provider_name}(...)`." - ) - - provider_runtime = _configure_provider(provider) - - self._provider = provider - self._provider_runtime = provider_runtime - - if api_key is not None and api_key != WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER and workload_identity is not None: - raise OpenAIError("The `api_key` and `workload_identity` arguments are mutually exclusive") - - if is_x509_workload_identity(workload_identity): - workload_identity = workload_identity.copy() - self.workload_identity = workload_identity if provider_runtime is None else None - - if provider_runtime is not None: - self.api_key = "" - self._api_key_provider = None - self._workload_identity_auth = None - elif workload_identity is not None: - self.api_key = WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER - self._api_key_provider = None - self._workload_identity_auth = None - else: - if api_key is None: - api_key = os.environ.get("OPENAI_API_KEY") - if callable(api_key): - self.api_key = "" - self._api_key_provider: Callable[[], str] | None = api_key # type: ignore[no-redef] - else: - self.api_key = api_key or "" - self._api_key_provider = None - self._workload_identity_auth = None - - if admin_api_key is None and provider_runtime is None: - admin_api_key = os.environ.get("OPENAI_ADMIN_KEY") - self.admin_api_key = admin_api_key if provider_runtime is None else None - - if ( - provider_runtime is None - and _enforce_credentials - and not self.api_key - and self._api_key_provider is None - and workload_identity is None - and self.admin_api_key is None - ): - raise OpenAIError( - "Missing credentials. Please pass an `api_key`, `workload_identity`, `admin_api_key`, or set the `OPENAI_API_KEY` or `OPENAI_ADMIN_KEY` environment variable." - ) - - if organization is None and provider_runtime is None: - organization = os.environ.get("OPENAI_ORG_ID") - self.organization = organization - - if project is None and provider_runtime is None: - project = os.environ.get("OPENAI_PROJECT_ID") - self.project = project - - if webhook_secret is None: - webhook_secret = os.environ.get("OPENAI_WEBHOOK_SECRET") - self.webhook_secret = webhook_secret - - self.websocket_base_url = websocket_base_url - - if is_x509_workload_identity(workload_identity): - x509_identity = workload_identity - subject_token_identity = None - elif workload_identity is None: - x509_identity = None - subject_token_identity = None - elif "provider" in workload_identity: - x509_identity = None - subject_token_identity = workload_identity - else: - raise OpenAIError("Invalid `workload_identity` configuration: expected an X.509 or subject-token identity") - if provider_runtime is not None: - base_url = provider_runtime.base_url - elif base_url is None: - base_url = os.environ.get("OPENAI_BASE_URL") - self._base_url_was_default = provider_runtime is None and base_url is None - self._data_residency = data_residency - if base_url is None: - base_url = MTLS_API_BASE_URL if x509_identity is not None else "https://api.openai.com/v1" - if x509_identity is not None: - validate_x509_api_url(base_url) - - self._ambient_authorizations = frozenset() - custom_headers_env = os.environ.get("OPENAI_CUSTOM_HEADERS") if provider_runtime is None else None - if custom_headers_env is not None: - parsed: dict[str, str] = {} - for line in custom_headers_env.split("\n"): - colon = line.find(":") - if colon >= 0: - parsed[line[:colon].strip()] = line[colon + 1 :].strip() - explicit_headers: Mapping[str, str] = default_headers if is_mapping_t(default_headers) else {} - explicit_authorization = any(name.lower() == "authorization" for name in explicit_headers) - if explicit_authorization: - parsed = {name: value for name, value in parsed.items() if name.lower() != "authorization"} - elif x509_identity is None: - self._ambient_authorizations = frozenset( - value for name, value in parsed.items() if name.lower() == "authorization" - ) - default_headers = { - **x509_safe_environment_headers(parsed, x509_identity), - **explicit_headers, - } - - super().__init__( - version=__version__, + from typing import Any as _RuntimeAny, cast as _runtime_cast + + from .lib._client_auth import sync__init__ + + return sync__init__( + _runtime_cast(_RuntimeAny, super()).__thisclass__, + self, + api_key=api_key, + admin_api_key=admin_api_key, + workload_identity=workload_identity, + organization=organization, + project=project, + webhook_secret=webhook_secret, + provider=provider, base_url=base_url, - max_retries=max_retries, + data_residency=data_residency, + websocket_base_url=websocket_base_url, timeout=timeout, + max_retries=max_retries, + default_headers=default_headers, + default_query=default_query, http_client=http_client, - custom_headers=default_headers, - custom_query=default_query, _strict_response_validation=_strict_response_validation, + _enforce_credentials=_enforce_credentials, ) - if x509_identity is not None: - self._workload_identity_auth = SyncX509WorkloadIdentityAuth( - workload_identity=x509_identity.copy(), http_client=self._client, max_retries=max_retries - ) - elif subject_token_identity is not None: - self._workload_identity_auth = WorkloadIdentityAuth( - workload_identity=subject_token_identity, - _use_httpx2=is_httpx2_sync_client(self._client), - ) - - self._default_stream_cls = Stream - @cached_property def decisions(self) -> Decisions: from .resources.decisions import Decisions @@ -574,50 +456,13 @@ def _send_with_auth_retry( retried: bool = False, **kwargs: Unpack[HttpxSendArgs], ) -> httpx2.Response: - used_access_token: str | None = None - request_is_replayable = False - x509_auth = self._workload_identity_auth - - if x509_auth is not None: - if isinstance(x509_auth, SyncX509WorkloadIdentityAuth): - if x509_auth.workload_identity != self.workload_identity: - raise OpenAIError("X.509 workload identity cannot be changed after client construction") - validate_x509_api_url(request.url, expected_origin=self.base_url) - validate_x509_request_authority(request) - validate_x509_api_credentials(request) - if x509_auth._follow_redirects is not None: - kwargs["follow_redirects"] = x509_auth._follow_redirects - authorization = request.headers.get("Authorization") - if authorization == f"Bearer {WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER}": - used_access_token = ( - x509_auth.get_token_for_request(request) - if isinstance(x509_auth, SyncX509WorkloadIdentityAuth) - else x509_auth.get_token() - ) - request.headers["Authorization"] = f"Bearer {used_access_token}" - request_is_replayable = x509_auth._can_retry_request(request) - - if isinstance(x509_auth, SyncX509WorkloadIdentityAuth): - response = x509_auth.send_api_request( - request, - expected_origin=self.base_url, - expected_authorization=request.headers.get("Authorization"), - stream=stream, - **kwargs, - ) - else: - response = super()._send_request(request, stream=stream, **kwargs) - if response.status_code != 401 or self._workload_identity_auth is None or used_access_token is None: - return response - - self._workload_identity_auth.invalidate_token(used_access_token) - if retried or not request_is_replayable: - return response - - response.close() - self._workload_identity_auth._prepare_retry_request(request) - request.headers["Authorization"] = f"Bearer {WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER}" - return self._send_with_auth_retry(request, stream=stream, retried=True, **kwargs) + from typing import Any as _RuntimeAny, cast as _runtime_cast + + from .lib._client_auth import sync_send_with_auth_retry + + return sync_send_with_auth_retry( + _runtime_cast(_RuntimeAny, super()).__thisclass__, self, request, stream=stream, retried=retried, **kwargs + ) @override def _send_request( @@ -627,42 +472,35 @@ def _send_request( stream: bool, **kwargs: Unpack[HttpxSendArgs], ) -> httpx2.Response: - response = self._send_with_auth_retry(request, stream=stream, **kwargs) - if self._provider_runtime is not None and self._provider_runtime.normalize_response is not None: - response = self._provider_runtime.normalize_response(response) - return response + from .lib._client_auth import sync_send_request + + return sync_send_request(self, request, stream=stream, **kwargs) @override - def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: - if self._provider_runtime is not None: - return {} + def _auth_headers( + self, + security: SecurityOptions, + ) -> dict[str, str]: + from .lib._client_auth import sync_auth_headers - headers: dict[str, str] = {} - if security.get("bearer_auth", False): - for key, value in self._bearer_auth.items(): - headers.setdefault(key, value) - if security.get("admin_api_key_auth", False): - for key, value in self._admin_api_key_auth.items(): - headers.setdefault(key, value) - return headers + return sync_auth_headers(self, security) @property - def _bearer_auth(self) -> dict[str, str]: - api_key = self.api_key - if not api_key: - return {} - return {"Authorization": f"Bearer {api_key}"} + def _bearer_auth( + self, + ) -> dict[str, str]: + from .lib._client_auth import sync_bearer_auth + + return sync_bearer_auth(self) @property @override - def auth_headers(self) -> dict[str, str]: - if self._provider_runtime is not None: - return {} + def auth_headers( + self, + ) -> dict[str, str]: + from .lib._client_auth import syncauth_headers - api_key = self.api_key - if not api_key or api_key == WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER: - return {} - return {"Authorization": f"Bearer {api_key}"} + return syncauth_headers(self) @property def _admin_api_key_auth(self) -> dict[str, str]: @@ -683,49 +521,52 @@ def default_headers(self) -> dict[str, str | Omit]: } @override - def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: - if self._provider_runtime is not None: - return - - if _has_header(headers, "Authorization") or _has_omitted_header(custom_headers, "Authorization"): - return + def _validate_headers( + self, + headers: Headers, + custom_headers: Headers, + ) -> None: + from .lib._client_auth import sync_validate_headers - raise TypeError( - "Could not resolve authentication method. Expected either api_key or admin_api_key to be set. Or for the `Authorization` header to be explicitly omitted." - ) + return sync_validate_headers(self, headers, custom_headers) @override - def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions: - if options.url.startswith(("ws://", "wss://")) and ( - self._provider_runtime is not None or self.workload_identity is not None - ): - raise OpenAIError("This authentication method is not supported by WebSocket connections") + def _prepare_options( + self, + options: FinalRequestOptions, + ) -> FinalRequestOptions: + from typing import Any as _RuntimeAny, cast as _runtime_cast - if self._provider_runtime is not None: - if self._provider_runtime.transform_request is not None: - options = self._provider_runtime.transform_request(options) - elif self._api_key_provider is not None and options.security.get("bearer_auth", False): - self._refresh_api_key() + from .lib._client_auth import sync_prepare_options - return super()._prepare_options(options) + return sync_prepare_options(_runtime_cast(_RuntimeAny, super()).__thisclass__, self, options) @override - def _prepare_request(self, request: httpx2.Request) -> None: - if self._provider_runtime is not None and self._provider_runtime.prepare_request is not None: - self._provider_runtime.prepare_request(request) + def _prepare_request( + self, + request: httpx2.Request, + ) -> None: + from .lib._client_auth import sync_prepare_request + + return sync_prepare_request(self, request) @override - def _custom_auth(self, security: SecurityOptions) -> httpx2.Auth | None: - if self._provider_runtime is not None or isinstance(self._workload_identity_auth, SyncX509WorkloadIdentityAuth): - return httpx2.Auth() + def _custom_auth( + self, + security: SecurityOptions, + ) -> httpx2.Auth | None: + from typing import Any as _RuntimeAny, cast as _runtime_cast - return super()._custom_auth(security) + from .lib._client_auth import sync_custom_auth - def _refresh_api_key(self) -> str: - if self._api_key_provider is not None: - self.api_key = self._api_key_provider() + return sync_custom_auth(_runtime_cast(_RuntimeAny, super()).__thisclass__, self, security) - return self.api_key + def _refresh_api_key( + self, + ) -> str: + from .lib._client_auth import sync_refresh_api_key + + return sync_refresh_api_key(self) def copy( self, @@ -754,123 +595,30 @@ def copy( Create a new client instance re-using the same options given to the current client with optional overriding. `data_residency` replaces the inherited HTTP and WebSocket endpoints, without changing this client. """ - if default_headers is not None and set_default_headers is not None: - raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") - - if default_query is not None and set_default_query is not None: - raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") - - provider_changed = not isinstance(provider, NotGiven) and provider is not self._provider - inherited_organization = None if provider_changed else self.organization - inherited_project = None if provider_changed else self.project - - headers: Mapping[str, str] = {} if provider_changed else self._custom_headers - if ( - is_x509_workload_identity(workload_identity) - and not is_x509_workload_identity(self.workload_identity) - and self._ambient_authorizations - ): - headers = { - name: value - for name, value in headers.items() - if name.lower() != "authorization" or value not in self._ambient_authorizations - } - if default_headers is not None: - if any(name.lower() == "authorization" for name in default_headers): - headers = {name: value for name, value in headers.items() if name.lower() != "authorization"} - headers = {**headers, **default_headers} - elif set_default_headers is not None: - headers = set_default_headers - - params = self._custom_query - if default_query is not None: - params = {**params, **default_query} - elif set_default_query is not None: - params = set_default_query - - http_client = http_client or self._client - - next_provider = self._provider if isinstance(provider, NotGiven) else provider - explicit_base_url = base_url is not None and not isinstance(base_url, NotGiven) - next_workload_identity = workload_identity if workload_identity is not None else self.workload_identity - if api_key is not None and workload_identity is None: - next_workload_identity = None - current_x509 = is_x509_workload_identity(self.workload_identity) - next_x509 = is_x509_workload_identity(next_workload_identity) - mode_changed = current_x509 != next_x509 - effective_data_residency = data_residency - if effective_data_residency is None and mode_changed and not explicit_base_url: - effective_data_residency = self._data_residency - base_url = resolve_data_residency( - effective_data_residency, - not_given if base_url is None and data_residency is None else base_url, - provider=next_provider, + from .lib._client_auth import synccopy + + return synccopy( + self, + api_key=api_key, + admin_api_key=admin_api_key, + workload_identity=workload_identity, + provider=provider, + organization=organization, + project=project, + webhook_secret=webhook_secret, websocket_base_url=websocket_base_url, - ) - base_url = x509_data_residency_base_url(base_url, effective_data_residency, next_workload_identity) - preserve_default_base_url = False - auth_options: dict[str, Any] - if next_provider is not None: - auth_options = { - "provider": next_provider, - "api_key": api_key, - "admin_api_key": admin_api_key, - "workload_identity": workload_identity, - "base_url": base_url, - } - elif self._provider is not None: - auth_options = { - "api_key": api_key, - "admin_api_key": admin_api_key, - "workload_identity": workload_identity, - "base_url": base_url, - } - else: - inherited_base_url = None if mode_changed and self._base_url_was_default else self.base_url - preserve_default_base_url = base_url is None and not mode_changed and self._base_url_was_default - auth_options = { - "api_key": api_key - if workload_identity is not None - else api_key or self._api_key_provider or self.api_key, - "admin_api_key": admin_api_key or self.admin_api_key, - "workload_identity": next_workload_identity, - "base_url": base_url or inherited_base_url, - } - - copied = self.__class__( - organization=organization or inherited_organization, - project=project or inherited_project, - webhook_secret=webhook_secret or self.webhook_secret, - websocket_base_url=None if data_residency is not None else websocket_base_url or self.websocket_base_url, - timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + base_url=base_url, + data_residency=data_residency, + timeout=timeout, http_client=http_client, - max_retries=max_retries if is_given(max_retries) else self.max_retries, - default_headers=headers, - default_query=params, - _enforce_credentials=True if _enforce_credentials is None else _enforce_credentials, - **auth_options, - **_extra_kwargs, - ) - if preserve_default_base_url: - copied._base_url_was_default = True - overridden_authorizations = default_headers if default_headers is not None else set_default_headers - explicit_authorization_override = overridden_authorizations is not None and any( - name.lower() == "authorization" for name in overridden_authorizations + max_retries=max_retries, + default_headers=default_headers, + set_default_headers=set_default_headers, + default_query=default_query, + set_default_query=set_default_query, + _enforce_credentials=_enforce_credentials, + _extra_kwargs=_extra_kwargs, ) - if ( - self._ambient_authorizations - and not explicit_authorization_override - and any( - name.lower() == "authorization" and value in self._ambient_authorizations - for name, value in copied._custom_headers.items() - ) - ): - copied._ambient_authorizations = self._ambient_authorizations - if data_residency is not None: - copied._data_residency = data_residency - elif not explicit_base_url and not provider_changed: - copied._data_residency = self._data_residency - return copied # Alias for `copy` for nicer inline usage, e.g. # client.with_options(timeout=10).foo.create(...) @@ -912,19 +660,21 @@ def _make_status_error( class AsyncOpenAI(AsyncAPIClient): - # client options - api_key: str - admin_api_key: str | None workload_identity: WorkloadIdentity | X509WorkloadIdentity | None - organization: str | None - project: str | None - webhook_secret: str | None _workload_identity_auth: WorkloadIdentityAuth | AsyncX509WorkloadIdentityAuth | None _provider: _Provider | None _provider_runtime: _ProviderRuntime | None _base_url_was_default: bool _data_residency: DataResidency | None _ambient_authorizations: frozenset[str] + _api_key_provider: Callable[[], Awaitable[str]] | None + + # client options + api_key: str + admin_api_key: str | None + organization: str | None + project: str | None + webhook_secret: str | None websocket_base_url: str | httpx2.URL | None """Base URL for WebSocket connections. @@ -936,17 +686,21 @@ class AsyncOpenAI(AsyncAPIClient): @property @override - def base_url(self) -> httpx2.URL: - return self._base_url + def base_url( + self, + ) -> httpx2.URL: + from .lib._client_auth import asyncbase_url + + return asyncbase_url(self) @base_url.setter - def base_url(self, url: httpx2.URL | str) -> None: - normalized_url = normalize_httpx_url(url) - if is_x509_workload_identity(self.workload_identity): - validate_x509_api_url(normalized_url) - self._base_url = self._enforce_trailing_slash(normalized_url) - self._base_url_was_default = False - self._data_residency = None + def base_url( + self, + url: httpx2.URL | str, + ) -> None: + from .lib._client_auth import asyncbase_url_set + + return asyncbase_url_set(self, url) def _websocket_query_params_fallback(self, url: httpx2.URL, extra_query: Query) -> httpx2.QueryParams: # Remove replaced logical defaults before encoding nested query values. @@ -983,18 +737,7 @@ def __init__( max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, - # Configure a custom httpx2 client. - # We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. - # See the [httpx2 documentation](https://httpx2.pydantic.dev/api/#asyncclient) for more details. http_client: httpx2.AsyncClient | None = None, - # Enable or disable schema validation for data returned by the API. - # When enabled an error APIResponseValidationError is raised - # if the API responds with invalid data for the expected schema. - # - # This parameter may be removed or changed in the future. - # If you rely on this feature, please open a GitHub issue - # outlining your use-case to help us decide if it should be - # part of our public interface in the future. _strict_response_validation: bool = False, _enforce_credentials: bool = True, ) -> None: @@ -1011,157 +754,32 @@ def __init__( `data_residency` selects an OpenAI regional endpoint and cannot be combined with `base_url`, `websocket_base_url`, or `provider`. """ - base_url = resolve_data_residency( - data_residency, base_url, provider=provider, websocket_base_url=websocket_base_url - ) - base_url = x509_data_residency_base_url(base_url, data_residency, workload_identity) - provider_runtime: _ProviderRuntime | None = None - if provider is not None: - provider_name = _provider_name(provider) - conflicts = [ - name - for name, value in ( - ("api_key", api_key), - ("admin_api_key", admin_api_key), - ("workload_identity", workload_identity), - ("base_url", base_url), - ) - if value is not None - ] - if conflicts: - formatted = ", ".join(f"`{name}`" for name in conflicts) - raise OpenAIError( - f"`provider` cannot be combined with top-level {formatted}. " - f"Move provider authentication and routing options into `{provider_name}(...)`." - ) - - provider_runtime = _configure_provider(provider) - - self._provider = provider - self._provider_runtime = provider_runtime - - if api_key is not None and api_key != WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER and workload_identity is not None: - raise OpenAIError("The `api_key` and `workload_identity` arguments are mutually exclusive") - - if is_x509_workload_identity(workload_identity): - workload_identity = workload_identity.copy() - self.workload_identity = workload_identity if provider_runtime is None else None - - if provider_runtime is not None: - self.api_key = "" - self._api_key_provider = None - self._workload_identity_auth = None - elif workload_identity is not None: - self.api_key = WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER - self._api_key_provider = None - self._workload_identity_auth = None - else: - if api_key is None: - api_key = os.environ.get("OPENAI_API_KEY") - if callable(api_key): - self.api_key = "" - self._api_key_provider: Callable[[], Awaitable[str]] | None = api_key # type: ignore[no-redef] - else: - self.api_key = api_key or "" - self._api_key_provider = None - self._workload_identity_auth = None - - if admin_api_key is None and provider_runtime is None: - admin_api_key = os.environ.get("OPENAI_ADMIN_KEY") - self.admin_api_key = admin_api_key if provider_runtime is None else None - - if ( - provider_runtime is None - and _enforce_credentials - and not self.api_key - and self._api_key_provider is None - and workload_identity is None - and self.admin_api_key is None - ): - raise OpenAIError( - "Missing credentials. Please pass an `api_key`, `workload_identity`, `admin_api_key`, or set the `OPENAI_API_KEY` or `OPENAI_ADMIN_KEY` environment variable." - ) - - if organization is None and provider_runtime is None: - organization = os.environ.get("OPENAI_ORG_ID") - self.organization = organization - - if project is None and provider_runtime is None: - project = os.environ.get("OPENAI_PROJECT_ID") - self.project = project - - if webhook_secret is None: - webhook_secret = os.environ.get("OPENAI_WEBHOOK_SECRET") - self.webhook_secret = webhook_secret - - self.websocket_base_url = websocket_base_url - - if is_x509_workload_identity(workload_identity): - x509_identity = workload_identity - subject_token_identity = None - elif workload_identity is None: - x509_identity = None - subject_token_identity = None - elif "provider" in workload_identity: - x509_identity = None - subject_token_identity = workload_identity - else: - raise OpenAIError("Invalid `workload_identity` configuration: expected an X.509 or subject-token identity") - if provider_runtime is not None: - base_url = provider_runtime.base_url - elif base_url is None: - base_url = os.environ.get("OPENAI_BASE_URL") - self._base_url_was_default = provider_runtime is None and base_url is None - self._data_residency = data_residency - if base_url is None: - base_url = MTLS_API_BASE_URL if x509_identity is not None else "https://api.openai.com/v1" - if x509_identity is not None: - validate_x509_api_url(base_url) - - self._ambient_authorizations = frozenset() - custom_headers_env = os.environ.get("OPENAI_CUSTOM_HEADERS") if provider_runtime is None else None - if custom_headers_env is not None: - parsed: dict[str, str] = {} - for line in custom_headers_env.split("\n"): - colon = line.find(":") - if colon >= 0: - parsed[line[:colon].strip()] = line[colon + 1 :].strip() - explicit_headers: Mapping[str, str] = default_headers if is_mapping_t(default_headers) else {} - explicit_authorization = any(name.lower() == "authorization" for name in explicit_headers) - if explicit_authorization: - parsed = {name: value for name, value in parsed.items() if name.lower() != "authorization"} - elif x509_identity is None: - self._ambient_authorizations = frozenset( - value for name, value in parsed.items() if name.lower() == "authorization" - ) - default_headers = { - **x509_safe_environment_headers(parsed, x509_identity), - **explicit_headers, - } - - super().__init__( - version=__version__, + from typing import Any as _RuntimeAny, cast as _runtime_cast + + from .lib._client_auth import async__init__ + + return async__init__( + _runtime_cast(_RuntimeAny, super()).__thisclass__, + self, + api_key=api_key, + admin_api_key=admin_api_key, + workload_identity=workload_identity, + organization=organization, + project=project, + webhook_secret=webhook_secret, + provider=provider, base_url=base_url, - max_retries=max_retries, + data_residency=data_residency, + websocket_base_url=websocket_base_url, timeout=timeout, + max_retries=max_retries, + default_headers=default_headers, + default_query=default_query, http_client=http_client, - custom_headers=default_headers, - custom_query=default_query, _strict_response_validation=_strict_response_validation, + _enforce_credentials=_enforce_credentials, ) - if x509_identity is not None: - self._workload_identity_auth = AsyncX509WorkloadIdentityAuth( - workload_identity=x509_identity.copy(), http_client=self._client, max_retries=max_retries - ) - elif subject_token_identity is not None: - self._workload_identity_auth = WorkloadIdentityAuth( - workload_identity=subject_token_identity, - _use_httpx2=is_httpx2_async_client(self._client), - ) - - self._default_stream_cls = AsyncStream - @cached_property def decisions(self) -> AsyncDecisions: from .resources.decisions import AsyncDecisions @@ -1358,50 +976,13 @@ async def _send_with_auth_retry( retried: bool = False, **kwargs: Unpack[HttpxSendArgs], ) -> httpx2.Response: - used_access_token: str | None = None - request_is_replayable = False - x509_auth = self._workload_identity_auth - - if x509_auth is not None: - if isinstance(x509_auth, AsyncX509WorkloadIdentityAuth): - if x509_auth.workload_identity != self.workload_identity: - raise OpenAIError("X.509 workload identity cannot be changed after client construction") - validate_x509_api_url(request.url, expected_origin=self.base_url) - validate_x509_request_authority(request) - validate_x509_api_credentials(request) - if x509_auth._follow_redirects is not None: - kwargs["follow_redirects"] = x509_auth._follow_redirects - authorization = request.headers.get("Authorization") - if authorization == f"Bearer {WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER}": - used_access_token = ( - await x509_auth.get_token_for_request(request) - if isinstance(x509_auth, AsyncX509WorkloadIdentityAuth) - else await x509_auth.get_token_async() - ) - request.headers["Authorization"] = f"Bearer {used_access_token}" - request_is_replayable = x509_auth._can_retry_request(request) - - if isinstance(x509_auth, AsyncX509WorkloadIdentityAuth): - response = await x509_auth.send_api_request( - request, - expected_origin=self.base_url, - expected_authorization=request.headers.get("Authorization"), - stream=stream, - **kwargs, - ) - else: - response = await super()._send_request(request, stream=stream, **kwargs) - if response.status_code != 401 or self._workload_identity_auth is None or used_access_token is None: - return response - - self._workload_identity_auth.invalidate_token(used_access_token) - if retried or not request_is_replayable: - return response - - await response.aclose() - self._workload_identity_auth._prepare_retry_request(request) - request.headers["Authorization"] = f"Bearer {WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER}" - return await self._send_with_auth_retry(request, stream=stream, retried=True, **kwargs) + from typing import Any as _RuntimeAny, cast as _runtime_cast + + from .lib._client_auth import async_send_with_auth_retry + + return await async_send_with_auth_retry( + _runtime_cast(_RuntimeAny, super()).__thisclass__, self, request, stream=stream, retried=retried, **kwargs + ) @override async def _send_request( @@ -1411,45 +992,35 @@ async def _send_request( stream: bool, **kwargs: Unpack[HttpxSendArgs], ) -> httpx2.Response: - response = await self._send_with_auth_retry(request, stream=stream, **kwargs) - if self._provider_runtime is not None: - if self._provider_runtime.normalize_async_response is not None: - response = await self._provider_runtime.normalize_async_response(response) - elif self._provider_runtime.normalize_response is not None: - response = self._provider_runtime.normalize_response(response) - return response + from .lib._client_auth import async_send_request + + return await async_send_request(self, request, stream=stream, **kwargs) @override - def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: - if self._provider_runtime is not None: - return {} + def _auth_headers( + self, + security: SecurityOptions, + ) -> dict[str, str]: + from .lib._client_auth import async_auth_headers - headers: dict[str, str] = {} - if security.get("bearer_auth", False): - for key, value in self._bearer_auth.items(): - headers.setdefault(key, value) - if security.get("admin_api_key_auth", False): - for key, value in self._admin_api_key_auth.items(): - headers.setdefault(key, value) - return headers + return async_auth_headers(self, security) @property - def _bearer_auth(self) -> dict[str, str]: - api_key = self.api_key - if not api_key: - return {} - return {"Authorization": f"Bearer {api_key}"} + def _bearer_auth( + self, + ) -> dict[str, str]: + from .lib._client_auth import async_bearer_auth + + return async_bearer_auth(self) @property @override - def auth_headers(self) -> dict[str, str]: - if self._provider_runtime is not None: - return {} + def auth_headers( + self, + ) -> dict[str, str]: + from .lib._client_auth import asyncauth_headers - api_key = self.api_key - if not api_key or api_key == WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER: - return {} - return {"Authorization": f"Bearer {api_key}"} + return asyncauth_headers(self) @property def _admin_api_key_auth(self) -> dict[str, str]: @@ -1470,59 +1041,52 @@ def default_headers(self) -> dict[str, str | Omit]: } @override - def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: - if self._provider_runtime is not None: - return - - if _has_header(headers, "Authorization") or _has_omitted_header(custom_headers, "Authorization"): - return + def _validate_headers( + self, + headers: Headers, + custom_headers: Headers, + ) -> None: + from .lib._client_auth import async_validate_headers - raise TypeError( - "Could not resolve authentication method. Expected either api_key or admin_api_key to be set. Or for the `Authorization` header to be explicitly omitted." - ) + return async_validate_headers(self, headers, custom_headers) @override - async def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions: - if options.url.startswith(("ws://", "wss://")) and ( - self._provider_runtime is not None or self.workload_identity is not None - ): - raise OpenAIError("This authentication method is not supported by WebSocket connections") - - if self._provider_runtime is not None: - if self._provider_runtime.transform_async_request is not None: - options = await self._provider_runtime.transform_async_request(options) - elif self._provider_runtime.transform_request is not None: - options = self._provider_runtime.transform_request(options) - elif self._api_key_provider is not None and options.security.get("bearer_auth", False): - await self._refresh_api_key() - - return await super()._prepare_options(options) + async def _prepare_options( + self, + options: FinalRequestOptions, + ) -> FinalRequestOptions: + from typing import Any as _RuntimeAny, cast as _runtime_cast + + from .lib._client_auth import async_prepare_options + + return await async_prepare_options(_runtime_cast(_RuntimeAny, super()).__thisclass__, self, options) @override - async def _prepare_request(self, request: httpx2.Request) -> None: - if self._provider_runtime is None: - return + async def _prepare_request( + self, + request: httpx2.Request, + ) -> None: + from .lib._client_auth import async_prepare_request - if self._provider_runtime.prepare_async_request is not None: - await self._provider_runtime.prepare_async_request(request) - elif self._provider_runtime.prepare_request is not None: - self._provider_runtime.prepare_request(request) + return await async_prepare_request(self, request) @property @override - def custom_auth(self) -> httpx2.Auth | None: - if self._provider_runtime is not None or isinstance( - self._workload_identity_auth, AsyncX509WorkloadIdentityAuth - ): - return httpx2.Auth() + def custom_auth( + self, + ) -> httpx2.Auth | None: + from typing import Any as _RuntimeAny, cast as _runtime_cast - return super().custom_auth + from .lib._client_auth import asynccustom_auth - async def _refresh_api_key(self) -> str: - if self._api_key_provider is not None: - self.api_key = await self._api_key_provider() + return asynccustom_auth(_runtime_cast(_RuntimeAny, super()).__thisclass__, self) - return self.api_key + async def _refresh_api_key( + self, + ) -> str: + from .lib._client_auth import async_refresh_api_key + + return await async_refresh_api_key(self) def copy( self, @@ -1551,122 +1115,30 @@ def copy( Create a new client instance re-using the same options given to the current client with optional overriding. `data_residency` replaces the inherited HTTP and WebSocket endpoints, without changing this client. """ - if default_headers is not None and set_default_headers is not None: - raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") - - if default_query is not None and set_default_query is not None: - raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") - - provider_changed = not isinstance(provider, NotGiven) and provider is not self._provider - inherited_organization = None if provider_changed else self.organization - inherited_project = None if provider_changed else self.project - - headers: Mapping[str, str] = {} if provider_changed else self._custom_headers - if ( - is_x509_workload_identity(workload_identity) - and not is_x509_workload_identity(self.workload_identity) - and self._ambient_authorizations - ): - headers = { - name: value - for name, value in headers.items() - if name.lower() != "authorization" or value not in self._ambient_authorizations - } - if default_headers is not None: - if any(name.lower() == "authorization" for name in default_headers): - headers = {name: value for name, value in headers.items() if name.lower() != "authorization"} - headers = {**headers, **default_headers} - elif set_default_headers is not None: - headers = set_default_headers - - params = self._custom_query - if default_query is not None: - params = {**params, **default_query} - elif set_default_query is not None: - params = set_default_query - - http_client = http_client or self._client - next_provider = self._provider if isinstance(provider, NotGiven) else provider - explicit_base_url = base_url is not None and not isinstance(base_url, NotGiven) - next_workload_identity = workload_identity if workload_identity is not None else self.workload_identity - if api_key is not None and workload_identity is None: - next_workload_identity = None - current_x509 = is_x509_workload_identity(self.workload_identity) - next_x509 = is_x509_workload_identity(next_workload_identity) - mode_changed = current_x509 != next_x509 - effective_data_residency = data_residency - if effective_data_residency is None and mode_changed and not explicit_base_url: - effective_data_residency = self._data_residency - base_url = resolve_data_residency( - effective_data_residency, - not_given if base_url is None and data_residency is None else base_url, - provider=next_provider, + from .lib._client_auth import asynccopy + + return asynccopy( + self, + api_key=api_key, + admin_api_key=admin_api_key, + workload_identity=workload_identity, + provider=provider, + organization=organization, + project=project, + webhook_secret=webhook_secret, websocket_base_url=websocket_base_url, - ) - base_url = x509_data_residency_base_url(base_url, effective_data_residency, next_workload_identity) - preserve_default_base_url = False - auth_options: dict[str, Any] - if next_provider is not None: - auth_options = { - "provider": next_provider, - "api_key": api_key, - "admin_api_key": admin_api_key, - "workload_identity": workload_identity, - "base_url": base_url, - } - elif self._provider is not None: - auth_options = { - "api_key": api_key, - "admin_api_key": admin_api_key, - "workload_identity": workload_identity, - "base_url": base_url, - } - else: - inherited_base_url = None if mode_changed and self._base_url_was_default else self.base_url - preserve_default_base_url = base_url is None and not mode_changed and self._base_url_was_default - auth_options = { - "api_key": api_key - if workload_identity is not None - else api_key or self._api_key_provider or self.api_key, - "admin_api_key": admin_api_key or self.admin_api_key, - "workload_identity": next_workload_identity, - "base_url": base_url or inherited_base_url, - } - - copied = self.__class__( - organization=organization or inherited_organization, - project=project or inherited_project, - webhook_secret=webhook_secret or self.webhook_secret, - websocket_base_url=None if data_residency is not None else websocket_base_url or self.websocket_base_url, - timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + base_url=base_url, + data_residency=data_residency, + timeout=timeout, http_client=http_client, - max_retries=max_retries if is_given(max_retries) else self.max_retries, - default_headers=headers, - default_query=params, - _enforce_credentials=True if _enforce_credentials is None else _enforce_credentials, - **auth_options, - **_extra_kwargs, - ) - if preserve_default_base_url: - copied._base_url_was_default = True - overridden_authorizations = default_headers if default_headers is not None else set_default_headers - explicit_authorization_override = overridden_authorizations is not None and any( - name.lower() == "authorization" for name in overridden_authorizations + max_retries=max_retries, + default_headers=default_headers, + set_default_headers=set_default_headers, + default_query=default_query, + set_default_query=set_default_query, + _enforce_credentials=_enforce_credentials, + _extra_kwargs=_extra_kwargs, ) - if ( - self._ambient_authorizations - and not explicit_authorization_override - and any( - name.lower() == "authorization" and value in self._ambient_authorizations - for name, value in copied._custom_headers.items() - ) - ): - copied._ambient_authorizations = self._ambient_authorizations - if data_residency is not None: - copied._data_residency = data_residency - elif not explicit_base_url and not provider_changed: - copied._data_residency = self._data_residency - return copied # Alias for `copy` for nicer inline usage, e.g. # client.with_options(timeout=10).foo.create(...) diff --git a/src/openai/lib/_assistants_runs.py b/src/openai/lib/_assistants_runs.py new file mode 100644 index 0000000000..6e750e1fc2 --- /dev/null +++ b/src/openai/lib/_assistants_runs.py @@ -0,0 +1,768 @@ +"""Handwritten Assistants helper behavior behind generated public facades.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Union, Iterable, Optional +from functools import partial + +import httpx2 + +from openai._types import NOT_GIVEN, Body, Omit, Query, Headers, NotGiven, omit, not_given +from openai._utils import is_given, path_template, maybe_transform +from openai._streaming import Stream, AsyncStream +from openai._base_client import make_request_options +from openai.lib.streaming import ( + AssistantEventHandler, + AssistantEventHandlerT, + AssistantStreamManager, + AsyncAssistantEventHandler, + AsyncAssistantEventHandlerT, + AsyncAssistantStreamManager, +) +from openai.types.beta.threads import ( + run_create_params, + run_submit_tool_outputs_params, +) +from openai.types.beta.threads.run import Run +from openai.types.shared.chat_model import ChatModel +from openai.types.shared_params.metadata import Metadata +from openai.types.shared.reasoning_effort import ReasoningEffort +from openai.types.beta.assistant_tool_param import AssistantToolParam +from openai.types.beta.assistant_stream_event import AssistantStreamEvent +from openai.types.beta.threads.runs.run_step_include import RunStepInclude +from openai.types.beta.assistant_tool_choice_option_param import AssistantToolChoiceOptionParam +from openai.types.beta.assistant_response_format_option_param import AssistantResponseFormatOptionParam + +if TYPE_CHECKING: + from openai.resources.beta.threads.runs.runs import Runs, AsyncRuns + + +def runs_create_and_poll( + self: Runs, + *, + assistant_id: str, + include: List[RunStepInclude] | Omit = omit, + additional_instructions: Optional[str] | Omit = omit, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | Omit = omit, + instructions: Optional[str] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_prompt_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + model: Union[str, ChatModel, None] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + reasoning_effort: Optional[ReasoningEffort] | Omit = omit, + response_format: Optional[AssistantResponseFormatOptionParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + tool_choice: Optional[AssistantToolChoiceOptionParam] | Omit = omit, + tools: Optional[Iterable[AssistantToolParam]] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, + poll_interval_ms: int | Omit = omit, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> Run: + """ + A helper to create a run an poll for a terminal state. More information on Run + lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + run = self.create( # pyright: ignore[reportDeprecated] + thread_id=thread_id, + assistant_id=assistant_id, + include=include, + additional_instructions=additional_instructions, + additional_messages=additional_messages, + instructions=instructions, + max_completion_tokens=max_completion_tokens, + max_prompt_tokens=max_prompt_tokens, + metadata=metadata, + model=model, + response_format=response_format, + temperature=temperature, + tool_choice=tool_choice, + parallel_tool_calls=parallel_tool_calls, + reasoning_effort=reasoning_effort, + # We assume we are not streaming when polling + stream=False, + tools=tools, + truncation_strategy=truncation_strategy, + top_p=top_p, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return self.poll( # pyright: ignore[reportDeprecated] + run.id, + thread_id=thread_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + poll_interval_ms=poll_interval_ms, + timeout=timeout, + ) + + +def runs_create_and_stream( + self: Runs, + *, + assistant_id: str, + additional_instructions: Optional[str] | Omit = omit, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | Omit = omit, + instructions: Optional[str] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_prompt_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + model: Union[str, ChatModel, None] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + reasoning_effort: Optional[ReasoningEffort] | Omit = omit, + response_format: Optional[AssistantResponseFormatOptionParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + tool_choice: Optional[AssistantToolChoiceOptionParam] | Omit = omit, + tools: Optional[Iterable[AssistantToolParam]] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, + thread_id: str, + event_handler: AssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT]: + """Create a Run stream""" + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + + extra_headers = { + "OpenAI-Beta": "assistants=v2", + "X-Stainless-Stream-Helper": "threads.runs.create_and_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + make_request = partial( + self._post, + path_template("/threads/{thread_id}/runs", thread_id=thread_id), + body=maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "additional_messages": additional_messages, + "instructions": instructions, + "max_completion_tokens": max_completion_tokens, + "max_prompt_tokens": max_prompt_tokens, + "metadata": metadata, + "model": model, + "response_format": response_format, + "temperature": temperature, + "tool_choice": tool_choice, + "stream": True, + "tools": tools, + "truncation_strategy": truncation_strategy, + "parallel_tool_calls": parallel_tool_calls, + "reasoning_effort": reasoning_effort, + "top_p": top_p, + }, + run_create_params.RunCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, + ), + cast_to=Run, + stream=True, + stream_cls=Stream[AssistantStreamEvent], + ) + return AssistantStreamManager(make_request, event_handler=event_handler or AssistantEventHandler()) + + +def runs_poll( + self: Runs, + run_id: str, + thread_id: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = not_given, + poll_interval_ms: int | Omit = omit, +) -> Run: + """ + A helper to poll a run status until it reaches a terminal state. More + information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + extra_headers = {"X-Stainless-Poll-Helper": "true", **(extra_headers or {})} + + if is_given(poll_interval_ms): + extra_headers["X-Stainless-Custom-Poll-Interval"] = str(poll_interval_ms) + + terminal_states = {"requires_action", "cancelled", "completed", "failed", "expired", "incomplete"} + while True: + response = self.with_raw_response.retrieve( # pyright: ignore[reportDeprecated] + thread_id=thread_id, + run_id=run_id, + extra_headers=extra_headers, + extra_body=extra_body, + extra_query=extra_query, + timeout=timeout, + ) + + run = response.parse() + # Return if we reached a terminal state + if run.status in terminal_states: + return run + + if not is_given(poll_interval_ms): + from_header = response.headers.get("openai-poll-after-ms") + if from_header is not None: + poll_interval_ms = int(from_header) + else: + poll_interval_ms = 1000 + + self._sleep(poll_interval_ms / 1000) + + +def runs_stream( + self: Runs, + *, + assistant_id: str, + include: List[RunStepInclude] | Omit = omit, + additional_instructions: Optional[str] | Omit = omit, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | Omit = omit, + instructions: Optional[str] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_prompt_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + model: Union[str, ChatModel, None] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + reasoning_effort: Optional[ReasoningEffort] | Omit = omit, + response_format: Optional[AssistantResponseFormatOptionParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + tool_choice: Optional[AssistantToolChoiceOptionParam] | Omit = omit, + tools: Optional[Iterable[AssistantToolParam]] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, + thread_id: str, + event_handler: AssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT]: + """Create a Run stream""" + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + + extra_headers = { + "OpenAI-Beta": "assistants=v2", + "X-Stainless-Stream-Helper": "threads.runs.create_and_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + make_request = partial( + self._post, + path_template("/threads/{thread_id}/runs", thread_id=thread_id), + body=maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "additional_messages": additional_messages, + "instructions": instructions, + "max_completion_tokens": max_completion_tokens, + "max_prompt_tokens": max_prompt_tokens, + "metadata": metadata, + "model": model, + "response_format": response_format, + "temperature": temperature, + "tool_choice": tool_choice, + "stream": True, + "tools": tools, + "parallel_tool_calls": parallel_tool_calls, + "reasoning_effort": reasoning_effort, + "truncation_strategy": truncation_strategy, + "top_p": top_p, + }, + run_create_params.RunCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"include": include}, run_create_params.RunCreateParams), + security={"bearer_auth": True}, + ), + cast_to=Run, + stream=True, + stream_cls=Stream[AssistantStreamEvent], + ) + return AssistantStreamManager(make_request, event_handler=event_handler or AssistantEventHandler()) + + +def runs_submit_tool_outputs_and_poll( + self: Runs, + *, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + run_id: str, + thread_id: str, + poll_interval_ms: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> Run: + """ + A helper to submit a tool output to a run and poll for a terminal run state. + More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + run = self.submit_tool_outputs( # pyright: ignore[reportDeprecated] + run_id=run_id, + thread_id=thread_id, + tool_outputs=tool_outputs, + stream=False, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return self.poll( # pyright: ignore[reportDeprecated] + run_id=run.id, + thread_id=thread_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + poll_interval_ms=poll_interval_ms, + ) + + +def runs_submit_tool_outputs_stream( + self: Runs, + *, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + run_id: str, + thread_id: str, + event_handler: AssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT]: + """ + Submit the tool outputs from a previous run and stream the run to a terminal + state. More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + + extra_headers = { + "OpenAI-Beta": "assistants=v2", + "X-Stainless-Stream-Helper": "threads.runs.submit_tool_outputs_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + request = partial( + self._post, + path_template("/threads/{thread_id}/runs/{run_id}/submit_tool_outputs", thread_id=thread_id, run_id=run_id), + body=maybe_transform( + { + "tool_outputs": tool_outputs, + "stream": True, + }, + run_submit_tool_outputs_params.RunSubmitToolOutputsParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, + ), + cast_to=Run, + stream=True, + stream_cls=Stream[AssistantStreamEvent], + ) + return AssistantStreamManager(request, event_handler=event_handler or AssistantEventHandler()) + + +async def async_runs_create_and_poll( + self: AsyncRuns, + *, + assistant_id: str, + include: List[RunStepInclude] | Omit = omit, + additional_instructions: Optional[str] | Omit = omit, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | Omit = omit, + instructions: Optional[str] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_prompt_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + model: Union[str, ChatModel, None] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + reasoning_effort: Optional[ReasoningEffort] | Omit = omit, + response_format: Optional[AssistantResponseFormatOptionParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + tool_choice: Optional[AssistantToolChoiceOptionParam] | Omit = omit, + tools: Optional[Iterable[AssistantToolParam]] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, + poll_interval_ms: int | Omit = omit, + thread_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> Run: + """ + A helper to create a run an poll for a terminal state. More information on Run + lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + run = await self.create( # pyright: ignore[reportDeprecated] + thread_id=thread_id, + assistant_id=assistant_id, + include=include, + additional_instructions=additional_instructions, + additional_messages=additional_messages, + instructions=instructions, + max_completion_tokens=max_completion_tokens, + max_prompt_tokens=max_prompt_tokens, + metadata=metadata, + model=model, + response_format=response_format, + temperature=temperature, + tool_choice=tool_choice, + parallel_tool_calls=parallel_tool_calls, + reasoning_effort=reasoning_effort, + # We assume we are not streaming when polling + stream=False, + tools=tools, + truncation_strategy=truncation_strategy, + top_p=top_p, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return await self.poll( # pyright: ignore[reportDeprecated] + run.id, + thread_id=thread_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + poll_interval_ms=poll_interval_ms, + timeout=timeout, + ) + + +def async_runs_create_and_stream( + self: AsyncRuns, + *, + assistant_id: str, + additional_instructions: Optional[str] | Omit = omit, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | Omit = omit, + instructions: Optional[str] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_prompt_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + model: Union[str, ChatModel, None] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + response_format: Optional[AssistantResponseFormatOptionParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + tool_choice: Optional[AssistantToolChoiceOptionParam] | Omit = omit, + tools: Optional[Iterable[AssistantToolParam]] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, + thread_id: str, + event_handler: AsyncAssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> AsyncAssistantStreamManager[AsyncAssistantEventHandler] | AsyncAssistantStreamManager[AsyncAssistantEventHandlerT]: + """Create a Run stream""" + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + + extra_headers = { + "OpenAI-Beta": "assistants=v2", + "X-Stainless-Stream-Helper": "threads.runs.create_and_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + request = self._post( + path_template("/threads/{thread_id}/runs", thread_id=thread_id), + body=maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "additional_messages": additional_messages, + "instructions": instructions, + "max_completion_tokens": max_completion_tokens, + "max_prompt_tokens": max_prompt_tokens, + "metadata": metadata, + "model": model, + "response_format": response_format, + "temperature": temperature, + "tool_choice": tool_choice, + "stream": True, + "tools": tools, + "truncation_strategy": truncation_strategy, + "top_p": top_p, + "parallel_tool_calls": parallel_tool_calls, + }, + run_create_params.RunCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, + ), + cast_to=Run, + stream=True, + stream_cls=AsyncStream[AssistantStreamEvent], + ) + return AsyncAssistantStreamManager(request, event_handler=event_handler or AsyncAssistantEventHandler()) + + +async def async_runs_poll( + self: AsyncRuns, + run_id: str, + thread_id: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = not_given, + poll_interval_ms: int | Omit = omit, +) -> Run: + """ + A helper to poll a run status until it reaches a terminal state. More + information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + extra_headers = {"X-Stainless-Poll-Helper": "true", **(extra_headers or {})} + + if is_given(poll_interval_ms): + extra_headers["X-Stainless-Custom-Poll-Interval"] = str(poll_interval_ms) + + terminal_states = {"requires_action", "cancelled", "completed", "failed", "expired", "incomplete"} + while True: + response = await self.with_raw_response.retrieve( # pyright: ignore[reportDeprecated] + thread_id=thread_id, + run_id=run_id, + extra_headers=extra_headers, + extra_body=extra_body, + extra_query=extra_query, + timeout=timeout, + ) + + run = response.parse() + # Return if we reached a terminal state + if run.status in terminal_states: + return run + + if not is_given(poll_interval_ms): + from_header = response.headers.get("openai-poll-after-ms") + if from_header is not None: + poll_interval_ms = int(from_header) + else: + poll_interval_ms = 1000 + + await self._sleep(poll_interval_ms / 1000) + + +def async_runs_stream( + self: AsyncRuns, + *, + assistant_id: str, + include: List[RunStepInclude] | Omit = omit, + additional_instructions: Optional[str] | Omit = omit, + additional_messages: Optional[Iterable[run_create_params.AdditionalMessage]] | Omit = omit, + instructions: Optional[str] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_prompt_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + model: Union[str, ChatModel, None] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + reasoning_effort: Optional[ReasoningEffort] | Omit = omit, + response_format: Optional[AssistantResponseFormatOptionParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + tool_choice: Optional[AssistantToolChoiceOptionParam] | Omit = omit, + tools: Optional[Iterable[AssistantToolParam]] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, + thread_id: str, + event_handler: AsyncAssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> AsyncAssistantStreamManager[AsyncAssistantEventHandler] | AsyncAssistantStreamManager[AsyncAssistantEventHandlerT]: + """Create a Run stream""" + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + + extra_headers = { + "OpenAI-Beta": "assistants=v2", + "X-Stainless-Stream-Helper": "threads.runs.create_and_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + request = self._post( + path_template("/threads/{thread_id}/runs", thread_id=thread_id), + body=maybe_transform( + { + "assistant_id": assistant_id, + "additional_instructions": additional_instructions, + "additional_messages": additional_messages, + "instructions": instructions, + "max_completion_tokens": max_completion_tokens, + "max_prompt_tokens": max_prompt_tokens, + "metadata": metadata, + "model": model, + "response_format": response_format, + "temperature": temperature, + "tool_choice": tool_choice, + "stream": True, + "tools": tools, + "parallel_tool_calls": parallel_tool_calls, + "reasoning_effort": reasoning_effort, + "truncation_strategy": truncation_strategy, + "top_p": top_p, + }, + run_create_params.RunCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"include": include}, run_create_params.RunCreateParams), + security={"bearer_auth": True}, + ), + cast_to=Run, + stream=True, + stream_cls=AsyncStream[AssistantStreamEvent], + ) + return AsyncAssistantStreamManager(request, event_handler=event_handler or AsyncAssistantEventHandler()) + + +async def async_runs_submit_tool_outputs_and_poll( + self: AsyncRuns, + *, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + run_id: str, + thread_id: str, + poll_interval_ms: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> Run: + """ + A helper to submit a tool output to a run and poll for a terminal run state. + More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + run = await self.submit_tool_outputs( # pyright: ignore[reportDeprecated] + run_id=run_id, + thread_id=thread_id, + tool_outputs=tool_outputs, + stream=False, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return await self.poll( # pyright: ignore[reportDeprecated] + run_id=run.id, + thread_id=thread_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + poll_interval_ms=poll_interval_ms, + ) + + +def async_runs_submit_tool_outputs_stream( + self: AsyncRuns, + *, + tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], + run_id: str, + thread_id: str, + event_handler: AsyncAssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> AsyncAssistantStreamManager[AsyncAssistantEventHandler] | AsyncAssistantStreamManager[AsyncAssistantEventHandlerT]: + """ + Submit the tool outputs from a previous run and stream the run to a terminal + state. More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + if not run_id: + raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") + + if not thread_id: + raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") + + extra_headers = { + "OpenAI-Beta": "assistants=v2", + "X-Stainless-Stream-Helper": "threads.runs.submit_tool_outputs_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + request = self._post( + path_template("/threads/{thread_id}/runs/{run_id}/submit_tool_outputs", thread_id=thread_id, run_id=run_id), + body=maybe_transform( + { + "tool_outputs": tool_outputs, + "stream": True, + }, + run_submit_tool_outputs_params.RunSubmitToolOutputsParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, + ), + cast_to=Run, + stream=True, + stream_cls=AsyncStream[AssistantStreamEvent], + ) + return AsyncAssistantStreamManager(request, event_handler=event_handler or AsyncAssistantEventHandler()) diff --git a/src/openai/lib/_assistants_threads.py b/src/openai/lib/_assistants_threads.py new file mode 100644 index 0000000000..ab3754f4e0 --- /dev/null +++ b/src/openai/lib/_assistants_threads.py @@ -0,0 +1,287 @@ +"""Handwritten Assistants helper behavior behind generated public facades.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Union, Iterable, Optional +from functools import partial + +import httpx2 + +from openai._types import NOT_GIVEN, Body, Omit, Query, Headers, NotGiven, omit +from openai._utils import maybe_transform +from openai._streaming import Stream, AsyncStream +from openai.types.beta import thread_create_and_run_params +from openai._base_client import make_request_options +from openai.lib.streaming import ( + AssistantEventHandler, + AssistantEventHandlerT, + AssistantStreamManager, + AsyncAssistantEventHandler, + AsyncAssistantEventHandlerT, + AsyncAssistantStreamManager, +) +from openai.types.beta.threads.run import Run +from openai.types.shared.chat_model import ChatModel +from openai.types.shared_params.metadata import Metadata +from openai.types.beta.assistant_tool_param import AssistantToolParam +from openai.types.beta.assistant_stream_event import AssistantStreamEvent +from openai.types.beta.assistant_tool_choice_option_param import AssistantToolChoiceOptionParam +from openai.types.beta.assistant_response_format_option_param import AssistantResponseFormatOptionParam + +if TYPE_CHECKING: + from openai.resources.beta.threads.threads import Threads, AsyncThreads + + +def threads_create_and_run_poll( + self: Threads, + *, + assistant_id: str, + instructions: Optional[str] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_prompt_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + model: Union[str, ChatModel, None] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + response_format: Optional[AssistantResponseFormatOptionParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + thread: thread_create_and_run_params.Thread | Omit = omit, + tool_choice: Optional[AssistantToolChoiceOptionParam] | Omit = omit, + tool_resources: Optional[thread_create_and_run_params.ToolResources] | Omit = omit, + tools: Optional[Iterable[AssistantToolParam]] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation_strategy: Optional[thread_create_and_run_params.TruncationStrategy] | Omit = omit, + poll_interval_ms: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> Run: + """ + A helper to create a thread, start a run and then poll for a terminal state. + More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + run = self.create_and_run( # pyright: ignore[reportDeprecated] + assistant_id=assistant_id, + instructions=instructions, + max_completion_tokens=max_completion_tokens, + max_prompt_tokens=max_prompt_tokens, + metadata=metadata, + model=model, + parallel_tool_calls=parallel_tool_calls, + response_format=response_format, + temperature=temperature, + stream=False, + thread=thread, + tool_resources=tool_resources, + tool_choice=tool_choice, + truncation_strategy=truncation_strategy, + top_p=top_p, + tools=tools, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return self.runs.poll(run.id, run.thread_id, extra_headers, extra_query, extra_body, timeout, poll_interval_ms) # pyright: ignore[reportDeprecated] + + +def threads_create_and_run_stream( + self: Threads, + *, + assistant_id: str, + instructions: Optional[str] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_prompt_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + model: Union[str, ChatModel, None] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + response_format: Optional[AssistantResponseFormatOptionParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + thread: thread_create_and_run_params.Thread | Omit = omit, + tool_choice: Optional[AssistantToolChoiceOptionParam] | Omit = omit, + tool_resources: Optional[thread_create_and_run_params.ToolResources] | Omit = omit, + tools: Optional[Iterable[AssistantToolParam]] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation_strategy: Optional[thread_create_and_run_params.TruncationStrategy] | Omit = omit, + event_handler: AssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT]: + """Create a thread and stream the run back""" + extra_headers = { + "OpenAI-Beta": "assistants=v2", + "X-Stainless-Stream-Helper": "threads.create_and_run_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + make_request = partial( + self._post, + "/threads/runs", + body=maybe_transform( + { + "assistant_id": assistant_id, + "instructions": instructions, + "max_completion_tokens": max_completion_tokens, + "max_prompt_tokens": max_prompt_tokens, + "metadata": metadata, + "model": model, + "parallel_tool_calls": parallel_tool_calls, + "response_format": response_format, + "temperature": temperature, + "tool_choice": tool_choice, + "stream": True, + "thread": thread, + "tools": tools, + "tool_resources": tool_resources, + "truncation_strategy": truncation_strategy, + "top_p": top_p, + }, + thread_create_and_run_params.ThreadCreateAndRunParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, + ), + cast_to=Run, + stream=True, + stream_cls=Stream[AssistantStreamEvent], + ) + return AssistantStreamManager(make_request, event_handler=event_handler or AssistantEventHandler()) + + +async def async_threads_create_and_run_poll( + self: AsyncThreads, + *, + assistant_id: str, + instructions: Optional[str] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_prompt_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + model: Union[str, ChatModel, None] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + response_format: Optional[AssistantResponseFormatOptionParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + thread: thread_create_and_run_params.Thread | Omit = omit, + tool_choice: Optional[AssistantToolChoiceOptionParam] | Omit = omit, + tool_resources: Optional[thread_create_and_run_params.ToolResources] | Omit = omit, + tools: Optional[Iterable[AssistantToolParam]] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation_strategy: Optional[thread_create_and_run_params.TruncationStrategy] | Omit = omit, + poll_interval_ms: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> Run: + """ + A helper to create a thread, start a run and then poll for a terminal state. + More information on Run lifecycles can be found here: + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps + """ + run = await self.create_and_run( # pyright: ignore[reportDeprecated] + assistant_id=assistant_id, + instructions=instructions, + max_completion_tokens=max_completion_tokens, + max_prompt_tokens=max_prompt_tokens, + metadata=metadata, + model=model, + parallel_tool_calls=parallel_tool_calls, + response_format=response_format, + temperature=temperature, + stream=False, + thread=thread, + tool_resources=tool_resources, + tool_choice=tool_choice, + truncation_strategy=truncation_strategy, + top_p=top_p, + tools=tools, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return await self.runs.poll( # pyright: ignore[reportDeprecated] + run.id, run.thread_id, extra_headers, extra_query, extra_body, timeout, poll_interval_ms + ) + + +def async_threads_create_and_run_stream( + self: AsyncThreads, + *, + assistant_id: str, + instructions: Optional[str] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_prompt_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + model: Union[str, ChatModel, None] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + response_format: Optional[AssistantResponseFormatOptionParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + thread: thread_create_and_run_params.Thread | Omit = omit, + tool_choice: Optional[AssistantToolChoiceOptionParam] | Omit = omit, + tool_resources: Optional[thread_create_and_run_params.ToolResources] | Omit = omit, + tools: Optional[Iterable[AssistantToolParam]] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation_strategy: Optional[thread_create_and_run_params.TruncationStrategy] | Omit = omit, + event_handler: AsyncAssistantEventHandlerT | None = None, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> AsyncAssistantStreamManager[AsyncAssistantEventHandler] | AsyncAssistantStreamManager[AsyncAssistantEventHandlerT]: + """Create a thread and stream the run back""" + extra_headers = { + "OpenAI-Beta": "assistants=v2", + "X-Stainless-Stream-Helper": "threads.create_and_run_stream", + "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", + **(extra_headers or {}), + } + request = self._post( + "/threads/runs", + body=maybe_transform( + { + "assistant_id": assistant_id, + "instructions": instructions, + "max_completion_tokens": max_completion_tokens, + "max_prompt_tokens": max_prompt_tokens, + "metadata": metadata, + "model": model, + "parallel_tool_calls": parallel_tool_calls, + "response_format": response_format, + "temperature": temperature, + "tool_choice": tool_choice, + "stream": True, + "thread": thread, + "tools": tools, + "tool_resources": tool_resources, + "truncation_strategy": truncation_strategy, + "top_p": top_p, + }, + thread_create_and_run_params.ThreadCreateAndRunParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, + ), + cast_to=Run, + stream=True, + stream_cls=AsyncStream[AssistantStreamEvent], + ) + return AsyncAssistantStreamManager(request, event_handler=event_handler or AsyncAssistantEventHandler()) diff --git a/src/openai/lib/_chat_completion_resources.py b/src/openai/lib/_chat_completion_resources.py new file mode 100644 index 0000000000..038448c25f --- /dev/null +++ b/src/openai/lib/_chat_completion_resources.py @@ -0,0 +1,490 @@ +from __future__ import annotations + +import inspect +from typing import TYPE_CHECKING, Dict, List, Type, Union, Iterable, Optional, cast +from functools import partial +from typing_extensions import Literal + +import httpx2 +import pydantic + +from openai._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given +from openai._utils import maybe_transform, async_maybe_transform +from openai._streaming import Stream +from openai.types.chat import ChatCompletionAudioParam, completion_create_params +from openai._base_client import make_request_options +from openai.lib._parsing import ( + ResponseFormatT, + validate_input_tools as _validate_input_tools, + parse_chat_completion as _parse_chat_completion, + materialize_input_tools as _materialize_input_tools, + type_to_response_format_param as _type_to_response_format, +) +from openai.lib.streaming.chat import ChatCompletionStreamManager, AsyncChatCompletionStreamManager +from openai.types.shared.chat_model import ChatModel +from openai.types.chat.chat_completion import ChatCompletion +from openai.types.shared_params.metadata import Metadata +from openai.types.shared.reasoning_effort import ReasoningEffort +from openai.types.chat.chat_completion_chunk import ChatCompletionChunk +from openai.types.chat.parsed_chat_completion import ParsedChatCompletion +from openai.types.chat.chat_completion_audio_param import ChatCompletionAudioParam +from openai.types.chat.chat_completion_message_param import ChatCompletionMessageParam +from openai.types.chat.chat_completion_tool_union_param import ChatCompletionToolUnionParam +from openai.types.chat.chat_completion_stream_options_param import ChatCompletionStreamOptionsParam +from openai.types.chat.chat_completion_prediction_content_param import ChatCompletionPredictionContentParam +from openai.types.chat.chat_completion_tool_choice_option_param import ChatCompletionToolChoiceOptionParam + +if TYPE_CHECKING: + from openai.resources.chat.completions.completions import Completions, AsyncCompletions + + +def parse( + resource: Completions, + *, + messages: Iterable[ChatCompletionMessageParam], + model: Union[str, ChatModel], + audio: Optional[ChatCompletionAudioParam] | Omit = omit, + response_format: type[ResponseFormatT] | Omit = omit, + frequency_penalty: Optional[float] | Omit = omit, + function_call: completion_create_params.FunctionCall | Omit = omit, + functions: Iterable[completion_create_params.Function] | Omit = omit, + logit_bias: Optional[Dict[str, int]] | Omit = omit, + logprobs: Optional[bool] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + modalities: Optional[List[Literal["text", "audio"]]] | Omit = omit, + moderation: Optional[completion_create_params.Moderation] | Omit = omit, + n: Optional[int] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + prediction: Optional[ChatCompletionPredictionContentParam] | Omit = omit, + presence_penalty: Optional[float] | Omit = omit, + prompt_cache_key: Optional[str] | Omit = omit, + prompt_cache_options: completion_create_params.PromptCacheOptions | Omit = omit, + prompt_cache_retention: Optional[Literal["in_memory", "24h"]] | Omit = omit, + reasoning_effort: Optional[ReasoningEffort] | Omit = omit, + safety_identifier: Optional[str] | Omit = omit, + seed: Optional[int] | Omit = omit, + service_tier: Optional[Literal["auto", "default", "flex", "scale", "priority", "fast"]] | Omit = omit, + stop: Union[str, SequenceNotStr[str], None] | Omit = omit, + store: Optional[bool] | Omit = omit, + stream_options: Optional[ChatCompletionStreamOptionsParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + tool_choice: ChatCompletionToolChoiceOptionParam | Omit = omit, + tools: Iterable[ChatCompletionToolUnionParam] | Omit = omit, + top_logprobs: Optional[int] | Omit = omit, + top_p: Optional[float] | Omit = omit, + user: str | Omit = omit, + verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit, + web_search_options: completion_create_params.WebSearchOptions | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = not_given, +) -> ParsedChatCompletion[ResponseFormatT]: + chat_completion_tools = _validate_input_tools(tools) + + extra_headers = { + "X-Stainless-Helper-Method": "chat.completions.parse", + **(extra_headers or {}), + } + + def parser(raw_completion: ChatCompletion) -> ParsedChatCompletion[ResponseFormatT]: + return _parse_chat_completion( + response_format=response_format, + chat_completion=raw_completion, + input_tools=chat_completion_tools, + ) + + return resource._post( + "/chat/completions", + body=maybe_transform( + { + "model": model, + "stream": False, + "service_tier": service_tier, + "messages": messages, + "audio": audio, + "frequency_penalty": frequency_penalty, + "function_call": function_call, + "functions": functions, + "logit_bias": logit_bias, + "logprobs": logprobs, + "max_completion_tokens": max_completion_tokens, + "max_tokens": max_tokens, + "metadata": metadata, + "modalities": modalities, + "moderation": moderation, + "n": n, + "parallel_tool_calls": parallel_tool_calls, + "prediction": prediction, + "presence_penalty": presence_penalty, + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "reasoning_effort": reasoning_effort, + "response_format": _type_to_response_format(response_format), + "safety_identifier": safety_identifier, + "seed": seed, + "stop": stop, + "store": store, + "stream_options": stream_options, + "temperature": temperature, + "tool_choice": tool_choice, + "tools": chat_completion_tools, + "top_logprobs": top_logprobs, + "top_p": top_p, + "user": user, + "verbosity": verbosity, + "web_search_options": web_search_options, + }, + completion_create_params.CompletionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + post_parser=parser, + security={"bearer_auth": True}, + ), + # we turn the `ChatCompletion` instance into a `ParsedChatCompletion` + # in the `parser` function above + cast_to=cast(Type[ParsedChatCompletion[ResponseFormatT]], ChatCompletion), + stream=False, + ) + + +async def async_parse( + resource: AsyncCompletions, + *, + messages: Iterable[ChatCompletionMessageParam], + model: Union[str, ChatModel], + audio: Optional[ChatCompletionAudioParam] | Omit = omit, + response_format: type[ResponseFormatT] | Omit = omit, + frequency_penalty: Optional[float] | Omit = omit, + function_call: completion_create_params.FunctionCall | Omit = omit, + functions: Iterable[completion_create_params.Function] | Omit = omit, + logit_bias: Optional[Dict[str, int]] | Omit = omit, + logprobs: Optional[bool] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + modalities: Optional[List[Literal["text", "audio"]]] | Omit = omit, + moderation: Optional[completion_create_params.Moderation] | Omit = omit, + n: Optional[int] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + prediction: Optional[ChatCompletionPredictionContentParam] | Omit = omit, + presence_penalty: Optional[float] | Omit = omit, + prompt_cache_key: Optional[str] | Omit = omit, + prompt_cache_options: completion_create_params.PromptCacheOptions | Omit = omit, + prompt_cache_retention: Optional[Literal["in_memory", "24h"]] | Omit = omit, + reasoning_effort: Optional[ReasoningEffort] | Omit = omit, + safety_identifier: Optional[str] | Omit = omit, + seed: Optional[int] | Omit = omit, + service_tier: Optional[Literal["auto", "default", "flex", "scale", "priority", "fast"]] | Omit = omit, + stop: Union[str, SequenceNotStr[str], None] | Omit = omit, + store: Optional[bool] | Omit = omit, + stream_options: Optional[ChatCompletionStreamOptionsParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + tool_choice: ChatCompletionToolChoiceOptionParam | Omit = omit, + tools: Iterable[ChatCompletionToolUnionParam] | Omit = omit, + top_logprobs: Optional[int] | Omit = omit, + top_p: Optional[float] | Omit = omit, + user: str | Omit = omit, + verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit, + web_search_options: completion_create_params.WebSearchOptions | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = not_given, +) -> ParsedChatCompletion[ResponseFormatT]: + chat_completion_tools = _validate_input_tools(tools) + + extra_headers = { + "X-Stainless-Helper-Method": "chat.completions.parse", + **(extra_headers or {}), + } + + def parser(raw_completion: ChatCompletion) -> ParsedChatCompletion[ResponseFormatT]: + return _parse_chat_completion( + response_format=response_format, + chat_completion=raw_completion, + input_tools=chat_completion_tools, + ) + + return await resource._post( + "/chat/completions", + body=await async_maybe_transform( + { + "model": model, + "stream": False, + "service_tier": service_tier, + "messages": messages, + "audio": audio, + "frequency_penalty": frequency_penalty, + "function_call": function_call, + "functions": functions, + "logit_bias": logit_bias, + "logprobs": logprobs, + "max_completion_tokens": max_completion_tokens, + "max_tokens": max_tokens, + "metadata": metadata, + "modalities": modalities, + "moderation": moderation, + "n": n, + "parallel_tool_calls": parallel_tool_calls, + "prediction": prediction, + "presence_penalty": presence_penalty, + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "reasoning_effort": reasoning_effort, + "response_format": _type_to_response_format(response_format), + "safety_identifier": safety_identifier, + "seed": seed, + "store": store, + "stop": stop, + "stream_options": stream_options, + "temperature": temperature, + "tool_choice": tool_choice, + "tools": chat_completion_tools, + "top_logprobs": top_logprobs, + "top_p": top_p, + "user": user, + "verbosity": verbosity, + "web_search_options": web_search_options, + }, + completion_create_params.CompletionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + post_parser=parser, + security={"bearer_auth": True}, + ), + # we turn the `ChatCompletion` instance into a `ParsedChatCompletion` + # in the `parser` function above + cast_to=cast(Type[ParsedChatCompletion[ResponseFormatT]], ChatCompletion), + stream=False, + ) + + +def stream( + resource: Completions, + *, + messages: Iterable[ChatCompletionMessageParam], + model: Union[str, ChatModel], + audio: Optional[ChatCompletionAudioParam] | Omit = omit, + response_format: completion_create_params.ResponseFormat | type[ResponseFormatT] | Omit = omit, + frequency_penalty: Optional[float] | Omit = omit, + function_call: completion_create_params.FunctionCall | Omit = omit, + functions: Iterable[completion_create_params.Function] | Omit = omit, + logit_bias: Optional[Dict[str, int]] | Omit = omit, + logprobs: Optional[bool] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + modalities: Optional[List[Literal["text", "audio"]]] | Omit = omit, + moderation: Optional[completion_create_params.Moderation] | Omit = omit, + n: Optional[int] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + prediction: Optional[ChatCompletionPredictionContentParam] | Omit = omit, + presence_penalty: Optional[float] | Omit = omit, + prompt_cache_key: Optional[str] | Omit = omit, + prompt_cache_options: completion_create_params.PromptCacheOptions | Omit = omit, + prompt_cache_retention: Optional[Literal["in_memory", "24h"]] | Omit = omit, + reasoning_effort: Optional[ReasoningEffort] | Omit = omit, + safety_identifier: Optional[str] | Omit = omit, + seed: Optional[int] | Omit = omit, + service_tier: Optional[Literal["auto", "default", "flex", "scale", "priority", "fast"]] | Omit = omit, + stop: Union[str, SequenceNotStr[str], None] | Omit = omit, + store: Optional[bool] | Omit = omit, + stream_options: Optional[ChatCompletionStreamOptionsParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + tool_choice: ChatCompletionToolChoiceOptionParam | Omit = omit, + tools: Iterable[ChatCompletionToolUnionParam] | Omit = omit, + top_logprobs: Optional[int] | Omit = omit, + top_p: Optional[float] | Omit = omit, + user: str | Omit = omit, + verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit, + web_search_options: completion_create_params.WebSearchOptions | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = not_given, +) -> ChatCompletionStreamManager[ResponseFormatT]: + chat_completion_tools = _materialize_input_tools(tools) + + extra_headers = { + "X-Stainless-Helper-Method": "chat.completions.stream", + **(extra_headers or {}), + } + + api_request: partial[Stream[ChatCompletionChunk]] = partial( + resource.create, + messages=messages, + model=model, + audio=audio, + stream=True, + response_format=_type_to_response_format(response_format), + frequency_penalty=frequency_penalty, + function_call=function_call, + functions=functions, + logit_bias=logit_bias, + logprobs=logprobs, + max_completion_tokens=max_completion_tokens, + max_tokens=max_tokens, + metadata=metadata, + modalities=modalities, + moderation=moderation, + n=n, + parallel_tool_calls=parallel_tool_calls, + prediction=prediction, + presence_penalty=presence_penalty, + prompt_cache_key=prompt_cache_key, + prompt_cache_options=prompt_cache_options, + prompt_cache_retention=prompt_cache_retention, + reasoning_effort=reasoning_effort, + safety_identifier=safety_identifier, + seed=seed, + service_tier=service_tier, + store=store, + stop=stop, + stream_options=stream_options, + temperature=temperature, + tool_choice=tool_choice, + tools=chat_completion_tools, + top_logprobs=top_logprobs, + top_p=top_p, + user=user, + verbosity=verbosity, + web_search_options=web_search_options, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return ChatCompletionStreamManager( + api_request, + response_format=response_format, + input_tools=chat_completion_tools, + ) + + +def async_stream( + resource: AsyncCompletions, + *, + messages: Iterable[ChatCompletionMessageParam], + model: Union[str, ChatModel], + audio: Optional[ChatCompletionAudioParam] | Omit = omit, + response_format: completion_create_params.ResponseFormat | type[ResponseFormatT] | Omit = omit, + frequency_penalty: Optional[float] | Omit = omit, + function_call: completion_create_params.FunctionCall | Omit = omit, + functions: Iterable[completion_create_params.Function] | Omit = omit, + logit_bias: Optional[Dict[str, int]] | Omit = omit, + logprobs: Optional[bool] | Omit = omit, + max_completion_tokens: Optional[int] | Omit = omit, + max_tokens: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + modalities: Optional[List[Literal["text", "audio"]]] | Omit = omit, + moderation: Optional[completion_create_params.Moderation] | Omit = omit, + n: Optional[int] | Omit = omit, + parallel_tool_calls: bool | Omit = omit, + prediction: Optional[ChatCompletionPredictionContentParam] | Omit = omit, + presence_penalty: Optional[float] | Omit = omit, + prompt_cache_key: Optional[str] | Omit = omit, + prompt_cache_options: completion_create_params.PromptCacheOptions | Omit = omit, + prompt_cache_retention: Optional[Literal["in_memory", "24h"]] | Omit = omit, + reasoning_effort: Optional[ReasoningEffort] | Omit = omit, + safety_identifier: Optional[str] | Omit = omit, + seed: Optional[int] | Omit = omit, + service_tier: Optional[Literal["auto", "default", "flex", "scale", "priority", "fast"]] | Omit = omit, + stop: Union[str, SequenceNotStr[str], None] | Omit = omit, + store: Optional[bool] | Omit = omit, + stream_options: Optional[ChatCompletionStreamOptionsParam] | Omit = omit, + temperature: Optional[float] | Omit = omit, + tool_choice: ChatCompletionToolChoiceOptionParam | Omit = omit, + tools: Iterable[ChatCompletionToolUnionParam] | Omit = omit, + top_logprobs: Optional[int] | Omit = omit, + top_p: Optional[float] | Omit = omit, + user: str | Omit = omit, + verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit, + web_search_options: completion_create_params.WebSearchOptions | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = not_given, +) -> AsyncChatCompletionStreamManager[ResponseFormatT]: + chat_completion_tools = _materialize_input_tools(tools) + + extra_headers = { + "X-Stainless-Helper-Method": "chat.completions.stream", + **(extra_headers or {}), + } + + api_request = resource.create( + messages=messages, + model=model, + audio=audio, + stream=True, + response_format=_type_to_response_format(response_format), + frequency_penalty=frequency_penalty, + function_call=function_call, + functions=functions, + logit_bias=logit_bias, + logprobs=logprobs, + max_completion_tokens=max_completion_tokens, + max_tokens=max_tokens, + metadata=metadata, + modalities=modalities, + moderation=moderation, + n=n, + parallel_tool_calls=parallel_tool_calls, + prediction=prediction, + presence_penalty=presence_penalty, + prompt_cache_key=prompt_cache_key, + prompt_cache_options=prompt_cache_options, + prompt_cache_retention=prompt_cache_retention, + reasoning_effort=reasoning_effort, + safety_identifier=safety_identifier, + seed=seed, + service_tier=service_tier, + stop=stop, + store=store, + stream_options=stream_options, + temperature=temperature, + tool_choice=tool_choice, + tools=chat_completion_tools, + top_logprobs=top_logprobs, + top_p=top_p, + user=user, + verbosity=verbosity, + web_search_options=web_search_options, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return AsyncChatCompletionStreamManager( + api_request, + response_format=response_format, + input_tools=chat_completion_tools, + ) + + +def validate_response_format(response_format: object) -> None: + if inspect.isclass(response_format) and issubclass(response_format, pydantic.BaseModel): + raise TypeError( + "You tried to pass a `BaseModel` class to `chat.completions.create()`; You must use `chat.completions.parse()` instead" + ) diff --git a/src/openai/lib/_client_auth.py b/src/openai/lib/_client_auth.py new file mode 100644 index 0000000000..b4039c1e21 --- /dev/null +++ b/src/openai/lib/_client_auth.py @@ -0,0 +1,976 @@ +"""Handwritten authentication hooks for the generated client facade.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Mapping, TypeVar, Callable, Awaitable +from importlib import import_module +from typing_extensions import Unpack + +import httpx2 + +from ..auth import WorkloadIdentity, X509WorkloadIdentity +from .._types import ( + Headers, + Timeout, + NotGiven, + HttpxSendArgs, + not_given, +) +from .._models import SecurityOptions, FinalRequestOptions +from .._provider import _Provider, _ProviderRuntime +from .._base_client import DEFAULT_MAX_RETRIES +from .._data_residency import DataResidency + +if TYPE_CHECKING: + import openai._client as _sdk_client +else: + _sdk_client = import_module(".._client", __package__) + +_SyncClientT = TypeVar("_SyncClientT", bound="_sdk_client.OpenAI") + +_AsyncClientT = TypeVar("_AsyncClientT", bound="_sdk_client.AsyncOpenAI") + +__all__ = ["_has_header", "_has_omitted_header"] + + +def _has_header(headers: Headers, header: str) -> bool: + header = header.lower() + return any((key.lower() == header for key in headers)) + + +def _has_omitted_header(headers: Headers, header: str) -> bool: + header = header.lower() + return any((key.lower() == header and isinstance(value, _sdk_client.Omit) for key, value in headers.items())) + + +def syncbase_url(self: _sdk_client.OpenAI) -> httpx2.URL: + return self._base_url + + +def syncbase_url_set(self: _sdk_client.OpenAI, url: httpx2.URL | str) -> None: + normalized_url = _sdk_client.normalize_httpx_url(url) + if _sdk_client.is_x509_workload_identity(self.workload_identity): + _sdk_client.validate_x509_api_url(normalized_url) + self._base_url = self._enforce_trailing_slash(normalized_url) + self._base_url_was_default = False + self._data_residency = None + + +def sync__init__( + _owner: type[_sdk_client.OpenAI], + /, + self: _sdk_client.OpenAI, + *, + api_key: str | Callable[[], str] | None = None, + admin_api_key: str | None = None, + workload_identity: WorkloadIdentity | X509WorkloadIdentity | None = None, + organization: str | None = None, + project: str | None = None, + webhook_secret: str | None = None, + provider: _Provider | None = None, + base_url: str | httpx2.URL | None | NotGiven = not_given, + data_residency: DataResidency | None = None, + websocket_base_url: str | httpx2.URL | None = None, + timeout: float | Timeout | None | NotGiven = not_given, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx2.Client | None = None, + _strict_response_validation: bool = False, + _enforce_credentials: bool = True, +) -> None: + """Construct a new synchronous OpenAI client instance. + + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `api_key` from `OPENAI_API_KEY` + - `admin_api_key` from `OPENAI_ADMIN_KEY` + - `organization` from `OPENAI_ORG_ID` + - `project` from `OPENAI_PROJECT_ID` + - `webhook_secret` from `OPENAI_WEBHOOK_SECRET` + + When `provider` is supplied, authentication and the base URL are configured by that provider instead. + `data_residency` selects an OpenAI regional endpoint and cannot be combined with + `base_url`, `websocket_base_url`, or `provider`. + """ + base_url = _sdk_client.resolve_data_residency( + data_residency, base_url, provider=provider, websocket_base_url=websocket_base_url + ) + base_url = _sdk_client.x509_data_residency_base_url(base_url, data_residency, workload_identity) + provider_runtime: _ProviderRuntime | None = None + if provider is not None: + provider_name = _sdk_client._provider_name(provider) + conflicts = [ + name + for name, value in ( + ("api_key", api_key), + ("admin_api_key", admin_api_key), + ("workload_identity", workload_identity), + ("base_url", base_url), + ) + if value is not None + ] + if conflicts: + formatted = ", ".join((f"`{name}`" for name in conflicts)) + raise _sdk_client.OpenAIError( + f"`provider` cannot be combined with top-level {formatted}. Move provider authentication and routing options into `{provider_name}(...)`." + ) + provider_runtime = _sdk_client._configure_provider(provider) + self._provider = provider + self._provider_runtime = provider_runtime + if ( + api_key is not None + and api_key != _sdk_client.WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER + and (workload_identity is not None) + ): + raise _sdk_client.OpenAIError("The `api_key` and `workload_identity` arguments are mutually exclusive") + if _sdk_client.is_x509_workload_identity(workload_identity): + workload_identity = workload_identity.copy() + self.workload_identity = workload_identity if provider_runtime is None else None + if provider_runtime is not None: + self.api_key = "" + self._api_key_provider = None + self._workload_identity_auth = None + elif workload_identity is not None: + self.api_key = _sdk_client.WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER + self._api_key_provider = None + self._workload_identity_auth = None + else: + if api_key is None: + api_key = _sdk_client.os.environ.get("OPENAI_API_KEY") + if callable(api_key): + self.api_key = "" + self._api_key_provider = api_key + else: + self.api_key = api_key or "" + self._api_key_provider = None + self._workload_identity_auth = None + if admin_api_key is None and provider_runtime is None: + admin_api_key = _sdk_client.os.environ.get("OPENAI_ADMIN_KEY") + self.admin_api_key = admin_api_key if provider_runtime is None else None + if ( + provider_runtime is None + and _enforce_credentials + and (not self.api_key) + and (self._api_key_provider is None) + and (workload_identity is None) + and (self.admin_api_key is None) + ): + raise _sdk_client.OpenAIError( + "Missing credentials. Please pass an `api_key`, `workload_identity`, `admin_api_key`, or set the `OPENAI_API_KEY` or `OPENAI_ADMIN_KEY` environment variable." + ) + if organization is None and provider_runtime is None: + organization = _sdk_client.os.environ.get("OPENAI_ORG_ID") + self.organization = organization + if project is None and provider_runtime is None: + project = _sdk_client.os.environ.get("OPENAI_PROJECT_ID") + self.project = project + if webhook_secret is None: + webhook_secret = _sdk_client.os.environ.get("OPENAI_WEBHOOK_SECRET") + self.webhook_secret = webhook_secret + self.websocket_base_url = websocket_base_url + if _sdk_client.is_x509_workload_identity(workload_identity): + x509_identity = workload_identity + subject_token_identity = None + elif workload_identity is None: + x509_identity = None + subject_token_identity = None + elif "provider" in workload_identity: + x509_identity = None + subject_token_identity = workload_identity + else: + raise _sdk_client.OpenAIError( + "Invalid `workload_identity` configuration: expected an X.509 or subject-token identity" + ) + if provider_runtime is not None: + base_url = provider_runtime.base_url + elif base_url is None: + base_url = _sdk_client.os.environ.get("OPENAI_BASE_URL") + self._base_url_was_default = provider_runtime is None and base_url is None + self._data_residency = data_residency + if base_url is None: + base_url = _sdk_client.MTLS_API_BASE_URL if x509_identity is not None else "https://api.openai.com/v1" + if x509_identity is not None: + _sdk_client.validate_x509_api_url(base_url) + self._ambient_authorizations = frozenset() + custom_headers_env = _sdk_client.os.environ.get("OPENAI_CUSTOM_HEADERS") if provider_runtime is None else None + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + explicit_headers: Mapping[str, str] = default_headers if _sdk_client.is_mapping_t(default_headers) else {} + explicit_authorization = any((name.lower() == "authorization" for name in explicit_headers)) + if explicit_authorization: + parsed = {name: value for name, value in parsed.items() if name.lower() != "authorization"} + elif x509_identity is None: + self._ambient_authorizations = frozenset( + (value for name, value in parsed.items() if name.lower() == "authorization") + ) + default_headers = {**_sdk_client.x509_safe_environment_headers(parsed, x509_identity), **explicit_headers} + super(_owner, self).__init__( + version=_sdk_client.__version__, + base_url=base_url, + max_retries=max_retries, + timeout=timeout, + http_client=http_client, + custom_headers=default_headers, + custom_query=default_query, + _strict_response_validation=_strict_response_validation, + ) + if x509_identity is not None: + self._workload_identity_auth = _sdk_client.SyncX509WorkloadIdentityAuth( + workload_identity=x509_identity.copy(), http_client=self._client, max_retries=max_retries + ) + elif subject_token_identity is not None: + self._workload_identity_auth = _sdk_client.WorkloadIdentityAuth( + workload_identity=subject_token_identity, _use_httpx2=_sdk_client.is_httpx2_sync_client(self._client) + ) + self._default_stream_cls = _sdk_client.Stream + + +def sync_send_with_auth_retry( + _owner: type[_sdk_client.OpenAI], + /, + self: _sdk_client.OpenAI, + request: httpx2.Request, + *, + stream: bool, + retried: bool = False, + **kwargs: Unpack[HttpxSendArgs], +) -> httpx2.Response: + used_access_token: str | None = None + request_is_replayable = False + x509_auth = self._workload_identity_auth + if x509_auth is not None: + if isinstance(x509_auth, _sdk_client.SyncX509WorkloadIdentityAuth): + if x509_auth.workload_identity != self.workload_identity: + raise _sdk_client.OpenAIError("X.509 workload identity cannot be changed after client construction") + _sdk_client.validate_x509_api_url(request.url, expected_origin=self.base_url) + _sdk_client.validate_x509_request_authority(request) + _sdk_client.validate_x509_api_credentials(request) + if x509_auth._follow_redirects is not None: + kwargs["follow_redirects"] = x509_auth._follow_redirects + authorization = request.headers.get("Authorization") + if authorization == f"Bearer {_sdk_client.WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER}": + used_access_token = ( + x509_auth.get_token_for_request(request) + if isinstance(x509_auth, _sdk_client.SyncX509WorkloadIdentityAuth) + else x509_auth.get_token() + ) + request.headers["Authorization"] = f"Bearer {used_access_token}" + request_is_replayable = x509_auth._can_retry_request(request) + if isinstance(x509_auth, _sdk_client.SyncX509WorkloadIdentityAuth): + response = x509_auth.send_api_request( + request, + expected_origin=self.base_url, + expected_authorization=request.headers.get("Authorization"), + stream=stream, + **kwargs, + ) + else: + response = super(_owner, self)._send_request(request, stream=stream, **kwargs) + if response.status_code != 401 or self._workload_identity_auth is None or used_access_token is None: + return response + self._workload_identity_auth.invalidate_token(used_access_token) + if retried or not request_is_replayable: + return response + response.close() + self._workload_identity_auth._prepare_retry_request(request) + request.headers["Authorization"] = f"Bearer {_sdk_client.WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER}" + return self._send_with_auth_retry(request, stream=stream, retried=True, **kwargs) + + +def sync_send_request( + self: _sdk_client.OpenAI, request: httpx2.Request, *, stream: bool, **kwargs: Unpack[HttpxSendArgs] +) -> httpx2.Response: + response = self._send_with_auth_retry(request, stream=stream, **kwargs) + if self._provider_runtime is not None and self._provider_runtime.normalize_response is not None: + response = self._provider_runtime.normalize_response(response) + return response + + +def sync_auth_headers(self: _sdk_client.OpenAI, security: SecurityOptions) -> dict[str, str]: + if self._provider_runtime is not None: + return {} + headers: dict[str, str] = {} + if security.get("bearer_auth", False): + for key, value in self._bearer_auth.items(): + headers.setdefault(key, value) + if security.get("admin_api_key_auth", False): + for key, value in self._admin_api_key_auth.items(): + headers.setdefault(key, value) + return headers + + +def sync_bearer_auth(self: _sdk_client.OpenAI) -> dict[str, str]: + api_key = self.api_key + if not api_key: + return {} + return {"Authorization": f"Bearer {api_key}"} + + +def syncauth_headers(self: _sdk_client.OpenAI) -> dict[str, str]: + if self._provider_runtime is not None: + return {} + api_key = self.api_key + if not api_key or api_key == _sdk_client.WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER: + return {} + return {"Authorization": f"Bearer {api_key}"} + + +def sync_validate_headers(self: _sdk_client.OpenAI, headers: Headers, custom_headers: Headers) -> None: + if self._provider_runtime is not None: + return + if _sdk_client._has_header(headers, "Authorization") or _sdk_client._has_omitted_header( + custom_headers, "Authorization" + ): + return + raise TypeError( + "Could not resolve authentication method. Expected either api_key or admin_api_key to be set. Or for the `Authorization` header to be explicitly omitted." + ) + + +def sync_prepare_options( + _owner: type[_sdk_client.OpenAI], /, self: _sdk_client.OpenAI, options: FinalRequestOptions +) -> FinalRequestOptions: + if options.url.startswith(("ws://", "wss://")) and ( + self._provider_runtime is not None or self.workload_identity is not None + ): + raise _sdk_client.OpenAIError("This authentication method is not supported by WebSocket connections") + if self._provider_runtime is not None: + if self._provider_runtime.transform_request is not None: + options = self._provider_runtime.transform_request(options) + elif self._api_key_provider is not None and options.security.get("bearer_auth", False): + self._refresh_api_key() + return super(_owner, self)._prepare_options(options) + + +def sync_prepare_request(self: _sdk_client.OpenAI, request: httpx2.Request) -> None: + if self._provider_runtime is not None and self._provider_runtime.prepare_request is not None: + self._provider_runtime.prepare_request(request) + + +def sync_custom_auth( + _owner: type[_sdk_client.OpenAI], /, self: _sdk_client.OpenAI, security: SecurityOptions +) -> httpx2.Auth | None: + if self._provider_runtime is not None or isinstance( + self._workload_identity_auth, _sdk_client.SyncX509WorkloadIdentityAuth + ): + return _sdk_client.httpx2.Auth() + return super(_owner, self)._custom_auth(security) + + +def sync_refresh_api_key(self: _sdk_client.OpenAI) -> str: + if self._api_key_provider is not None: + self.api_key = self._api_key_provider() + return self.api_key + + +def synccopy( + self: _SyncClientT, + *, + api_key: str | Callable[[], str] | None = None, + admin_api_key: str | None = None, + workload_identity: WorkloadIdentity | X509WorkloadIdentity | None = None, + provider: _Provider | None | NotGiven = not_given, + organization: str | None = None, + project: str | None = None, + webhook_secret: str | None = None, + websocket_base_url: str | httpx2.URL | None = None, + base_url: str | httpx2.URL | None | NotGiven = not_given, + data_residency: DataResidency | None = None, + timeout: float | Timeout | None | NotGiven = not_given, + http_client: httpx2.Client | None = None, + max_retries: int | NotGiven = not_given, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _enforce_credentials: bool | None = None, + _extra_kwargs: Mapping[str, Any] = {}, +) -> _SyncClientT: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + `data_residency` replaces the inherited HTTP and WebSocket endpoints, without changing this client. + """ + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + provider_changed = not isinstance(provider, _sdk_client.NotGiven) and provider is not self._provider + inherited_organization = None if provider_changed else self.organization + inherited_project = None if provider_changed else self.project + headers: Mapping[str, str] = {} if provider_changed else self._custom_headers + if ( + _sdk_client.is_x509_workload_identity(workload_identity) + and (not _sdk_client.is_x509_workload_identity(self.workload_identity)) + and self._ambient_authorizations + ): + headers = { + name: value + for name, value in headers.items() + if name.lower() != "authorization" or value not in self._ambient_authorizations + } + if default_headers is not None: + if any((name.lower() == "authorization" for name in default_headers)): + headers = {name: value for name, value in headers.items() if name.lower() != "authorization"} + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + http_client = http_client or self._client + next_provider = self._provider if isinstance(provider, _sdk_client.NotGiven) else provider + explicit_base_url = base_url is not None and (not isinstance(base_url, _sdk_client.NotGiven)) + next_workload_identity = workload_identity if workload_identity is not None else self.workload_identity + if api_key is not None and workload_identity is None: + next_workload_identity = None + current_x509 = _sdk_client.is_x509_workload_identity(self.workload_identity) + next_x509 = _sdk_client.is_x509_workload_identity(next_workload_identity) + mode_changed = current_x509 != next_x509 + effective_data_residency = data_residency + if effective_data_residency is None and mode_changed and (not explicit_base_url): + effective_data_residency = self._data_residency + base_url = _sdk_client.resolve_data_residency( + effective_data_residency, + _sdk_client.not_given if base_url is None and data_residency is None else base_url, + provider=next_provider, + websocket_base_url=websocket_base_url, + ) + base_url = _sdk_client.x509_data_residency_base_url(base_url, effective_data_residency, next_workload_identity) + preserve_default_base_url = False + auth_options: dict[str, Any] + if next_provider is not None: + auth_options = { + "provider": next_provider, + "api_key": api_key, + "admin_api_key": admin_api_key, + "workload_identity": workload_identity, + "base_url": base_url, + } + elif self._provider is not None: + auth_options = { + "api_key": api_key, + "admin_api_key": admin_api_key, + "workload_identity": workload_identity, + "base_url": base_url, + } + else: + inherited_base_url = None if mode_changed and self._base_url_was_default else self.base_url + preserve_default_base_url = base_url is None and (not mode_changed) and self._base_url_was_default + auth_options = { + "api_key": api_key if workload_identity is not None else api_key or self._api_key_provider or self.api_key, + "admin_api_key": admin_api_key or self.admin_api_key, + "workload_identity": next_workload_identity, + "base_url": base_url or inherited_base_url, + } + copied = self.__class__( + organization=organization or inherited_organization, + project=project or inherited_project, + webhook_secret=webhook_secret or self.webhook_secret, + websocket_base_url=None if data_residency is not None else websocket_base_url or self.websocket_base_url, + timeout=self.timeout if isinstance(timeout, _sdk_client.NotGiven) else timeout, + http_client=http_client, + max_retries=max_retries if _sdk_client.is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + _enforce_credentials=True if _enforce_credentials is None else _enforce_credentials, + **auth_options, + **_extra_kwargs, + ) + if preserve_default_base_url: + copied._base_url_was_default = True + overridden_authorizations = default_headers if default_headers is not None else set_default_headers + explicit_authorization_override = overridden_authorizations is not None and any( + (name.lower() == "authorization" for name in overridden_authorizations) + ) + if ( + self._ambient_authorizations + and (not explicit_authorization_override) + and any( + ( + name.lower() == "authorization" and value in self._ambient_authorizations + for name, value in copied._custom_headers.items() + ) + ) + ): + copied._ambient_authorizations = self._ambient_authorizations + if data_residency is not None: + copied._data_residency = data_residency + elif not explicit_base_url and (not provider_changed): + copied._data_residency = self._data_residency + return copied + + +def asyncbase_url(self: _sdk_client.AsyncOpenAI) -> httpx2.URL: + return self._base_url + + +def asyncbase_url_set(self: _sdk_client.AsyncOpenAI, url: httpx2.URL | str) -> None: + normalized_url = _sdk_client.normalize_httpx_url(url) + if _sdk_client.is_x509_workload_identity(self.workload_identity): + _sdk_client.validate_x509_api_url(normalized_url) + self._base_url = self._enforce_trailing_slash(normalized_url) + self._base_url_was_default = False + self._data_residency = None + + +def async__init__( + _owner: type[_sdk_client.AsyncOpenAI], + /, + self: _sdk_client.AsyncOpenAI, + *, + api_key: str | Callable[[], Awaitable[str]] | None = None, + admin_api_key: str | None = None, + workload_identity: WorkloadIdentity | X509WorkloadIdentity | None = None, + organization: str | None = None, + project: str | None = None, + webhook_secret: str | None = None, + provider: _Provider | None = None, + base_url: str | httpx2.URL | None | NotGiven = not_given, + data_residency: DataResidency | None = None, + websocket_base_url: str | httpx2.URL | None = None, + timeout: float | Timeout | None | NotGiven = not_given, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx2.AsyncClient | None = None, + _strict_response_validation: bool = False, + _enforce_credentials: bool = True, +) -> None: + """Construct a new async AsyncOpenAI client instance. + + This automatically infers the following arguments from their corresponding environment variables if they are not provided: + - `api_key` from `OPENAI_API_KEY` + - `admin_api_key` from `OPENAI_ADMIN_KEY` + - `organization` from `OPENAI_ORG_ID` + - `project` from `OPENAI_PROJECT_ID` + - `webhook_secret` from `OPENAI_WEBHOOK_SECRET` + + When `provider` is supplied, authentication and the base URL are configured by that provider instead. + `data_residency` selects an OpenAI regional endpoint and cannot be combined with + `base_url`, `websocket_base_url`, or `provider`. + """ + base_url = _sdk_client.resolve_data_residency( + data_residency, base_url, provider=provider, websocket_base_url=websocket_base_url + ) + base_url = _sdk_client.x509_data_residency_base_url(base_url, data_residency, workload_identity) + provider_runtime: _ProviderRuntime | None = None + if provider is not None: + provider_name = _sdk_client._provider_name(provider) + conflicts = [ + name + for name, value in ( + ("api_key", api_key), + ("admin_api_key", admin_api_key), + ("workload_identity", workload_identity), + ("base_url", base_url), + ) + if value is not None + ] + if conflicts: + formatted = ", ".join((f"`{name}`" for name in conflicts)) + raise _sdk_client.OpenAIError( + f"`provider` cannot be combined with top-level {formatted}. Move provider authentication and routing options into `{provider_name}(...)`." + ) + provider_runtime = _sdk_client._configure_provider(provider) + self._provider = provider + self._provider_runtime = provider_runtime + if ( + api_key is not None + and api_key != _sdk_client.WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER + and (workload_identity is not None) + ): + raise _sdk_client.OpenAIError("The `api_key` and `workload_identity` arguments are mutually exclusive") + if _sdk_client.is_x509_workload_identity(workload_identity): + workload_identity = workload_identity.copy() + self.workload_identity = workload_identity if provider_runtime is None else None + if provider_runtime is not None: + self.api_key = "" + self._api_key_provider = None + self._workload_identity_auth = None + elif workload_identity is not None: + self.api_key = _sdk_client.WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER + self._api_key_provider = None + self._workload_identity_auth = None + else: + if api_key is None: + api_key = _sdk_client.os.environ.get("OPENAI_API_KEY") + if callable(api_key): + self.api_key = "" + self._api_key_provider = api_key + else: + self.api_key = api_key or "" + self._api_key_provider = None + self._workload_identity_auth = None + if admin_api_key is None and provider_runtime is None: + admin_api_key = _sdk_client.os.environ.get("OPENAI_ADMIN_KEY") + self.admin_api_key = admin_api_key if provider_runtime is None else None + if ( + provider_runtime is None + and _enforce_credentials + and (not self.api_key) + and (self._api_key_provider is None) + and (workload_identity is None) + and (self.admin_api_key is None) + ): + raise _sdk_client.OpenAIError( + "Missing credentials. Please pass an `api_key`, `workload_identity`, `admin_api_key`, or set the `OPENAI_API_KEY` or `OPENAI_ADMIN_KEY` environment variable." + ) + if organization is None and provider_runtime is None: + organization = _sdk_client.os.environ.get("OPENAI_ORG_ID") + self.organization = organization + if project is None and provider_runtime is None: + project = _sdk_client.os.environ.get("OPENAI_PROJECT_ID") + self.project = project + if webhook_secret is None: + webhook_secret = _sdk_client.os.environ.get("OPENAI_WEBHOOK_SECRET") + self.webhook_secret = webhook_secret + self.websocket_base_url = websocket_base_url + if _sdk_client.is_x509_workload_identity(workload_identity): + x509_identity = workload_identity + subject_token_identity = None + elif workload_identity is None: + x509_identity = None + subject_token_identity = None + elif "provider" in workload_identity: + x509_identity = None + subject_token_identity = workload_identity + else: + raise _sdk_client.OpenAIError( + "Invalid `workload_identity` configuration: expected an X.509 or subject-token identity" + ) + if provider_runtime is not None: + base_url = provider_runtime.base_url + elif base_url is None: + base_url = _sdk_client.os.environ.get("OPENAI_BASE_URL") + self._base_url_was_default = provider_runtime is None and base_url is None + self._data_residency = data_residency + if base_url is None: + base_url = _sdk_client.MTLS_API_BASE_URL if x509_identity is not None else "https://api.openai.com/v1" + if x509_identity is not None: + _sdk_client.validate_x509_api_url(base_url) + self._ambient_authorizations = frozenset() + custom_headers_env = _sdk_client.os.environ.get("OPENAI_CUSTOM_HEADERS") if provider_runtime is None else None + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + explicit_headers: Mapping[str, str] = default_headers if _sdk_client.is_mapping_t(default_headers) else {} + explicit_authorization = any((name.lower() == "authorization" for name in explicit_headers)) + if explicit_authorization: + parsed = {name: value for name, value in parsed.items() if name.lower() != "authorization"} + elif x509_identity is None: + self._ambient_authorizations = frozenset( + (value for name, value in parsed.items() if name.lower() == "authorization") + ) + default_headers = {**_sdk_client.x509_safe_environment_headers(parsed, x509_identity), **explicit_headers} + super(_owner, self).__init__( + version=_sdk_client.__version__, + base_url=base_url, + max_retries=max_retries, + timeout=timeout, + http_client=http_client, + custom_headers=default_headers, + custom_query=default_query, + _strict_response_validation=_strict_response_validation, + ) + if x509_identity is not None: + self._workload_identity_auth = _sdk_client.AsyncX509WorkloadIdentityAuth( + workload_identity=x509_identity.copy(), http_client=self._client, max_retries=max_retries + ) + elif subject_token_identity is not None: + self._workload_identity_auth = _sdk_client.WorkloadIdentityAuth( + workload_identity=subject_token_identity, _use_httpx2=_sdk_client.is_httpx2_async_client(self._client) + ) + self._default_stream_cls = _sdk_client.AsyncStream + + +async def async_send_with_auth_retry( + _owner: type[_sdk_client.AsyncOpenAI], + /, + self: _sdk_client.AsyncOpenAI, + request: httpx2.Request, + *, + stream: bool, + retried: bool = False, + **kwargs: Unpack[HttpxSendArgs], +) -> httpx2.Response: + used_access_token: str | None = None + request_is_replayable = False + x509_auth = self._workload_identity_auth + if x509_auth is not None: + if isinstance(x509_auth, _sdk_client.AsyncX509WorkloadIdentityAuth): + if x509_auth.workload_identity != self.workload_identity: + raise _sdk_client.OpenAIError("X.509 workload identity cannot be changed after client construction") + _sdk_client.validate_x509_api_url(request.url, expected_origin=self.base_url) + _sdk_client.validate_x509_request_authority(request) + _sdk_client.validate_x509_api_credentials(request) + if x509_auth._follow_redirects is not None: + kwargs["follow_redirects"] = x509_auth._follow_redirects + authorization = request.headers.get("Authorization") + if authorization == f"Bearer {_sdk_client.WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER}": + used_access_token = ( + await x509_auth.get_token_for_request(request) + if isinstance(x509_auth, _sdk_client.AsyncX509WorkloadIdentityAuth) + else await x509_auth.get_token_async() + ) + request.headers["Authorization"] = f"Bearer {used_access_token}" + request_is_replayable = x509_auth._can_retry_request(request) + if isinstance(x509_auth, _sdk_client.AsyncX509WorkloadIdentityAuth): + response = await x509_auth.send_api_request( + request, + expected_origin=self.base_url, + expected_authorization=request.headers.get("Authorization"), + stream=stream, + **kwargs, + ) + else: + response = await super(_owner, self)._send_request(request, stream=stream, **kwargs) + if response.status_code != 401 or self._workload_identity_auth is None or used_access_token is None: + return response + self._workload_identity_auth.invalidate_token(used_access_token) + if retried or not request_is_replayable: + return response + await response.aclose() + self._workload_identity_auth._prepare_retry_request(request) + request.headers["Authorization"] = f"Bearer {_sdk_client.WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER}" + return await self._send_with_auth_retry(request, stream=stream, retried=True, **kwargs) + + +async def async_send_request( + self: _sdk_client.AsyncOpenAI, request: httpx2.Request, *, stream: bool, **kwargs: Unpack[HttpxSendArgs] +) -> httpx2.Response: + response = await self._send_with_auth_retry(request, stream=stream, **kwargs) + if self._provider_runtime is not None: + if self._provider_runtime.normalize_async_response is not None: + response = await self._provider_runtime.normalize_async_response(response) + elif self._provider_runtime.normalize_response is not None: + response = self._provider_runtime.normalize_response(response) + return response + + +def async_auth_headers(self: _sdk_client.AsyncOpenAI, security: SecurityOptions) -> dict[str, str]: + if self._provider_runtime is not None: + return {} + headers: dict[str, str] = {} + if security.get("bearer_auth", False): + for key, value in self._bearer_auth.items(): + headers.setdefault(key, value) + if security.get("admin_api_key_auth", False): + for key, value in self._admin_api_key_auth.items(): + headers.setdefault(key, value) + return headers + + +def async_bearer_auth(self: _sdk_client.AsyncOpenAI) -> dict[str, str]: + api_key = self.api_key + if not api_key: + return {} + return {"Authorization": f"Bearer {api_key}"} + + +def asyncauth_headers(self: _sdk_client.AsyncOpenAI) -> dict[str, str]: + if self._provider_runtime is not None: + return {} + api_key = self.api_key + if not api_key or api_key == _sdk_client.WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER: + return {} + return {"Authorization": f"Bearer {api_key}"} + + +def async_validate_headers(self: _sdk_client.AsyncOpenAI, headers: Headers, custom_headers: Headers) -> None: + if self._provider_runtime is not None: + return + if _sdk_client._has_header(headers, "Authorization") or _sdk_client._has_omitted_header( + custom_headers, "Authorization" + ): + return + raise TypeError( + "Could not resolve authentication method. Expected either api_key or admin_api_key to be set. Or for the `Authorization` header to be explicitly omitted." + ) + + +async def async_prepare_options( + _owner: type[_sdk_client.AsyncOpenAI], /, self: _sdk_client.AsyncOpenAI, options: FinalRequestOptions +) -> FinalRequestOptions: + if options.url.startswith(("ws://", "wss://")) and ( + self._provider_runtime is not None or self.workload_identity is not None + ): + raise _sdk_client.OpenAIError("This authentication method is not supported by WebSocket connections") + if self._provider_runtime is not None: + if self._provider_runtime.transform_async_request is not None: + options = await self._provider_runtime.transform_async_request(options) + elif self._provider_runtime.transform_request is not None: + options = self._provider_runtime.transform_request(options) + elif self._api_key_provider is not None and options.security.get("bearer_auth", False): + await self._refresh_api_key() + return await super(_owner, self)._prepare_options(options) + + +async def async_prepare_request(self: _sdk_client.AsyncOpenAI, request: httpx2.Request) -> None: + if self._provider_runtime is None: + return + if self._provider_runtime.prepare_async_request is not None: + await self._provider_runtime.prepare_async_request(request) + elif self._provider_runtime.prepare_request is not None: + self._provider_runtime.prepare_request(request) + + +def asynccustom_auth(_owner: type[_sdk_client.AsyncOpenAI], /, self: _sdk_client.AsyncOpenAI) -> httpx2.Auth | None: + if self._provider_runtime is not None or isinstance( + self._workload_identity_auth, _sdk_client.AsyncX509WorkloadIdentityAuth + ): + return _sdk_client.httpx2.Auth() + return super(_owner, self).custom_auth + + +async def async_refresh_api_key(self: _sdk_client.AsyncOpenAI) -> str: + if self._api_key_provider is not None: + self.api_key = await self._api_key_provider() + return self.api_key + + +def asynccopy( + self: _AsyncClientT, + *, + api_key: str | Callable[[], Awaitable[str]] | None = None, + admin_api_key: str | None = None, + workload_identity: WorkloadIdentity | X509WorkloadIdentity | None = None, + provider: _Provider | None | NotGiven = not_given, + organization: str | None = None, + project: str | None = None, + webhook_secret: str | None = None, + websocket_base_url: str | httpx2.URL | None = None, + base_url: str | httpx2.URL | None | NotGiven = not_given, + data_residency: DataResidency | None = None, + timeout: float | Timeout | None | NotGiven = not_given, + http_client: httpx2.AsyncClient | None = None, + max_retries: int | NotGiven = not_given, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _enforce_credentials: bool | None = None, + _extra_kwargs: Mapping[str, Any] = {}, +) -> _AsyncClientT: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + `data_residency` replaces the inherited HTTP and WebSocket endpoints, without changing this client. + """ + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + provider_changed = not isinstance(provider, _sdk_client.NotGiven) and provider is not self._provider + inherited_organization = None if provider_changed else self.organization + inherited_project = None if provider_changed else self.project + headers: Mapping[str, str] = {} if provider_changed else self._custom_headers + if ( + _sdk_client.is_x509_workload_identity(workload_identity) + and (not _sdk_client.is_x509_workload_identity(self.workload_identity)) + and self._ambient_authorizations + ): + headers = { + name: value + for name, value in headers.items() + if name.lower() != "authorization" or value not in self._ambient_authorizations + } + if default_headers is not None: + if any((name.lower() == "authorization" for name in default_headers)): + headers = {name: value for name, value in headers.items() if name.lower() != "authorization"} + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + http_client = http_client or self._client + next_provider = self._provider if isinstance(provider, _sdk_client.NotGiven) else provider + explicit_base_url = base_url is not None and (not isinstance(base_url, _sdk_client.NotGiven)) + next_workload_identity = workload_identity if workload_identity is not None else self.workload_identity + if api_key is not None and workload_identity is None: + next_workload_identity = None + current_x509 = _sdk_client.is_x509_workload_identity(self.workload_identity) + next_x509 = _sdk_client.is_x509_workload_identity(next_workload_identity) + mode_changed = current_x509 != next_x509 + effective_data_residency = data_residency + if effective_data_residency is None and mode_changed and (not explicit_base_url): + effective_data_residency = self._data_residency + base_url = _sdk_client.resolve_data_residency( + effective_data_residency, + _sdk_client.not_given if base_url is None and data_residency is None else base_url, + provider=next_provider, + websocket_base_url=websocket_base_url, + ) + base_url = _sdk_client.x509_data_residency_base_url(base_url, effective_data_residency, next_workload_identity) + preserve_default_base_url = False + auth_options: dict[str, Any] + if next_provider is not None: + auth_options = { + "provider": next_provider, + "api_key": api_key, + "admin_api_key": admin_api_key, + "workload_identity": workload_identity, + "base_url": base_url, + } + elif self._provider is not None: + auth_options = { + "api_key": api_key, + "admin_api_key": admin_api_key, + "workload_identity": workload_identity, + "base_url": base_url, + } + else: + inherited_base_url = None if mode_changed and self._base_url_was_default else self.base_url + preserve_default_base_url = base_url is None and (not mode_changed) and self._base_url_was_default + auth_options = { + "api_key": api_key if workload_identity is not None else api_key or self._api_key_provider or self.api_key, + "admin_api_key": admin_api_key or self.admin_api_key, + "workload_identity": next_workload_identity, + "base_url": base_url or inherited_base_url, + } + copied = self.__class__( + organization=organization or inherited_organization, + project=project or inherited_project, + webhook_secret=webhook_secret or self.webhook_secret, + websocket_base_url=None if data_residency is not None else websocket_base_url or self.websocket_base_url, + timeout=self.timeout if isinstance(timeout, _sdk_client.NotGiven) else timeout, + http_client=http_client, + max_retries=max_retries if _sdk_client.is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + _enforce_credentials=True if _enforce_credentials is None else _enforce_credentials, + **auth_options, + **_extra_kwargs, + ) + if preserve_default_base_url: + copied._base_url_was_default = True + overridden_authorizations = default_headers if default_headers is not None else set_default_headers + explicit_authorization_override = overridden_authorizations is not None and any( + (name.lower() == "authorization" for name in overridden_authorizations) + ) + if ( + self._ambient_authorizations + and (not explicit_authorization_override) + and any( + ( + name.lower() == "authorization" and value in self._ambient_authorizations + for name, value in copied._custom_headers.items() + ) + ) + ): + copied._ambient_authorizations = self._ambient_authorizations + if data_residency is not None: + copied._data_residency = data_residency + elif not explicit_base_url and (not provider_changed): + copied._data_residency = self._data_residency + return copied diff --git a/src/openai/lib/_module_runtime.py b/src/openai/lib/_module_runtime.py new file mode 100644 index 0000000000..cb64ffd9ca --- /dev/null +++ b/src/openai/lib/_module_runtime.py @@ -0,0 +1,120 @@ +"""Handwritten module-client provider selection and credential routing.""" + +from __future__ import annotations + +import openai as _sdk + +from .._client import OpenAI + + +def has_openai_credentials() -> bool: + return _sdk._os.environ.get("OPENAI_API_KEY") is not None + + +def has_azure_credentials() -> bool: + return _sdk.azure_endpoint is not None or _sdk._os.environ.get("AZURE_OPENAI_API_KEY") is not None + + +def has_azure_ad_credentials() -> bool: + return ( + _sdk._os.environ.get("AZURE_OPENAI_AD_TOKEN") is not None + or _sdk.azure_ad_token is not None + or _sdk.azure_ad_token_provider is not None + ) + + +def load_client() -> OpenAI: + if _sdk._client is None: + if _sdk.azure_endpoint is None: + _sdk.azure_endpoint = _sdk._os.environ.get("AZURE_OPENAI_ENDPOINT") + if _sdk.api_version is None: + _sdk.api_version = _sdk._os.environ.get("OPENAI_API_VERSION") + if _sdk.api_type is None: + has_openai = _sdk._has_openai_credentials() + has_azure = _sdk._has_azure_credentials() + has_azure_ad = _sdk._has_azure_ad_credentials() + if has_openai and (has_azure or has_azure_ad): + raise _sdk._AmbiguousModuleClientUsageError() + if has_azure or has_azure_ad: + _sdk.api_type = "azure" + else: + _sdk.api_type = "openai" + if _sdk.api_type == "azure": + _sdk._client = _sdk._AzureModuleClient( # type: ignore + api_version=_sdk.api_version, + azure_endpoint=_sdk.azure_endpoint, + api_key=_sdk.api_key, + azure_ad_token=_sdk.azure_ad_token, + azure_ad_token_provider=_sdk.azure_ad_token_provider, + organization=_sdk.organization, + base_url=_sdk.base_url, + timeout=_sdk.timeout, + max_retries=_sdk.max_retries, + default_headers=_sdk.default_headers, + default_query=_sdk.default_query, + http_client=_sdk.http_client, + ) + return _sdk._client + if _sdk.api_type == "amazon-bedrock": + _sdk._client = _sdk._BedrockModuleClient( + api_key=_sdk.api_key, + bedrock_token_provider=_sdk.bedrock_token_provider, + organization=_sdk.organization, + project=_sdk.project, + webhook_secret=_sdk.webhook_secret, + base_url=_sdk.base_url, + timeout=_sdk.timeout, + max_retries=_sdk.max_retries, + default_headers=_sdk.default_headers, + default_query=_sdk.default_query, + http_client=_sdk.http_client, + ) + return _sdk._client + _sdk._client = _sdk._ModuleClient( + api_key=_sdk.api_key, + admin_api_key=_sdk.admin_api_key, + organization=_sdk.organization, + project=_sdk.project, + webhook_secret=_sdk.webhook_secret, + base_url=_sdk.base_url, + timeout=_sdk.timeout, + max_retries=_sdk.max_retries, + default_headers=_sdk.default_headers, + default_query=_sdk.default_query, + http_client=_sdk.http_client, + _enforce_credentials=False, + ) + return _sdk._client + return _sdk._client + + +def BedrockModuleClient_api_key(_self: _sdk._BedrockModuleClient) -> str | None: + return _sdk.api_key if _sdk.api_key is not None else _sdk._bedrock_api_key + + +def BedrockModuleClient_api_key_setter(_self: _sdk._BedrockModuleClient, value: str | None) -> None: + _sdk._bedrock_api_key = value + + +def BedrockModuleClient_refresh_api_key( + _owner: type[_sdk._BedrockModuleClient], /, self: _sdk._BedrockModuleClient +) -> str: + if _sdk.api_key is not None: + return _sdk.api_key + return super(_owner, self)._refresh_api_key() + + +def BedrockModuleClient_legacy_auth_configuration( + _owner: type[_sdk._BedrockModuleClient], /, self: _sdk._BedrockModuleClient +) -> _sdk._bedrock._LegacyAuthConfiguration: + if _sdk.api_key is not None: + return ("bearer", _sdk.api_key) + return super(_owner, self)._legacy_auth_configuration() + + +def AmbiguousModuleClientUsageError_init( + _owner: type[_sdk._AmbiguousModuleClientUsageError], /, self: _sdk._AmbiguousModuleClientUsageError +) -> None: + super(_owner, self).__init__( + "Ambiguous use of module client; please set `openai.api_type` or the `OPENAI_API_TYPE` environment variable to `openai`, `azure`, or `amazon-bedrock`" + ) diff --git a/src/openai/lib/_parsing/_embeddings.py b/src/openai/lib/_parsing/_embeddings.py index 5b03068bcd..e64185e1b5 100644 --- a/src/openai/lib/_parsing/_embeddings.py +++ b/src/openai/lib/_parsing/_embeddings.py @@ -3,10 +3,12 @@ import array import base64 from typing import cast +from functools import partial from ..._types import Omit, NotGiven from ..._utils import is_given from ..._extras import numpy as np, has_numpy +from .._request_context import PreparedRequestContext from ...types.create_embedding_response import CreateEmbeddingResponse @@ -37,3 +39,22 @@ def parse_embedding_response( ).tolist() return obj + + +def prepare_embedding_request( + _resource: object, + body: dict[str, object], + *, + encoding_format: str | Omit | NotGiven, +) -> PreparedRequestContext: + # Match the released request's insertion order and preserve future fields. + params: dict[str, object] = { + name: body[name] for name in ("model", "input", "user", "dimensions", "encoding_format") if name in body + } + params.update({name: value for name, value in body.items() if name not in params}) + if not is_given(encoding_format): + params["encoding_format"] = "base64" + return PreparedRequestContext( + body=params, + post_parser=partial(parse_embedding_response, encoding_format=encoding_format), + ) diff --git a/src/openai/lib/_request_context.py b/src/openai/lib/_request_context.py new file mode 100644 index 0000000000..270dff7b8f --- /dev/null +++ b/src/openai/lib/_request_context.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from .._types import PostParser + + +@dataclass(frozen=True) +class PreparedRequestContext: + """Runtime preparation consumed by a generated HTTP request facade.""" + + body: object + post_parser: PostParser diff --git a/src/openai/lib/_response_resources.py b/src/openai/lib/_response_resources.py new file mode 100644 index 0000000000..3442821951 --- /dev/null +++ b/src/openai/lib/_response_resources.py @@ -0,0 +1,643 @@ +from __future__ import annotations + +from copy import copy +from typing import TYPE_CHECKING, Any, List, Type, Union, Iterable, Optional, cast +from functools import partial +from typing_extensions import Literal + +import httpx2 + +from openai._types import NOT_GIVEN, Body, Omit, Query, Headers, NotGiven, omit +from openai._utils import is_given, maybe_transform +from openai._streaming import Stream +from openai.lib._tools import PydanticFunctionTool, ResponsesPydanticFunctionTool +from openai._base_client import make_request_options +from openai.types.responses import response_create_params +from openai.lib._parsing._responses import ( + TextFormatT, + parse_response, + type_to_text_format_param as _type_to_text_format_param, +) +from openai.types.responses.response import Response +from openai.types.responses.tool_param import ToolParam, ParseableToolParam +from openai.types.responses.service_tier import ServiceTier +from openai.types.shared_params.metadata import Metadata +from openai.types.shared_params.reasoning import Reasoning +from openai.types.responses.parsed_response import ParsedResponse +from openai.lib.streaming.responses._responses import ResponseStreamManager, AsyncResponseStreamManager +from openai.types.responses.response_includable import ResponseIncludable +from openai.types.shared_params.responses_model import ResponsesModel +from openai.types.responses.response_input_param import ResponseInputParam +from openai.types.responses.response_prompt_param import ResponsePromptParam +from openai.types.responses.response_stream_event import ResponseStreamEvent +from openai.types.responses.response_text_config_param import ResponseTextConfigParam + +if TYPE_CHECKING: + from openai.resources.responses.responses import Responses, AsyncResponses + + +def parse( + resource: Responses, + *, + text_format: type[TextFormatT] | Omit = omit, + access_programs: response_create_params.AccessPrograms | Omit = omit, + background: Optional[bool] | Omit = omit, + context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit, + conversation: Optional[response_create_params.Conversation] | Omit = omit, + include: Optional[List[ResponseIncludable]] | Omit = omit, + input: Union[str, ResponseInputParam] | Omit = omit, + instructions: Optional[str] | Omit = omit, + max_output_tokens: Optional[int] | Omit = omit, + max_tool_calls: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + model: ResponsesModel | Omit = omit, + moderation: Optional[response_create_params.Moderation] | Omit = omit, + parallel_tool_calls: Optional[bool] | Omit = omit, + previous_response_id: Optional[str] | Omit = omit, + prompt: Optional[ResponsePromptParam] | Omit = omit, + prompt_cache_key: Optional[str] | Omit = omit, + prompt_cache_options: response_create_params.PromptCacheOptions | Omit = omit, + prompt_cache_retention: Optional[Literal["in_memory", "24h"]] | Omit = omit, + reasoning: Optional[Reasoning] | Omit = omit, + safety_identifier: Optional[str] | Omit = omit, + service_tier: Optional[ServiceTier] | Omit = omit, + store: Optional[bool] | Omit = omit, + stream: Optional[Literal[False]] | Literal[True] | Omit = omit, + stream_options: Optional[response_create_params.StreamOptions] | Omit = omit, + temperature: Optional[float] | Omit = omit, + text: ResponseTextConfigParam | Omit = omit, + tool_choice: response_create_params.ToolChoice | Omit = omit, + tools: Iterable[ParseableToolParam] | Omit = omit, + top_logprobs: Optional[int] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation: Optional[Literal["auto", "disabled"]] | Omit = omit, + user: str | Omit = omit, + verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> ParsedResponse[TextFormatT]: + if is_given(text_format): + if not text: + text = {} + + if "format" in text: + raise TypeError("Cannot mix and match text.format with text_format") + text = copy(text) + text["format"] = _type_to_text_format_param(text_format) + + tools = _make_tools(tools) + + def parser(raw_response: Response) -> ParsedResponse[TextFormatT]: + return parse_response( + input_tools=tools, + text_format=text_format, + response=raw_response, + ) + + return resource._post( + "/responses", + body=maybe_transform( + { + "model": model, + "stream": stream, + "service_tier": service_tier, + "access_programs": access_programs, + "background": background, + "context_management": context_management, + "conversation": conversation, + "include": include, + "input": input, + "instructions": instructions, + "max_output_tokens": max_output_tokens, + "max_tool_calls": max_tool_calls, + "metadata": metadata, + "moderation": moderation, + "parallel_tool_calls": parallel_tool_calls, + "previous_response_id": previous_response_id, + "prompt": prompt, + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "reasoning": reasoning, + "safety_identifier": safety_identifier, + "store": store, + "stream_options": stream_options, + "temperature": temperature, + "text": text, + "tool_choice": tool_choice, + "tools": tools, + "top_logprobs": top_logprobs, + "top_p": top_p, + "truncation": truncation, + "user": user, + "verbosity": verbosity, + }, + response_create_params.ResponseCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + post_parser=parser, + security={"bearer_auth": True}, + ), + # we turn the `Response` instance into a `ParsedResponse` + # in the `parser` function above + cast_to=cast("Type[ParsedResponse[TextFormatT]]", Response), + ) + + +async def async_parse( + resource: AsyncResponses, + *, + text_format: type[TextFormatT] | Omit = omit, + access_programs: response_create_params.AccessPrograms | Omit = omit, + background: Optional[bool] | Omit = omit, + context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit, + conversation: Optional[response_create_params.Conversation] | Omit = omit, + include: Optional[List[ResponseIncludable]] | Omit = omit, + input: Union[str, ResponseInputParam] | Omit = omit, + instructions: Optional[str] | Omit = omit, + max_output_tokens: Optional[int] | Omit = omit, + max_tool_calls: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + model: ResponsesModel | Omit = omit, + moderation: Optional[response_create_params.Moderation] | Omit = omit, + parallel_tool_calls: Optional[bool] | Omit = omit, + previous_response_id: Optional[str] | Omit = omit, + prompt: Optional[ResponsePromptParam] | Omit = omit, + prompt_cache_key: Optional[str] | Omit = omit, + prompt_cache_options: response_create_params.PromptCacheOptions | Omit = omit, + prompt_cache_retention: Optional[Literal["in_memory", "24h"]] | Omit = omit, + reasoning: Optional[Reasoning] | Omit = omit, + safety_identifier: Optional[str] | Omit = omit, + service_tier: Optional[ServiceTier] | Omit = omit, + store: Optional[bool] | Omit = omit, + stream: Optional[Literal[False]] | Literal[True] | Omit = omit, + stream_options: Optional[response_create_params.StreamOptions] | Omit = omit, + temperature: Optional[float] | Omit = omit, + text: ResponseTextConfigParam | Omit = omit, + tool_choice: response_create_params.ToolChoice | Omit = omit, + tools: Iterable[ParseableToolParam] | Omit = omit, + top_logprobs: Optional[int] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation: Optional[Literal["auto", "disabled"]] | Omit = omit, + user: str | Omit = omit, + verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> ParsedResponse[TextFormatT]: + if is_given(text_format): + if not text: + text = {} + + if "format" in text: + raise TypeError("Cannot mix and match text.format with text_format") + text = copy(text) + text["format"] = _type_to_text_format_param(text_format) + + tools = _make_tools(tools) + + def parser(raw_response: Response) -> ParsedResponse[TextFormatT]: + return parse_response( + input_tools=tools, + text_format=text_format, + response=raw_response, + ) + + return await resource._post( + "/responses", + body=maybe_transform( + { + "model": model, + "stream": stream, + "service_tier": service_tier, + "access_programs": access_programs, + "background": background, + "context_management": context_management, + "conversation": conversation, + "include": include, + "input": input, + "instructions": instructions, + "max_output_tokens": max_output_tokens, + "max_tool_calls": max_tool_calls, + "metadata": metadata, + "moderation": moderation, + "parallel_tool_calls": parallel_tool_calls, + "previous_response_id": previous_response_id, + "prompt": prompt, + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "reasoning": reasoning, + "safety_identifier": safety_identifier, + "store": store, + "stream_options": stream_options, + "temperature": temperature, + "text": text, + "tool_choice": tool_choice, + "tools": tools, + "top_logprobs": top_logprobs, + "top_p": top_p, + "truncation": truncation, + "user": user, + "verbosity": verbosity, + }, + response_create_params.ResponseCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + post_parser=parser, + security={"bearer_auth": True}, + ), + # we turn the `Response` instance into a `ParsedResponse` + # in the `parser` function above + cast_to=cast("Type[ParsedResponse[TextFormatT]]", Response), + ) + + +def stream( + resource: Responses, + *, + response_id: str | Omit = omit, + input: Union[str, ResponseInputParam] | Omit = omit, + model: ResponsesModel | Omit = omit, + access_programs: response_create_params.AccessPrograms | Omit = omit, + background: Optional[bool] | Omit = omit, + context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit, + text_format: type[TextFormatT] | Omit = omit, + tools: Iterable[ParseableToolParam] | Omit = omit, + conversation: Optional[response_create_params.Conversation] | Omit = omit, + include: Optional[List[ResponseIncludable]] | Omit = omit, + instructions: Optional[str] | Omit = omit, + max_output_tokens: Optional[int] | Omit = omit, + max_tool_calls: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + parallel_tool_calls: Optional[bool] | Omit = omit, + moderation: Optional[response_create_params.Moderation] | Omit = omit, + previous_response_id: Optional[str] | Omit = omit, + prompt: Optional[ResponsePromptParam] | Omit = omit, + prompt_cache_key: Optional[str] | Omit = omit, + prompt_cache_options: response_create_params.PromptCacheOptions | Omit = omit, + prompt_cache_retention: Optional[Literal["in_memory", "24h"]] | Omit = omit, + reasoning: Optional[Reasoning] | Omit = omit, + safety_identifier: Optional[str] | Omit = omit, + service_tier: Optional[ServiceTier] | Omit = omit, + store: Optional[bool] | Omit = omit, + stream_options: Optional[response_create_params.StreamOptions] | Omit = omit, + temperature: Optional[float] | Omit = omit, + text: ResponseTextConfigParam | Omit = omit, + tool_choice: response_create_params.ToolChoice | Omit = omit, + top_logprobs: Optional[int] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation: Optional[Literal["auto", "disabled"]] | Omit = omit, + user: str | Omit = omit, + starting_after: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> ResponseStreamManager[TextFormatT]: + new_response_args = { + "access_programs": access_programs, + "input": input, + "model": model, + "context_management": context_management, + "conversation": conversation, + "include": include, + "instructions": instructions, + "max_output_tokens": max_output_tokens, + "max_tool_calls": max_tool_calls, + "metadata": metadata, + "moderation": moderation, + "parallel_tool_calls": parallel_tool_calls, + "previous_response_id": previous_response_id, + "prompt": prompt, + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "reasoning": reasoning, + "safety_identifier": safety_identifier, + "service_tier": service_tier, + "store": store, + "stream_options": stream_options, + "temperature": temperature, + "text": text, + "tool_choice": tool_choice, + "top_logprobs": top_logprobs, + "top_p": top_p, + "truncation": truncation, + "user": user, + "background": background, + } + new_response_args_names = [k for k, v in new_response_args.items() if is_given(v)] + + if (is_given(response_id) or is_given(starting_after)) and len(new_response_args_names) > 0: + raise ValueError( + "Cannot provide both response_id/starting_after can't be provided together with " + + ", ".join(new_response_args_names) + ) + tools = _make_tools(tools) + if len(new_response_args_names) > 0: + if not is_given(input): + raise ValueError("input must be provided when creating a new response") + + if not is_given(model): + raise ValueError("model must be provided when creating a new response") + + if is_given(text_format): + if not text: + text = {} + + if "format" in text: + raise TypeError("Cannot mix and match text.format with text_format") + + text = copy(text) + text["format"] = _type_to_text_format_param(text_format) + + api_request: partial[Stream[ResponseStreamEvent]] = partial( + resource.create, + access_programs=access_programs, + input=input, + model=model, + tools=tools, + context_management=context_management, + conversation=conversation, + include=include, + instructions=instructions, + max_output_tokens=max_output_tokens, + max_tool_calls=max_tool_calls, + metadata=metadata, + moderation=moderation, + parallel_tool_calls=parallel_tool_calls, + previous_response_id=previous_response_id, + prompt=prompt, + prompt_cache_key=prompt_cache_key, + prompt_cache_options=prompt_cache_options, + prompt_cache_retention=prompt_cache_retention, + store=store, + stream_options=stream_options, + stream=True, + temperature=temperature, + text=text, + tool_choice=tool_choice, + reasoning=reasoning, + safety_identifier=safety_identifier, + service_tier=service_tier, + top_logprobs=top_logprobs, + top_p=top_p, + truncation=truncation, + user=user, + background=background, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + + return ResponseStreamManager( + api_request, + text_format=text_format, + input_tools=tools, + starting_after=None, + ) + else: + if not is_given(response_id): + raise ValueError("id must be provided when streaming an existing response") + + return ResponseStreamManager( + lambda: resource.retrieve( + response_id=response_id, + stream=True, + include=include or [], + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + starting_after=omit, + timeout=timeout, + ), + text_format=text_format, + input_tools=tools, + starting_after=starting_after if is_given(starting_after) else None, + ) + + +def async_stream( + resource: AsyncResponses, + *, + response_id: str | Omit = omit, + input: Union[str, ResponseInputParam] | Omit = omit, + model: ResponsesModel | Omit = omit, + access_programs: response_create_params.AccessPrograms | Omit = omit, + background: Optional[bool] | Omit = omit, + context_management: Optional[Iterable[response_create_params.ContextManagement]] | Omit = omit, + text_format: type[TextFormatT] | Omit = omit, + tools: Iterable[ParseableToolParam] | Omit = omit, + conversation: Optional[response_create_params.Conversation] | Omit = omit, + include: Optional[List[ResponseIncludable]] | Omit = omit, + instructions: Optional[str] | Omit = omit, + max_output_tokens: Optional[int] | Omit = omit, + max_tool_calls: Optional[int] | Omit = omit, + metadata: Optional[Metadata] | Omit = omit, + parallel_tool_calls: Optional[bool] | Omit = omit, + moderation: Optional[response_create_params.Moderation] | Omit = omit, + previous_response_id: Optional[str] | Omit = omit, + prompt: Optional[ResponsePromptParam] | Omit = omit, + prompt_cache_key: Optional[str] | Omit = omit, + prompt_cache_options: response_create_params.PromptCacheOptions | Omit = omit, + prompt_cache_retention: Optional[Literal["in_memory", "24h"]] | Omit = omit, + reasoning: Optional[Reasoning] | Omit = omit, + safety_identifier: Optional[str] | Omit = omit, + service_tier: Optional[ServiceTier] | Omit = omit, + store: Optional[bool] | Omit = omit, + stream_options: Optional[response_create_params.StreamOptions] | Omit = omit, + temperature: Optional[float] | Omit = omit, + text: ResponseTextConfigParam | Omit = omit, + tool_choice: response_create_params.ToolChoice | Omit = omit, + top_logprobs: Optional[int] | Omit = omit, + top_p: Optional[float] | Omit = omit, + truncation: Optional[Literal["auto", "disabled"]] | Omit = omit, + user: str | Omit = omit, + starting_after: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, +) -> AsyncResponseStreamManager[TextFormatT]: + new_response_args = { + "access_programs": access_programs, + "input": input, + "model": model, + "context_management": context_management, + "conversation": conversation, + "include": include, + "instructions": instructions, + "max_output_tokens": max_output_tokens, + "max_tool_calls": max_tool_calls, + "metadata": metadata, + "moderation": moderation, + "parallel_tool_calls": parallel_tool_calls, + "previous_response_id": previous_response_id, + "prompt": prompt, + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "reasoning": reasoning, + "safety_identifier": safety_identifier, + "service_tier": service_tier, + "store": store, + "stream_options": stream_options, + "temperature": temperature, + "text": text, + "tool_choice": tool_choice, + "top_logprobs": top_logprobs, + "top_p": top_p, + "truncation": truncation, + "user": user, + "background": background, + } + new_response_args_names = [k for k, v in new_response_args.items() if is_given(v)] + + if (is_given(response_id) or is_given(starting_after)) and len(new_response_args_names) > 0: + raise ValueError( + "Cannot provide both response_id/starting_after can't be provided together with " + + ", ".join(new_response_args_names) + ) + + tools = _make_tools(tools) + if len(new_response_args_names) > 0: + if isinstance(input, NotGiven): + raise ValueError("input must be provided when creating a new response") + + if not is_given(model): + raise ValueError("model must be provided when creating a new response") + + if is_given(text_format): + if not text: + text = {} + + if "format" in text: + raise TypeError("Cannot mix and match text.format with text_format") + text = copy(text) + text["format"] = _type_to_text_format_param(text_format) + + api_request = resource.create( + access_programs=access_programs, + input=input, + model=model, + stream=True, + tools=tools, + context_management=context_management, + conversation=conversation, + include=include, + instructions=instructions, + max_output_tokens=max_output_tokens, + max_tool_calls=max_tool_calls, + metadata=metadata, + moderation=moderation, + parallel_tool_calls=parallel_tool_calls, + previous_response_id=previous_response_id, + prompt=prompt, + prompt_cache_key=prompt_cache_key, + prompt_cache_options=prompt_cache_options, + prompt_cache_retention=prompt_cache_retention, + store=store, + stream_options=stream_options, + temperature=temperature, + text=text, + tool_choice=tool_choice, + reasoning=reasoning, + safety_identifier=safety_identifier, + service_tier=service_tier, + top_logprobs=top_logprobs, + top_p=top_p, + truncation=truncation, + user=user, + background=background, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + + return AsyncResponseStreamManager( + api_request, + text_format=text_format, + input_tools=tools, + starting_after=None, + ) + else: + if isinstance(response_id, Omit): + raise ValueError("response_id must be provided when streaming an existing response") + + api_request = resource.retrieve( + response_id, + stream=True, + include=include or [], + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) + return AsyncResponseStreamManager( + api_request, + text_format=text_format, + input_tools=tools, + starting_after=starting_after if is_given(starting_after) else None, + ) + + +def _make_tools(tools: Iterable[ParseableToolParam] | Omit) -> List[ToolParam] | Omit: + if not is_given(tools): + return omit + + converted_tools: List[ToolParam] = [] + for tool in tools: + if tool["type"] != "function": + converted_tools.append(tool) + continue + + if "function" not in tool: + # standard Responses API case + converted_tools.append(tool) + continue + + function = cast(Any, tool)["function"] # pyright: ignore[reportUnnecessaryCast] + if not isinstance(function, PydanticFunctionTool): + raise Exception( + "Expected Chat Completions function tool shape to be created using `openai.pydantic_function_tool()`" + ) + + assert "parameters" in function + new_tool = ResponsesPydanticFunctionTool( + { + "type": "function", + "name": function["name"], + "description": function.get("description"), + "parameters": function["parameters"], + "strict": function.get("strict") or False, + }, + function.model, + ) + + # Retain applicable options added to the compatibility definition. + new_tool.update({key: value for key, value in function.items() if key not in new_tool}) + converted_tools.append(new_tool.cast()) + + return converted_tools diff --git a/src/openai/lib/beta/agents/_create.py b/src/openai/lib/beta/agents/_create.py new file mode 100644 index 0000000000..b66c67a976 --- /dev/null +++ b/src/openai/lib/beta/agents/_create.py @@ -0,0 +1,69 @@ +"""Local output binding and tool dispatch for generated session creation.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Mapping, TypeVar, cast +from dataclasses import dataclass + +import httpx2 + +from ._output import bind_output_type, with_output_schema +from ...._types import Omit, Headers, NotGiven +from ...streaming.agents import ToolHandler, AsyncToolHandler +from ...streaming.agents._dispatch import ToolDispatcher, AsyncToolDispatcher +from ....types.beta.agents.session_create_params import Agent + +if TYPE_CHECKING: + from ....resources.beta.agents.sessions import Sessions, AsyncSessions + +ResponseT = TypeVar("ResponseT") + + +@dataclass +class SessionCreateContext: + body: dict[str, object] + output_type: type[Any] | None + dispatcher: ToolDispatcher | AsyncToolDispatcher | None + + def post_parser(self, response: ResponseT) -> ResponseT: + return bind_output_type(response, self.output_type, self.dispatcher) + + +def prepare_session_create( + resource: Sessions, + body: dict[str, object], + *, + output_type: type[Any] | None, + tool_handlers: Mapping[str, ToolHandler] | None, + stream: object, + extra_headers: Headers | None, + timeout: float | httpx2.Timeout | None | NotGiven, +) -> SessionCreateContext: + body["agent"] = with_output_schema(cast("Agent | Omit", body["agent"]), output_type) + if tool_handlers is not None and stream is not True: + raise ValueError("tool_handlers requires stream=True") + dispatcher = ( + ToolDispatcher(resource, tool_handlers, extra_headers=extra_headers, timeout=timeout) if tool_handlers else None + ) + return SessionCreateContext(body, output_type, dispatcher) + + +def prepare_async_session_create( + resource: AsyncSessions, + body: dict[str, object], + *, + output_type: type[Any] | None, + tool_handlers: Mapping[str, AsyncToolHandler] | None, + stream: object, + extra_headers: Headers | None, + timeout: float | httpx2.Timeout | None | NotGiven, +) -> SessionCreateContext: + body["agent"] = with_output_schema(cast("Agent | Omit", body["agent"]), output_type) + if tool_handlers is not None and stream is not True: + raise ValueError("tool_handlers requires stream=True") + dispatcher = ( + AsyncToolDispatcher(resource, tool_handlers, extra_headers=extra_headers, timeout=timeout) + if tool_handlers + else None + ) + return SessionCreateContext(body, output_type, dispatcher) diff --git a/src/openai/resources/audio/transcriptions.py b/src/openai/resources/audio/transcriptions.py index 17f4d67fa5..584545eca5 100644 --- a/src/openai/resources/audio/transcriptions.py +++ b/src/openai/resources/audio/transcriptions.py @@ -8,7 +8,10 @@ import httpx2 +import openai.lib._parsing._audio + from ... import _legacy_response +from ...types import AudioResponseFormat from ..._files import deepcopy_with_paths from ..._types import ( Body, @@ -29,9 +32,7 @@ from ...types.audio import transcription_create_params from ..._base_client import make_request_options from ...types.audio_model import AudioModel -from ...lib._parsing._audio import get_transcription_response_format_type as _get_transcription_response_format_type from ...types.audio.transcription import Transcription -from ...types.audio_response_format import AudioResponseFormat from ...types.audio.transcription_include import TranscriptionInclude from ...types.audio.transcription_verbose import TranscriptionVerbose from ...types.audio.transcription_diarized import TranscriptionDiarized @@ -40,9 +41,16 @@ __all__ = ["Transcriptions", "AsyncTranscriptions"] + log: logging.Logger = logging.getLogger("openai.audio.transcriptions") +def _get_response_format_type( + response_format: AudioResponseFormat | Omit, +) -> type[Transcription | TranscriptionVerbose | TranscriptionDiarized | str]: + return openai.lib._parsing._audio.get_transcription_response_format_type(response_format, log=log) + + class Transcriptions(SyncAPIResource): """Turn audio into text or text into audio.""" @@ -81,8 +89,6 @@ def create( stream: Optional[Literal[False]] | Omit = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -106,7 +112,7 @@ def create( (which is powered by our open source Whisper V2 model), and `gpt-4o-transcribe-diarize`. - chunking_strategy: Controls how the audio is cut into chunks. When set to `"auto"`, the server + chunking_strategy: Controls how the audio is cut into chunks. When set to `\"auto\"`, the server first normalizes loudness and then uses voice activity detection (VAD) to choose boundaries. `server_vad` object can be provided to tweak VAD detection parameters manually. If unset, the audio is transcribed as a single block. @@ -178,8 +184,6 @@ def create( prompt: str | Omit = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -201,8 +205,6 @@ def create( prompt: str | Omit = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -224,8 +226,6 @@ def create( languages: SequenceNotStr[str] | Omit = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -250,8 +250,6 @@ def create( response_format: Union[AudioResponseFormat, Omit] = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -284,7 +282,7 @@ def create( Note: Streaming is not supported for the `whisper-1` model and will be ignored. - chunking_strategy: Controls how the audio is cut into chunks. When set to `"auto"`, the server + chunking_strategy: Controls how the audio is cut into chunks. When set to `\"auto\"`, the server first normalizes loudness and then uses voice activity detection (VAD) to choose boundaries. `server_vad` object can be provided to tweak VAD detection parameters manually. If unset, the audio is transcribed as a single block. @@ -352,7 +350,6 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - ... @overload def create( @@ -372,8 +369,6 @@ def create( response_format: Union[AudioResponseFormat, Omit] = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -406,7 +401,7 @@ def create( Note: Streaming is not supported for the `whisper-1` model and will be ignored. - chunking_strategy: Controls how the audio is cut into chunks. When set to `"auto"`, the server + chunking_strategy: Controls how the audio is cut into chunks. When set to `\"auto\"`, the server first normalizes loudness and then uses voice activity detection (VAD) to choose boundaries. `server_vad` object can be provided to tweak VAD detection parameters manually. If unset, the audio is transcribed as a single block. @@ -474,7 +469,6 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - ... @required_args(["file", "model"], ["file", "model", "stream"]) def create( @@ -494,8 +488,6 @@ def create( stream: Optional[Literal[False]] | Literal[True] | Omit = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -525,7 +517,7 @@ def create( # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return self._post( # type: ignore[return-value] + return self._post( "/audio/transcriptions", body=maybe_transform( body, @@ -587,8 +579,6 @@ async def create( stream: Optional[Literal[False]] | Omit = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -612,7 +602,7 @@ async def create( (which is powered by our open source Whisper V2 model), and `gpt-4o-transcribe-diarize`. - chunking_strategy: Controls how the audio is cut into chunks. When set to `"auto"`, the server + chunking_strategy: Controls how the audio is cut into chunks. When set to `\"auto\"`, the server first normalizes loudness and then uses voice activity detection (VAD) to choose boundaries. `server_vad` object can be provided to tweak VAD detection parameters manually. If unset, the audio is transcribed as a single block. @@ -701,8 +691,6 @@ async def create( prompt: str | Omit = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -724,8 +712,6 @@ async def create( prompt: str | Omit = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -750,8 +736,6 @@ async def create( response_format: Union[AudioResponseFormat, Omit] = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -784,7 +768,7 @@ async def create( Note: Streaming is not supported for the `whisper-1` model and will be ignored. - chunking_strategy: Controls how the audio is cut into chunks. When set to `"auto"`, the server + chunking_strategy: Controls how the audio is cut into chunks. When set to `\"auto\"`, the server first normalizes loudness and then uses voice activity detection (VAD) to choose boundaries. `server_vad` object can be provided to tweak VAD detection parameters manually. If unset, the audio is transcribed as a single block. @@ -852,7 +836,6 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - ... @overload async def create( @@ -872,8 +855,6 @@ async def create( response_format: Union[AudioResponseFormat, Omit] = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -906,7 +887,7 @@ async def create( Note: Streaming is not supported for the `whisper-1` model and will be ignored. - chunking_strategy: Controls how the audio is cut into chunks. When set to `"auto"`, the server + chunking_strategy: Controls how the audio is cut into chunks. When set to `\"auto\"`, the server first normalizes loudness and then uses voice activity detection (VAD) to choose boundaries. `server_vad` object can be provided to tweak VAD detection parameters manually. If unset, the audio is transcribed as a single block. @@ -974,7 +955,6 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - ... @required_args(["file", "model"], ["file", "model", "stream"]) async def create( @@ -994,8 +974,6 @@ async def create( stream: Optional[Literal[False]] | Literal[True] | Omit = omit, temperature: float | Omit = omit, timestamp_granularities: List[Literal["word", "segment"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1081,9 +1059,3 @@ def __init__(self, transcriptions: AsyncTranscriptions) -> None: self.create = async_to_streamed_response_wrapper( transcriptions.create, ) - - -def _get_response_format_type( - response_format: AudioResponseFormat | Omit, -) -> type[Transcription | TranscriptionVerbose | TranscriptionDiarized | str]: - return _get_transcription_response_format_type(response_format, log=log) diff --git a/src/openai/resources/audio/translations.py b/src/openai/resources/audio/translations.py index 492b02d7bc..f05c9bccd9 100644 --- a/src/openai/resources/audio/translations.py +++ b/src/openai/resources/audio/translations.py @@ -8,6 +8,8 @@ import httpx2 +import openai.lib._parsing._audio + from ... import _legacy_response from ..._files import deepcopy_with_paths from ..._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given @@ -18,16 +20,22 @@ from ...types.audio import translation_create_params from ..._base_client import make_request_options from ...types.audio_model import AudioModel -from ...lib._parsing._audio import get_translation_response_format_type as _get_translation_response_format_type from ...types.audio.translation import Translation from ...types.audio_response_format import AudioResponseFormat from ...types.audio.translation_verbose import TranslationVerbose __all__ = ["Translations", "AsyncTranslations"] + log: logging.Logger = logging.getLogger("openai.audio.transcriptions") +def _get_response_format_type( + response_format: AudioResponseFormat | Omit, +) -> type[Translation | TranslationVerbose | str]: + return openai.lib._parsing._audio.get_translation_response_format_type(response_format, log=log) + + class Translations(SyncAPIResource): """Turn audio into text or text into audio.""" @@ -59,8 +67,6 @@ def create( response_format: Union[Literal["json"], Omit] = omit, prompt: str | Omit = omit, temperature: float | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -76,8 +82,6 @@ def create( response_format: Literal["verbose_json"], prompt: str | Omit = omit, temperature: float | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -93,8 +97,6 @@ def create( response_format: Literal["text", "srt", "vtt"], prompt: str | Omit = omit, temperature: float | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -109,8 +111,6 @@ def create( prompt: str | Omit = omit, response_format: Union[Literal["json", "text", "srt", "verbose_json", "vtt"], Omit] = omit, temperature: float | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -165,7 +165,7 @@ def create( # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} - return self._post( # type: ignore[return-value] + return self._post( "/audio/translations", body=maybe_transform(body, translation_create_params.TranslationCreateParams), files=files, @@ -211,8 +211,6 @@ async def create( response_format: Union[Literal["json"], Omit] = omit, prompt: str | Omit = omit, temperature: float | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -228,8 +226,6 @@ async def create( response_format: Literal["verbose_json"], prompt: str | Omit = omit, temperature: float | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -245,8 +241,6 @@ async def create( response_format: Literal["text", "srt", "vtt"], prompt: str | Omit = omit, temperature: float | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -261,8 +255,6 @@ async def create( prompt: str | Omit = omit, response_format: Union[AudioResponseFormat, Omit] = omit, temperature: float | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -366,9 +358,3 @@ def __init__(self, translations: AsyncTranslations) -> None: self.create = async_to_streamed_response_wrapper( translations.create, ) - - -def _get_response_format_type( - response_format: AudioResponseFormat | Omit, -) -> type[Translation | TranslationVerbose | str]: - return _get_translation_response_format_type(response_format, log=log) diff --git a/src/openai/resources/beta/agents/environments/files.py b/src/openai/resources/beta/agents/environments/files.py index 962f0f7a1f..42d5a1c72b 100644 --- a/src/openai/resources/beta/agents/environments/files.py +++ b/src/openai/resources/beta/agents/environments/files.py @@ -61,7 +61,7 @@ def prepare( extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> PreparedAgentFiles: - "Beta: upload selected files for a new hosted environment." + """Beta: upload selected files for a new hosted environment.""" return prepare( self, files, @@ -79,7 +79,7 @@ def prepare_directory( extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> PreparedAgentFiles: - "Beta: prepare an explicitly selected directory snapshot." + """Beta: prepare an explicitly selected directory snapshot.""" return self.prepare( directory_files( root, @@ -103,7 +103,7 @@ def upload( extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> StagedAgentFile: - "Beta: upload and stage one file in an existing environment." + """Beta: upload and stage one file in an existing environment.""" return upload( self, environment_id, @@ -323,7 +323,7 @@ async def prepare( extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> PreparedAgentFiles: - "Beta: upload selected files for a new hosted environment." + """Beta: upload selected files for a new hosted environment.""" return await async_prepare( self, files, @@ -341,7 +341,7 @@ async def prepare_directory( extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> PreparedAgentFiles: - "Beta: prepare an explicitly selected directory snapshot." + """Beta: prepare an explicitly selected directory snapshot.""" return await self.prepare( await async_directory_files( root, @@ -365,7 +365,7 @@ async def upload( extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> StagedAgentFile: - "Beta: upload and stage one file in an existing environment." + """Beta: upload and stage one file in an existing environment.""" return await async_upload( self, environment_id, diff --git a/src/openai/resources/beta/agents/sessions/artifacts.py b/src/openai/resources/beta/agents/sessions/artifacts.py index cd4a7e4415..0234fa1aa5 100644 --- a/src/openai/resources/beta/agents/sessions/artifacts.py +++ b/src/openai/resources/beta/agents/sessions/artifacts.py @@ -55,7 +55,7 @@ def for_result( self, result: AgentTurnResult, ) -> AgentResultArtifacts: - "Beta: download artifacts scoped to the exact completed result turn." + """Beta: download artifacts scoped to the exact completed result turn.""" return AgentResultArtifacts( self, result, @@ -289,7 +289,7 @@ def for_result( self, result: AgentTurnResult, ) -> AsyncAgentResultArtifacts: - "Beta: download artifacts scoped to the exact completed result turn." + """Beta: download artifacts scoped to the exact completed result turn.""" return AsyncAgentResultArtifacts( self, result, diff --git a/src/openai/resources/beta/agents/sessions/sessions.py b/src/openai/resources/beta/agents/sessions/sessions.py index 9bf23b9a30..6a9658a501 100644 --- a/src/openai/resources/beta/agents/sessions/sessions.py +++ b/src/openai/resources/beta/agents/sessions/sessions.py @@ -66,12 +66,11 @@ ) from .....types.beta.agents import session_list_params, session_create_params, session_update_params from .....lib.streaming.agents import ToolHandler, AsyncToolHandler, AgentSessionStream, AsyncAgentSessionStream -from .....lib.beta.agents._output import bind_output_type, with_output_schema +from .....lib.beta.agents._create import prepare_session_create, prepare_async_session_create from .....lib.beta.agents._result import OutputT from .....types.beta.agent_session import AgentSession from .....lib.beta.agents._tool_error import ToolErrorHandler, AsyncToolErrorHandler from .....types.beta.environment_param import EnvironmentParam -from .....lib.streaming.agents._dispatch import ToolDispatcher, AsyncToolDispatcher from .....types.beta.agent_session_deleted import AgentSessionDeleted from .....types.beta.agent_session_input_message_param import AgentSessionInputMessageParam @@ -98,15 +97,14 @@ def stream( their results are submitted automatically. Use on_tool_error alongside tool_handlers to log or monitor local argument, handler, and output failures. The callback does not receive API or stream errors. Ordinary observer - exceptions are ignored. See AgentSessionStream for details. - """ + exceptions are ignored. See AgentSessionStream for details.""" return AgentSessionStream( self, session_id, input=input, + output_type=output_type, tool_handlers=tool_handlers, on_tool_error=on_tool_error, - output_type=output_type, idempotency_key=idempotency_key, extra_headers=extra_headers, timeout=timeout, @@ -169,15 +167,12 @@ def create( spend_control: Optional[session_create_params.SpendControl] | Omit = omit, stream: Literal[False] | Omit = omit, vault_ids: Optional[SequenceNotStr[str]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> AgentSession: - """ - Creates a managed agent session, optionally submits initial input, and returns + """Creates a managed agent session, optionally submits initial input, and returns the session or streams its events when stream is true. See [running sessions](https://developers.openai.com/api/docs/guides/agents-api/sessions). @@ -211,9 +206,7 @@ def create( extra_body: Add additional JSON properties to the request - timeout: Override the client-level default timeout for this request, in seconds - """ - ... + timeout: Override the client-level default timeout for this request, in seconds""" @overload def create( @@ -229,15 +222,12 @@ def create( metadata: Optional[Dict[str, str]] | Omit = omit, spend_control: Optional[session_create_params.SpendControl] | Omit = omit, vault_ids: Optional[SequenceNotStr[str]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> AgentSessionEventStream[OutputT]: - """ - Creates a managed agent session, optionally submits initial input, and returns + """Creates a managed agent session, optionally submits initial input, and returns the session or streams its events when stream is true. See [running sessions](https://developers.openai.com/api/docs/guides/agents-api/sessions). @@ -271,9 +261,7 @@ def create( extra_body: Add additional JSON properties to the request - timeout: Override the client-level default timeout for this request, in seconds - """ - ... + timeout: Override the client-level default timeout for this request, in seconds""" @overload def create( @@ -289,15 +277,12 @@ def create( metadata: Optional[Dict[str, str]] | Omit = omit, spend_control: Optional[session_create_params.SpendControl] | Omit = omit, vault_ids: Optional[SequenceNotStr[str]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> AgentSession | AgentSessionEventStream[OutputT]: - """ - Creates a managed agent session, optionally submits initial input, and returns + """Creates a managed agent session, optionally submits initial input, and returns the session or streams its events when stream is true. See [running sessions](https://developers.openai.com/api/docs/guides/agents-api/sessions). @@ -331,9 +316,7 @@ def create( extra_body: Add additional JSON properties to the request - timeout: Override the client-level default timeout for this request, in seconds - """ - ... + timeout: Override the client-level default timeout for this request, in seconds""" @required_args(["environment"], ["environment", "stream"]) def create( @@ -349,44 +332,45 @@ def create( spend_control: Optional[session_create_params.SpendControl] | Omit = omit, stream: Literal[False] | Literal[True] | Omit = omit, vault_ids: Optional[SequenceNotStr[str]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> AgentSession | AgentSessionEventStream[OutputT]: extra_headers = {"OpenAI-Beta": "agents=v1", **(extra_headers or {})} - agent = with_output_schema(agent, output_type) - if tool_handlers is not None and stream is not True: - raise ValueError("tool_handlers requires stream=True") - dispatcher = ( - ToolDispatcher(self, tool_handlers, extra_headers=extra_headers, timeout=timeout) if tool_handlers else None + _runtime_context = prepare_session_create( + self, + { + "stream": stream, + "environment": environment, + "agent": agent, + "agent_id": agent_id, + "input": input, + "metadata": metadata, + "spend_control": spend_control, + "vault_ids": vault_ids, + }, + output_type=output_type, + tool_handlers=tool_handlers, + stream=stream, + extra_headers=extra_headers, + timeout=timeout, ) return self._post( "/agents/sessions", body=maybe_transform( - { - "stream": stream, - "environment": environment, - "agent": agent, - "agent_id": agent_id, - "input": input, - "metadata": metadata, - "spend_control": spend_control, - "vault_ids": vault_ids, - }, + _runtime_context.body, session_create_params.SessionCreateParamsStreaming if stream else session_create_params.SessionCreateParamsNonStreaming, ), options=make_request_options( - post_parser=lambda response: bind_output_type(response, output_type, dispatcher), extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, security={"bearer_auth": True}, + post_parser=_runtime_context.post_parser, ), cast_to=AgentSession, stream=stream or False, @@ -620,15 +604,14 @@ def stream( their results are submitted automatically. Use on_tool_error alongside tool_handlers to log or monitor local argument, handler, and output failures. The callback may be async; it does not receive API or stream errors. - Ordinary observer exceptions are ignored. See AsyncAgentSessionStream for details. - """ + Ordinary observer exceptions are ignored. See AsyncAgentSessionStream for details.""" return AsyncAgentSessionStream( self, session_id, input=input, + output_type=output_type, tool_handlers=tool_handlers, on_tool_error=on_tool_error, - output_type=output_type, idempotency_key=idempotency_key, extra_headers=extra_headers, timeout=timeout, @@ -691,15 +674,12 @@ async def create( spend_control: Optional[session_create_params.SpendControl] | Omit = omit, stream: Literal[False] | Omit = omit, vault_ids: Optional[SequenceNotStr[str]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> AgentSession: - """ - Creates a managed agent session, optionally submits initial input, and returns + """Creates a managed agent session, optionally submits initial input, and returns the session or streams its events when stream is true. See [running sessions](https://developers.openai.com/api/docs/guides/agents-api/sessions). @@ -733,9 +713,7 @@ async def create( extra_body: Add additional JSON properties to the request - timeout: Override the client-level default timeout for this request, in seconds - """ - ... + timeout: Override the client-level default timeout for this request, in seconds""" @overload async def create( @@ -751,15 +729,12 @@ async def create( metadata: Optional[Dict[str, str]] | Omit = omit, spend_control: Optional[session_create_params.SpendControl] | Omit = omit, vault_ids: Optional[SequenceNotStr[str]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> AsyncAgentSessionEventStream[OutputT]: - """ - Creates a managed agent session, optionally submits initial input, and returns + """Creates a managed agent session, optionally submits initial input, and returns the session or streams its events when stream is true. See [running sessions](https://developers.openai.com/api/docs/guides/agents-api/sessions). @@ -793,9 +768,7 @@ async def create( extra_body: Add additional JSON properties to the request - timeout: Override the client-level default timeout for this request, in seconds - """ - ... + timeout: Override the client-level default timeout for this request, in seconds""" @overload async def create( @@ -811,15 +784,12 @@ async def create( metadata: Optional[Dict[str, str]] | Omit = omit, spend_control: Optional[session_create_params.SpendControl] | Omit = omit, vault_ids: Optional[SequenceNotStr[str]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> AgentSession | AsyncAgentSessionEventStream[OutputT]: - """ - Creates a managed agent session, optionally submits initial input, and returns + """Creates a managed agent session, optionally submits initial input, and returns the session or streams its events when stream is true. See [running sessions](https://developers.openai.com/api/docs/guides/agents-api/sessions). @@ -853,9 +823,7 @@ async def create( extra_body: Add additional JSON properties to the request - timeout: Override the client-level default timeout for this request, in seconds - """ - ... + timeout: Override the client-level default timeout for this request, in seconds""" @required_args(["environment"], ["environment", "stream"]) async def create( @@ -871,46 +839,45 @@ async def create( spend_control: Optional[session_create_params.SpendControl] | Omit = omit, stream: Literal[False] | Literal[True] | Omit = omit, vault_ids: Optional[SequenceNotStr[str]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = not_given, ) -> AgentSession | AsyncAgentSessionEventStream[OutputT]: extra_headers = {"OpenAI-Beta": "agents=v1", **(extra_headers or {})} - agent = with_output_schema(agent, output_type) - if tool_handlers is not None and stream is not True: - raise ValueError("tool_handlers requires stream=True") - dispatcher = ( - AsyncToolDispatcher(self, tool_handlers, extra_headers=extra_headers, timeout=timeout) - if tool_handlers - else None + _runtime_context = prepare_async_session_create( + self, + { + "stream": stream, + "environment": environment, + "agent": agent, + "agent_id": agent_id, + "input": input, + "metadata": metadata, + "spend_control": spend_control, + "vault_ids": vault_ids, + }, + output_type=output_type, + tool_handlers=tool_handlers, + stream=stream, + extra_headers=extra_headers, + timeout=timeout, ) return await self._post( "/agents/sessions", body=await async_maybe_transform( - { - "stream": stream, - "environment": environment, - "agent": agent, - "agent_id": agent_id, - "input": input, - "metadata": metadata, - "spend_control": spend_control, - "vault_ids": vault_ids, - }, + _runtime_context.body, session_create_params.SessionCreateParamsStreaming if stream else session_create_params.SessionCreateParamsNonStreaming, ), options=make_request_options( - post_parser=lambda response: bind_output_type(response, output_type, dispatcher), extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, security={"bearer_auth": True}, + post_parser=_runtime_context.post_parser, ), cast_to=AgentSession, stream=stream or False, diff --git a/src/openai/resources/beta/threads/runs/runs.py b/src/openai/resources/beta/threads/runs/runs.py index 0d0253d520..93deafc3f9 100644 --- a/src/openai/resources/beta/threads/runs/runs.py +++ b/src/openai/resources/beta/threads/runs/runs.py @@ -4,7 +4,6 @@ import typing_extensions from typing import List, Union, Iterable, Optional -from functools import partial from typing_extensions import Literal, overload import httpx2 @@ -19,13 +18,7 @@ AsyncStepsWithStreamingResponse, ) from ....._types import NOT_GIVEN, Body, Omit, Query, Headers, NotGiven, omit, not_given -from ....._utils import ( - is_given, - path_template, - required_args, - maybe_transform, - async_maybe_transform, -) +from ....._utils import path_template, required_args, maybe_transform, async_maybe_transform from ....._compat import cached_property from ....._resource import SyncAPIResource, AsyncAPIResource from ....._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper @@ -46,6 +39,20 @@ run_update_params, run_submit_tool_outputs_params, ) +from .....lib._assistants_runs import ( + runs_poll, + runs_stream, + async_runs_poll, + async_runs_stream, + runs_create_and_poll, + runs_create_and_stream, + async_runs_create_and_poll, + async_runs_create_and_stream, + runs_submit_tool_outputs_stream, + runs_submit_tool_outputs_and_poll, + async_runs_submit_tool_outputs_stream, + async_runs_submit_tool_outputs_and_poll, +) from .....types.beta.threads.run import Run from .....types.shared.chat_model import ChatModel from .....types.shared_params.metadata import Metadata @@ -841,20 +848,16 @@ def create_and_poll( truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, poll_interval_ms: int | Omit = omit, thread_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> Run: - """ - A helper to create a run an poll for a terminal state. More information on Run + """A helper to create a run an poll for a terminal state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - run = self.create( # pyright: ignore[reportDeprecated] - thread_id=thread_id, + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" + return runs_create_and_poll( + self, assistant_id=assistant_id, include=include, additional_instructions=additional_instructions, @@ -864,28 +867,19 @@ def create_and_poll( max_prompt_tokens=max_prompt_tokens, metadata=metadata, model=model, + parallel_tool_calls=parallel_tool_calls, + reasoning_effort=reasoning_effort, response_format=response_format, temperature=temperature, tool_choice=tool_choice, - parallel_tool_calls=parallel_tool_calls, - reasoning_effort=reasoning_effort, - # We assume we are not streaming when polling - stream=False, tools=tools, - truncation_strategy=truncation_strategy, top_p=top_p, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - return self.poll( # pyright: ignore[reportDeprecated] - run.id, + truncation_strategy=truncation_strategy, + poll_interval_ms=poll_interval_ms, thread_id=thread_id, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, - poll_interval_ms=poll_interval_ms, timeout=timeout, ) @@ -911,15 +905,12 @@ def create_and_stream( top_p: Optional[float] | Omit = omit, truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, thread_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AssistantStreamManager[AssistantEventHandler]: """Create a Run stream""" - ... @overload @typing_extensions.deprecated("use `stream` instead") @@ -944,15 +935,12 @@ def create_and_stream( truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, thread_id: str, event_handler: AssistantEventHandlerT, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AssistantStreamManager[AssistantEventHandlerT]: """Create a Run stream""" - ... @typing_extensions.deprecated("use `stream` instead") def create_and_stream( @@ -976,60 +964,37 @@ def create_and_stream( truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, thread_id: str, event_handler: AssistantEventHandlerT | None = None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT]: """Create a Run stream""" - if not thread_id: - raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") - - extra_headers = { - "OpenAI-Beta": "assistants=v2", - "X-Stainless-Stream-Helper": "threads.runs.create_and_stream", - "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", - **(extra_headers or {}), - } - make_request = partial( - self._post, - path_template("/threads/{thread_id}/runs", thread_id=thread_id), - body=maybe_transform( - { - "assistant_id": assistant_id, - "additional_instructions": additional_instructions, - "additional_messages": additional_messages, - "instructions": instructions, - "max_completion_tokens": max_completion_tokens, - "max_prompt_tokens": max_prompt_tokens, - "metadata": metadata, - "model": model, - "response_format": response_format, - "temperature": temperature, - "tool_choice": tool_choice, - "stream": True, - "tools": tools, - "truncation_strategy": truncation_strategy, - "parallel_tool_calls": parallel_tool_calls, - "reasoning_effort": reasoning_effort, - "top_p": top_p, - }, - run_create_params.RunCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"bearer_auth": True}, - ), - cast_to=Run, - stream=True, - stream_cls=Stream[AssistantStreamEvent], + return runs_create_and_stream( + self, + assistant_id=assistant_id, + additional_instructions=additional_instructions, + additional_messages=additional_messages, + instructions=instructions, + max_completion_tokens=max_completion_tokens, + max_prompt_tokens=max_prompt_tokens, + metadata=metadata, + model=model, + parallel_tool_calls=parallel_tool_calls, + reasoning_effort=reasoning_effort, + response_format=response_format, + temperature=temperature, + tool_choice=tool_choice, + tools=tools, + top_p=top_p, + truncation_strategy=truncation_strategy, + thread_id=thread_id, + event_handler=event_handler, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) - return AssistantStreamManager(make_request, event_handler=event_handler or AssistantEventHandler()) @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") def poll( @@ -1042,40 +1007,19 @@ def poll( timeout: float | httpx2.Timeout | None | NotGiven = not_given, poll_interval_ms: int | Omit = omit, ) -> Run: - """ - A helper to poll a run status until it reaches a terminal state. More + """A helper to poll a run status until it reaches a terminal state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - extra_headers = {"X-Stainless-Poll-Helper": "true", **(extra_headers or {})} - - if is_given(poll_interval_ms): - extra_headers["X-Stainless-Custom-Poll-Interval"] = str(poll_interval_ms) - - terminal_states = {"requires_action", "cancelled", "completed", "failed", "expired", "incomplete"} - while True: - response = self.with_raw_response.retrieve( # pyright: ignore[reportDeprecated] - thread_id=thread_id, - run_id=run_id, - extra_headers=extra_headers, - extra_body=extra_body, - extra_query=extra_query, - timeout=timeout, - ) - - run = response.parse() - # Return if we reached a terminal state - if run.status in terminal_states: - return run - - if not is_given(poll_interval_ms): - from_header = response.headers.get("openai-poll-after-ms") - if from_header is not None: - poll_interval_ms = int(from_header) - else: - poll_interval_ms = 1000 - - self._sleep(poll_interval_ms / 1000) + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" + return runs_poll( + self, + run_id=run_id, + thread_id=thread_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + poll_interval_ms=poll_interval_ms, + ) @overload @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") @@ -1100,15 +1044,12 @@ def stream( top_p: Optional[float] | Omit = omit, truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, thread_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AssistantStreamManager[AssistantEventHandler]: """Create a Run stream""" - ... @overload @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") @@ -1134,15 +1075,12 @@ def stream( truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, thread_id: str, event_handler: AssistantEventHandlerT, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AssistantStreamManager[AssistantEventHandlerT]: """Create a Run stream""" - ... @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") def stream( @@ -1167,61 +1105,38 @@ def stream( truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, thread_id: str, event_handler: AssistantEventHandlerT | None = None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT]: """Create a Run stream""" - if not thread_id: - raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") - - extra_headers = { - "OpenAI-Beta": "assistants=v2", - "X-Stainless-Stream-Helper": "threads.runs.create_and_stream", - "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", - **(extra_headers or {}), - } - make_request = partial( - self._post, - path_template("/threads/{thread_id}/runs", thread_id=thread_id), - body=maybe_transform( - { - "assistant_id": assistant_id, - "additional_instructions": additional_instructions, - "additional_messages": additional_messages, - "instructions": instructions, - "max_completion_tokens": max_completion_tokens, - "max_prompt_tokens": max_prompt_tokens, - "metadata": metadata, - "model": model, - "response_format": response_format, - "temperature": temperature, - "tool_choice": tool_choice, - "stream": True, - "tools": tools, - "parallel_tool_calls": parallel_tool_calls, - "reasoning_effort": reasoning_effort, - "truncation_strategy": truncation_strategy, - "top_p": top_p, - }, - run_create_params.RunCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform({"include": include}, run_create_params.RunCreateParams), - security={"bearer_auth": True}, - ), - cast_to=Run, - stream=True, - stream_cls=Stream[AssistantStreamEvent], + return runs_stream( + self, + assistant_id=assistant_id, + include=include, + additional_instructions=additional_instructions, + additional_messages=additional_messages, + instructions=instructions, + max_completion_tokens=max_completion_tokens, + max_prompt_tokens=max_prompt_tokens, + metadata=metadata, + model=model, + parallel_tool_calls=parallel_tool_calls, + reasoning_effort=reasoning_effort, + response_format=response_format, + temperature=temperature, + tool_choice=tool_choice, + tools=tools, + top_p=top_p, + truncation_strategy=truncation_strategy, + thread_id=thread_id, + event_handler=event_handler, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) - return AssistantStreamManager(make_request, event_handler=event_handler or AssistantEventHandler()) @overload @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") @@ -1394,36 +1309,24 @@ def submit_tool_outputs_and_poll( run_id: str, thread_id: str, poll_interval_ms: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> Run: - """ - A helper to submit a tool output to a run and poll for a terminal run state. + """A helper to submit a tool output to a run and poll for a terminal run state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - run = self.submit_tool_outputs( # pyright: ignore[reportDeprecated] - run_id=run_id, - thread_id=thread_id, + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" + return runs_submit_tool_outputs_and_poll( + self, tool_outputs=tool_outputs, - stream=False, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - return self.poll( # pyright: ignore[reportDeprecated] - run_id=run.id, + run_id=run_id, thread_id=thread_id, + poll_interval_ms=poll_interval_ms, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - poll_interval_ms=poll_interval_ms, ) @overload @@ -1434,19 +1337,14 @@ def submit_tool_outputs_stream( tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], run_id: str, thread_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AssistantStreamManager[AssistantEventHandler]: - """ - Submit the tool outputs from a previous run and stream the run to a terminal + """Submit the tool outputs from a previous run and stream the run to a terminal state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - ... + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" @overload @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") @@ -1457,19 +1355,14 @@ def submit_tool_outputs_stream( run_id: str, thread_id: str, event_handler: AssistantEventHandlerT, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AssistantStreamManager[AssistantEventHandlerT]: - """ - Submit the tool outputs from a previous run and stream the run to a terminal + """Submit the tool outputs from a previous run and stream the run to a terminal state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - ... + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") def submit_tool_outputs_stream( @@ -1479,52 +1372,25 @@ def submit_tool_outputs_stream( run_id: str, thread_id: str, event_handler: AssistantEventHandlerT | None = None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT]: - """ - Submit the tool outputs from a previous run and stream the run to a terminal + """Submit the tool outputs from a previous run and stream the run to a terminal state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - if not run_id: - raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") - - if not thread_id: - raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") - - extra_headers = { - "OpenAI-Beta": "assistants=v2", - "X-Stainless-Stream-Helper": "threads.runs.submit_tool_outputs_stream", - "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", - **(extra_headers or {}), - } - request = partial( - self._post, - path_template("/threads/{thread_id}/runs/{run_id}/submit_tool_outputs", thread_id=thread_id, run_id=run_id), - body=maybe_transform( - { - "tool_outputs": tool_outputs, - "stream": True, - }, - run_submit_tool_outputs_params.RunSubmitToolOutputsParams, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"bearer_auth": True}, - ), - cast_to=Run, - stream=True, - stream_cls=Stream[AssistantStreamEvent], + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" + return runs_submit_tool_outputs_stream( + self, + tool_outputs=tool_outputs, + run_id=run_id, + thread_id=thread_id, + event_handler=event_handler, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) - return AssistantStreamManager(request, event_handler=event_handler or AssistantEventHandler()) class AsyncRuns(AsyncAPIResource): @@ -2310,20 +2176,16 @@ async def create_and_poll( truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, poll_interval_ms: int | Omit = omit, thread_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> Run: - """ - A helper to create a run an poll for a terminal state. More information on Run + """A helper to create a run an poll for a terminal state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - run = await self.create( # pyright: ignore[reportDeprecated] - thread_id=thread_id, + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" + return await async_runs_create_and_poll( + self, assistant_id=assistant_id, include=include, additional_instructions=additional_instructions, @@ -2333,28 +2195,19 @@ async def create_and_poll( max_prompt_tokens=max_prompt_tokens, metadata=metadata, model=model, + parallel_tool_calls=parallel_tool_calls, + reasoning_effort=reasoning_effort, response_format=response_format, temperature=temperature, tool_choice=tool_choice, - parallel_tool_calls=parallel_tool_calls, - reasoning_effort=reasoning_effort, - # We assume we are not streaming when polling - stream=False, tools=tools, - truncation_strategy=truncation_strategy, top_p=top_p, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - return await self.poll( # pyright: ignore[reportDeprecated] - run.id, + truncation_strategy=truncation_strategy, + poll_interval_ms=poll_interval_ms, thread_id=thread_id, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, - poll_interval_ms=poll_interval_ms, timeout=timeout, ) @@ -2379,15 +2232,12 @@ def create_and_stream( top_p: Optional[float] | Omit = omit, truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, thread_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandler]: """Create a Run stream""" - ... @overload @typing_extensions.deprecated("use `stream` instead") @@ -2411,15 +2261,12 @@ def create_and_stream( truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, thread_id: str, event_handler: AsyncAssistantEventHandlerT, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandlerT]: """Create a Run stream""" - ... @typing_extensions.deprecated("use `stream` instead") def create_and_stream( @@ -2442,8 +2289,6 @@ def create_and_stream( truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, thread_id: str, event_handler: AsyncAssistantEventHandlerT | None = None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -2453,50 +2298,30 @@ def create_and_stream( | AsyncAssistantStreamManager[AsyncAssistantEventHandlerT] ): """Create a Run stream""" - if not thread_id: - raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") - - extra_headers = { - "OpenAI-Beta": "assistants=v2", - "X-Stainless-Stream-Helper": "threads.runs.create_and_stream", - "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", - **(extra_headers or {}), - } - request = self._post( - path_template("/threads/{thread_id}/runs", thread_id=thread_id), - body=maybe_transform( - { - "assistant_id": assistant_id, - "additional_instructions": additional_instructions, - "additional_messages": additional_messages, - "instructions": instructions, - "max_completion_tokens": max_completion_tokens, - "max_prompt_tokens": max_prompt_tokens, - "metadata": metadata, - "model": model, - "response_format": response_format, - "temperature": temperature, - "tool_choice": tool_choice, - "stream": True, - "tools": tools, - "truncation_strategy": truncation_strategy, - "top_p": top_p, - "parallel_tool_calls": parallel_tool_calls, - }, - run_create_params.RunCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"bearer_auth": True}, - ), - cast_to=Run, - stream=True, - stream_cls=AsyncStream[AssistantStreamEvent], + return async_runs_create_and_stream( + self, + assistant_id=assistant_id, + additional_instructions=additional_instructions, + additional_messages=additional_messages, + instructions=instructions, + max_completion_tokens=max_completion_tokens, + max_prompt_tokens=max_prompt_tokens, + metadata=metadata, + model=model, + parallel_tool_calls=parallel_tool_calls, + response_format=response_format, + temperature=temperature, + tool_choice=tool_choice, + tools=tools, + top_p=top_p, + truncation_strategy=truncation_strategy, + thread_id=thread_id, + event_handler=event_handler, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) - return AsyncAssistantStreamManager(request, event_handler=event_handler or AsyncAssistantEventHandler()) @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") async def poll( @@ -2509,40 +2334,19 @@ async def poll( timeout: float | httpx2.Timeout | None | NotGiven = not_given, poll_interval_ms: int | Omit = omit, ) -> Run: - """ - A helper to poll a run status until it reaches a terminal state. More + """A helper to poll a run status until it reaches a terminal state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - extra_headers = {"X-Stainless-Poll-Helper": "true", **(extra_headers or {})} - - if is_given(poll_interval_ms): - extra_headers["X-Stainless-Custom-Poll-Interval"] = str(poll_interval_ms) - - terminal_states = {"requires_action", "cancelled", "completed", "failed", "expired", "incomplete"} - while True: - response = await self.with_raw_response.retrieve( # pyright: ignore[reportDeprecated] - thread_id=thread_id, - run_id=run_id, - extra_headers=extra_headers, - extra_body=extra_body, - extra_query=extra_query, - timeout=timeout, - ) - - run = response.parse() - # Return if we reached a terminal state - if run.status in terminal_states: - return run - - if not is_given(poll_interval_ms): - from_header = response.headers.get("openai-poll-after-ms") - if from_header is not None: - poll_interval_ms = int(from_header) - else: - poll_interval_ms = 1000 - - await self._sleep(poll_interval_ms / 1000) + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" + return await async_runs_poll( + self, + run_id=run_id, + thread_id=thread_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + poll_interval_ms=poll_interval_ms, + ) @overload @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") @@ -2566,15 +2370,12 @@ def stream( top_p: Optional[float] | Omit = omit, truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, thread_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandler]: """Create a Run stream""" - ... @overload @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") @@ -2600,15 +2401,12 @@ def stream( truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, thread_id: str, event_handler: AsyncAssistantEventHandlerT, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandlerT]: """Create a Run stream""" - ... @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") def stream( @@ -2633,8 +2431,6 @@ def stream( truncation_strategy: Optional[run_create_params.TruncationStrategy] | Omit = omit, thread_id: str, event_handler: AsyncAssistantEventHandlerT | None = None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -2644,52 +2440,32 @@ def stream( | AsyncAssistantStreamManager[AsyncAssistantEventHandlerT] ): """Create a Run stream""" - if not thread_id: - raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") - - extra_headers = { - "OpenAI-Beta": "assistants=v2", - "X-Stainless-Stream-Helper": "threads.runs.create_and_stream", - "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", - **(extra_headers or {}), - } - request = self._post( - path_template("/threads/{thread_id}/runs", thread_id=thread_id), - body=maybe_transform( - { - "assistant_id": assistant_id, - "additional_instructions": additional_instructions, - "additional_messages": additional_messages, - "instructions": instructions, - "max_completion_tokens": max_completion_tokens, - "max_prompt_tokens": max_prompt_tokens, - "metadata": metadata, - "model": model, - "response_format": response_format, - "temperature": temperature, - "tool_choice": tool_choice, - "stream": True, - "tools": tools, - "parallel_tool_calls": parallel_tool_calls, - "reasoning_effort": reasoning_effort, - "truncation_strategy": truncation_strategy, - "top_p": top_p, - }, - run_create_params.RunCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform({"include": include}, run_create_params.RunCreateParams), - security={"bearer_auth": True}, - ), - cast_to=Run, - stream=True, - stream_cls=AsyncStream[AssistantStreamEvent], + return async_runs_stream( + self, + assistant_id=assistant_id, + include=include, + additional_instructions=additional_instructions, + additional_messages=additional_messages, + instructions=instructions, + max_completion_tokens=max_completion_tokens, + max_prompt_tokens=max_prompt_tokens, + metadata=metadata, + model=model, + parallel_tool_calls=parallel_tool_calls, + reasoning_effort=reasoning_effort, + response_format=response_format, + temperature=temperature, + tool_choice=tool_choice, + tools=tools, + top_p=top_p, + truncation_strategy=truncation_strategy, + thread_id=thread_id, + event_handler=event_handler, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) - return AsyncAssistantStreamManager(request, event_handler=event_handler or AsyncAssistantEventHandler()) @overload @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") @@ -2862,36 +2638,24 @@ async def submit_tool_outputs_and_poll( run_id: str, thread_id: str, poll_interval_ms: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> Run: - """ - A helper to submit a tool output to a run and poll for a terminal run state. + """A helper to submit a tool output to a run and poll for a terminal run state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - run = await self.submit_tool_outputs( # pyright: ignore[reportDeprecated] - run_id=run_id, - thread_id=thread_id, + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" + return await async_runs_submit_tool_outputs_and_poll( + self, tool_outputs=tool_outputs, - stream=False, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - return await self.poll( # pyright: ignore[reportDeprecated] - run_id=run.id, + run_id=run_id, thread_id=thread_id, + poll_interval_ms=poll_interval_ms, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - poll_interval_ms=poll_interval_ms, ) @overload @@ -2902,19 +2666,14 @@ def submit_tool_outputs_stream( tool_outputs: Iterable[run_submit_tool_outputs_params.ToolOutput], run_id: str, thread_id: str, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandler]: - """ - Submit the tool outputs from a previous run and stream the run to a terminal + """Submit the tool outputs from a previous run and stream the run to a terminal state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - ... + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" @overload @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") @@ -2925,19 +2684,14 @@ def submit_tool_outputs_stream( run_id: str, thread_id: str, event_handler: AsyncAssistantEventHandlerT, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandlerT]: - """ - Submit the tool outputs from a previous run and stream the run to a terminal + """Submit the tool outputs from a previous run and stream the run to a terminal state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - ... + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" @typing_extensions.deprecated("The Assistants API is deprecated in favor of the Responses API") def submit_tool_outputs_stream( @@ -2947,8 +2701,6 @@ def submit_tool_outputs_stream( run_id: str, thread_id: str, event_handler: AsyncAssistantEventHandlerT | None = None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -2957,44 +2709,20 @@ def submit_tool_outputs_stream( AsyncAssistantStreamManager[AsyncAssistantEventHandler] | AsyncAssistantStreamManager[AsyncAssistantEventHandlerT] ): - """ - Submit the tool outputs from a previous run and stream the run to a terminal + """Submit the tool outputs from a previous run and stream the run to a terminal state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - if not run_id: - raise ValueError(f"Expected a non-empty value for `run_id` but received {run_id!r}") - - if not thread_id: - raise ValueError(f"Expected a non-empty value for `thread_id` but received {thread_id!r}") - - extra_headers = { - "OpenAI-Beta": "assistants=v2", - "X-Stainless-Stream-Helper": "threads.runs.submit_tool_outputs_stream", - "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", - **(extra_headers or {}), - } - request = self._post( - path_template("/threads/{thread_id}/runs/{run_id}/submit_tool_outputs", thread_id=thread_id, run_id=run_id), - body=maybe_transform( - { - "tool_outputs": tool_outputs, - "stream": True, - }, - run_submit_tool_outputs_params.RunSubmitToolOutputsParams, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"bearer_auth": True}, - ), - cast_to=Run, - stream=True, - stream_cls=AsyncStream[AssistantStreamEvent], + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" + return async_runs_submit_tool_outputs_stream( + self, + tool_outputs=tool_outputs, + run_id=run_id, + thread_id=thread_id, + event_handler=event_handler, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) - return AsyncAssistantStreamManager(request, event_handler=event_handler or AsyncAssistantEventHandler()) class RunsWithRawResponse: diff --git a/src/openai/resources/beta/threads/threads.py b/src/openai/resources/beta/threads/threads.py index 66e1467b2c..d2c1ca7fd8 100644 --- a/src/openai/resources/beta/threads/threads.py +++ b/src/openai/resources/beta/threads/threads.py @@ -4,7 +4,6 @@ import typing_extensions from typing import Union, Iterable, Optional -from functools import partial from typing_extensions import Literal, overload import httpx2 @@ -48,6 +47,12 @@ ) from ....types.beta.thread import Thread from ....types.beta.threads.run import Run +from ....lib._assistants_threads import ( + threads_create_and_run_poll, + threads_create_and_run_stream, + async_threads_create_and_run_poll, + async_threads_create_and_run_stream, +) from ....types.shared.chat_model import ChatModel from ....types.beta.thread_deleted import ThreadDeleted from ....types.shared_params.metadata import Metadata @@ -784,19 +789,16 @@ def create_and_run_poll( top_p: Optional[float] | Omit = omit, truncation_strategy: Optional[thread_create_and_run_params.TruncationStrategy] | Omit = omit, poll_interval_ms: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> Run: - """ - A helper to create a thread, start a run and then poll for a terminal state. + """A helper to create a thread, start a run and then poll for a terminal state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - run = self.create_and_run( # pyright: ignore[reportDeprecated] + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" + return threads_create_and_run_poll( + self, assistant_id=assistant_id, instructions=instructions, max_completion_tokens=max_completion_tokens, @@ -806,19 +808,18 @@ def create_and_run_poll( parallel_tool_calls=parallel_tool_calls, response_format=response_format, temperature=temperature, - stream=False, thread=thread, - tool_resources=tool_resources, tool_choice=tool_choice, - truncation_strategy=truncation_strategy, - top_p=top_p, + tool_resources=tool_resources, tools=tools, + top_p=top_p, + truncation_strategy=truncation_strategy, + poll_interval_ms=poll_interval_ms, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, ) - return self.runs.poll(run.id, run.thread_id, extra_headers, extra_query, extra_body, timeout, poll_interval_ms) # pyright: ignore[reportDeprecated] @overload def create_and_run_stream( @@ -839,15 +840,12 @@ def create_and_run_stream( tools: Optional[Iterable[AssistantToolParam]] | Omit = omit, top_p: Optional[float] | Omit = omit, truncation_strategy: Optional[thread_create_and_run_params.TruncationStrategy] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AssistantStreamManager[AssistantEventHandler]: """Create a thread and stream the run back""" - ... @overload def create_and_run_stream( @@ -869,15 +867,12 @@ def create_and_run_stream( top_p: Optional[float] | Omit = omit, truncation_strategy: Optional[thread_create_and_run_params.TruncationStrategy] | Omit = omit, event_handler: AssistantEventHandlerT, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AssistantStreamManager[AssistantEventHandlerT]: """Create a thread and stream the run back""" - ... def create_and_run_stream( self, @@ -898,56 +893,35 @@ def create_and_run_stream( top_p: Optional[float] | Omit = omit, truncation_strategy: Optional[thread_create_and_run_params.TruncationStrategy] | Omit = omit, event_handler: AssistantEventHandlerT | None = None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AssistantStreamManager[AssistantEventHandler] | AssistantStreamManager[AssistantEventHandlerT]: """Create a thread and stream the run back""" - extra_headers = { - "OpenAI-Beta": "assistants=v2", - "X-Stainless-Stream-Helper": "threads.create_and_run_stream", - "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", - **(extra_headers or {}), - } - make_request = partial( - self._post, - "/threads/runs", - body=maybe_transform( - { - "assistant_id": assistant_id, - "instructions": instructions, - "max_completion_tokens": max_completion_tokens, - "max_prompt_tokens": max_prompt_tokens, - "metadata": metadata, - "model": model, - "parallel_tool_calls": parallel_tool_calls, - "response_format": response_format, - "temperature": temperature, - "tool_choice": tool_choice, - "stream": True, - "thread": thread, - "tools": tools, - "tool_resources": tool_resources, - "truncation_strategy": truncation_strategy, - "top_p": top_p, - }, - thread_create_and_run_params.ThreadCreateAndRunParams, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"bearer_auth": True}, - ), - cast_to=Run, - stream=True, - stream_cls=Stream[AssistantStreamEvent], + return threads_create_and_run_stream( + self, + assistant_id=assistant_id, + instructions=instructions, + max_completion_tokens=max_completion_tokens, + max_prompt_tokens=max_prompt_tokens, + metadata=metadata, + model=model, + parallel_tool_calls=parallel_tool_calls, + response_format=response_format, + temperature=temperature, + thread=thread, + tool_choice=tool_choice, + tool_resources=tool_resources, + tools=tools, + top_p=top_p, + truncation_strategy=truncation_strategy, + event_handler=event_handler, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) - return AssistantStreamManager(make_request, event_handler=event_handler or AssistantEventHandler()) class AsyncThreads(AsyncAPIResource): @@ -1675,19 +1649,16 @@ async def create_and_run_poll( top_p: Optional[float] | Omit = omit, truncation_strategy: Optional[thread_create_and_run_params.TruncationStrategy] | Omit = omit, poll_interval_ms: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> Run: - """ - A helper to create a thread, start a run and then poll for a terminal state. + """A helper to create a thread, start a run and then poll for a terminal state. More information on Run lifecycles can be found here: - https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps - """ - run = await self.create_and_run( # pyright: ignore[reportDeprecated] + https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps""" + return await async_threads_create_and_run_poll( + self, assistant_id=assistant_id, instructions=instructions, max_completion_tokens=max_completion_tokens, @@ -1697,21 +1668,18 @@ async def create_and_run_poll( parallel_tool_calls=parallel_tool_calls, response_format=response_format, temperature=temperature, - stream=False, thread=thread, - tool_resources=tool_resources, tool_choice=tool_choice, - truncation_strategy=truncation_strategy, - top_p=top_p, + tool_resources=tool_resources, tools=tools, + top_p=top_p, + truncation_strategy=truncation_strategy, + poll_interval_ms=poll_interval_ms, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, ) - return await self.runs.poll( # pyright: ignore[reportDeprecated] - run.id, run.thread_id, extra_headers, extra_query, extra_body, timeout, poll_interval_ms - ) @overload def create_and_run_stream( @@ -1732,15 +1700,12 @@ def create_and_run_stream( tools: Optional[Iterable[AssistantToolParam]] | Omit = omit, top_p: Optional[float] | Omit = omit, truncation_strategy: Optional[thread_create_and_run_params.TruncationStrategy] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandler]: """Create a thread and stream the run back""" - ... @overload def create_and_run_stream( @@ -1762,15 +1727,12 @@ def create_and_run_stream( top_p: Optional[float] | Omit = omit, truncation_strategy: Optional[thread_create_and_run_params.TruncationStrategy] | Omit = omit, event_handler: AsyncAssistantEventHandlerT, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncAssistantStreamManager[AsyncAssistantEventHandlerT]: """Create a thread and stream the run back""" - ... def create_and_run_stream( self, @@ -1791,8 +1753,6 @@ def create_and_run_stream( top_p: Optional[float] | Omit = omit, truncation_strategy: Optional[thread_create_and_run_params.TruncationStrategy] | Omit = omit, event_handler: AsyncAssistantEventHandlerT | None = None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1802,47 +1762,29 @@ def create_and_run_stream( | AsyncAssistantStreamManager[AsyncAssistantEventHandlerT] ): """Create a thread and stream the run back""" - extra_headers = { - "OpenAI-Beta": "assistants=v2", - "X-Stainless-Stream-Helper": "threads.create_and_run_stream", - "X-Stainless-Custom-Event-Handler": "true" if event_handler else "false", - **(extra_headers or {}), - } - request = self._post( - "/threads/runs", - body=maybe_transform( - { - "assistant_id": assistant_id, - "instructions": instructions, - "max_completion_tokens": max_completion_tokens, - "max_prompt_tokens": max_prompt_tokens, - "metadata": metadata, - "model": model, - "parallel_tool_calls": parallel_tool_calls, - "response_format": response_format, - "temperature": temperature, - "tool_choice": tool_choice, - "stream": True, - "thread": thread, - "tools": tools, - "tool_resources": tool_resources, - "truncation_strategy": truncation_strategy, - "top_p": top_p, - }, - thread_create_and_run_params.ThreadCreateAndRunParams, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"bearer_auth": True}, - ), - cast_to=Run, - stream=True, - stream_cls=AsyncStream[AssistantStreamEvent], + return async_threads_create_and_run_stream( + self, + assistant_id=assistant_id, + instructions=instructions, + max_completion_tokens=max_completion_tokens, + max_prompt_tokens=max_prompt_tokens, + metadata=metadata, + model=model, + parallel_tool_calls=parallel_tool_calls, + response_format=response_format, + temperature=temperature, + thread=thread, + tool_choice=tool_choice, + tool_resources=tool_resources, + tools=tools, + top_p=top_p, + truncation_strategy=truncation_strategy, + event_handler=event_handler, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) - return AsyncAssistantStreamManager(request, event_handler=event_handler or AsyncAssistantEventHandler()) class ThreadsWithRawResponse: diff --git a/src/openai/resources/chat/completions/completions.py b/src/openai/resources/chat/completions/completions.py index e9dae4aa6d..1b987cf3df 100644 --- a/src/openai/resources/chat/completions/completions.py +++ b/src/openai/resources/chat/completions/completions.py @@ -2,13 +2,12 @@ from __future__ import annotations -import inspect -from typing import Dict, List, Type, Union, Iterable, Optional, cast -from functools import partial +from typing import Dict, List, Union, Iterable, Optional from typing_extensions import Literal, overload import httpx2 -import pydantic + +import openai.lib._chat_completion_resources from .... import _legacy_response from .messages import ( @@ -33,22 +32,16 @@ completion_update_params, ) from ...._base_client import AsyncPaginator, make_request_options -from ....lib._parsing import ( - ResponseFormatT, - validate_input_tools as _validate_input_tools, - parse_chat_completion as _parse_chat_completion, - materialize_input_tools as _materialize_input_tools, - type_to_response_format_param as _type_to_response_format, -) +from ....lib._parsing import ResponseFormatT from ....lib.streaming.chat import ChatCompletionStreamManager, AsyncChatCompletionStreamManager from ....types.shared.chat_model import ChatModel from ....types.chat.chat_completion import ChatCompletion from ....types.shared_params.metadata import Metadata from ....types.shared.reasoning_effort import ReasoningEffort +from ....lib._chat_completion_resources import parse, stream, async_parse, async_stream from ....types.chat.chat_completion_chunk import ChatCompletionChunk from ....types.chat.parsed_chat_completion import ParsedChatCompletion from ....types.chat.chat_completion_deleted import ChatCompletionDeleted -from ....types.chat.chat_completion_audio_param import ChatCompletionAudioParam from ....types.chat.chat_completion_message_param import ChatCompletionMessageParam from ....types.chat.chat_completion_tool_union_param import ChatCompletionToolUnionParam from ....types.chat.chat_completion_stream_options_param import ChatCompletionStreamOptionsParam @@ -58,6 +51,12 @@ __all__ = ["Completions", "AsyncCompletions"] +def validate_response_format( + response_format: object, +) -> None: + return openai.lib._chat_completion_resources.validate_response_format(response_format) + + class Completions(SyncAPIResource): """ Given a list of messages comprising a conversation, the model will return a response. @@ -128,8 +127,6 @@ def parse( user: str | Omit = omit, verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit, web_search_options: completion_create_params.WebSearchOptions | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -143,7 +140,7 @@ def parse( This method will also automatically parse `function` tool calls if: - You use the `openai.pydantic_function_tool()` helper method - - You mark your tool schema with `"strict": True` + - You mark your tool schema with `\"strict\": True` Example usage: ```py @@ -163,10 +160,10 @@ class MathResponse(BaseModel): client = OpenAI() completion = client.chat.completions.parse( - model="gpt-4o-2024-08-06", + model=\"gpt-4o-2024-08-06\", messages=[ - {"role": "system", "content": "You are a helpful math tutor."}, - {"role": "user", "content": "solve 8x + 31 = 2"}, + {\"role\": \"system\", \"content\": \"You are a helpful math tutor.\"}, + {\"role\": \"user\", \"content\": \"solve 8x + 31 = 2\"}, ], response_format=MathResponse, ) @@ -174,79 +171,51 @@ class MathResponse(BaseModel): message = completion.choices[0].message if message.parsed: print(message.parsed.steps) - print("answer: ", message.parsed.final_answer) + print(\"answer: \", message.parsed.final_answer) ``` """ - chat_completion_tools = _validate_input_tools(tools) - - extra_headers = { - "X-Stainless-Helper-Method": "chat.completions.parse", - **(extra_headers or {}), - } - - def parser(raw_completion: ChatCompletion) -> ParsedChatCompletion[ResponseFormatT]: - return _parse_chat_completion( - response_format=response_format, - chat_completion=raw_completion, - input_tools=chat_completion_tools, - ) - - return self._post( - "/chat/completions", - body=maybe_transform( - { - "model": model, - "stream": False, - "service_tier": service_tier, - "messages": messages, - "audio": audio, - "frequency_penalty": frequency_penalty, - "function_call": function_call, - "functions": functions, - "logit_bias": logit_bias, - "logprobs": logprobs, - "max_completion_tokens": max_completion_tokens, - "max_tokens": max_tokens, - "metadata": metadata, - "modalities": modalities, - "moderation": moderation, - "n": n, - "parallel_tool_calls": parallel_tool_calls, - "prediction": prediction, - "presence_penalty": presence_penalty, - "prompt_cache_key": prompt_cache_key, - "prompt_cache_options": prompt_cache_options, - "prompt_cache_retention": prompt_cache_retention, - "reasoning_effort": reasoning_effort, - "response_format": _type_to_response_format(response_format), - "safety_identifier": safety_identifier, - "seed": seed, - "stop": stop, - "store": store, - "stream_options": stream_options, - "temperature": temperature, - "tool_choice": tool_choice, - "tools": chat_completion_tools, - "top_logprobs": top_logprobs, - "top_p": top_p, - "user": user, - "verbosity": verbosity, - "web_search_options": web_search_options, - }, - completion_create_params.CompletionCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - post_parser=parser, - security={"bearer_auth": True}, - ), - # we turn the `ChatCompletion` instance into a `ParsedChatCompletion` - # in the `parser` function above - cast_to=cast(Type[ParsedChatCompletion[ResponseFormatT]], ChatCompletion), - stream=False, + return parse( + self, + messages=messages, + model=model, + audio=audio, + response_format=response_format, + frequency_penalty=frequency_penalty, + function_call=function_call, + functions=functions, + logit_bias=logit_bias, + logprobs=logprobs, + max_completion_tokens=max_completion_tokens, + max_tokens=max_tokens, + metadata=metadata, + modalities=modalities, + moderation=moderation, + n=n, + parallel_tool_calls=parallel_tool_calls, + prediction=prediction, + presence_penalty=presence_penalty, + prompt_cache_key=prompt_cache_key, + prompt_cache_options=prompt_cache_options, + prompt_cache_retention=prompt_cache_retention, + reasoning_effort=reasoning_effort, + safety_identifier=safety_identifier, + seed=seed, + service_tier=service_tier, + stop=stop, + store=store, + stream_options=stream_options, + temperature=temperature, + tool_choice=tool_choice, + tools=tools, + top_logprobs=top_logprobs, + top_p=top_p, + user=user, + verbosity=verbosity, + web_search_options=web_search_options, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) @overload @@ -1605,8 +1574,6 @@ def stream( user: str | Omit = omit, verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit, web_search_options: completion_create_params.WebSearchOptions | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1621,12 +1588,12 @@ def stream( ```py with client.chat.completions.stream( - model="gpt-4o-2024-08-06", + model=\"gpt-4o-2024-08-06\", messages=[...], ) as stream: for event in stream: - if event.type == "content.delta": - print(event.delta, flush=True, end="") + if event.type == \"content.delta\": + print(event.delta, flush=True, end=\"\") ``` When the context manager is entered, a `ChatCompletionStream` instance is returned which, like `.create(stream=True)` is an iterator. The full list of events that are yielded by the iterator are outlined in [these docs](https://github.com/openai/openai-python/blob/main/helpers.md#chat-completions-events). @@ -1634,20 +1601,12 @@ def stream( When the context manager exits, the response will be closed, however the `stream` instance is still available outside the context manager. """ - chat_completion_tools = _materialize_input_tools(tools) - - extra_headers = { - "X-Stainless-Helper-Method": "chat.completions.stream", - **(extra_headers or {}), - } - - api_request: partial[Stream[ChatCompletionChunk]] = partial( - self.create, + return stream( + self, messages=messages, model=model, audio=audio, - stream=True, - response_format=_type_to_response_format(response_format), + response_format=response_format, frequency_penalty=frequency_penalty, function_call=function_call, functions=functions, @@ -1669,12 +1628,12 @@ def stream( safety_identifier=safety_identifier, seed=seed, service_tier=service_tier, - store=store, stop=stop, + store=store, stream_options=stream_options, temperature=temperature, tool_choice=tool_choice, - tools=chat_completion_tools, + tools=tools, top_logprobs=top_logprobs, top_p=top_p, user=user, @@ -1685,11 +1644,6 @@ def stream( extra_body=extra_body, timeout=timeout, ) - return ChatCompletionStreamManager( - api_request, - response_format=response_format, - input_tools=chat_completion_tools, - ) class AsyncCompletions(AsyncAPIResource): @@ -1762,8 +1716,6 @@ async def parse( user: str | Omit = omit, verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit, web_search_options: completion_create_params.WebSearchOptions | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1777,7 +1729,7 @@ async def parse( This method will also automatically parse `function` tool calls if: - You use the `openai.pydantic_function_tool()` helper method - - You mark your tool schema with `"strict": True` + - You mark your tool schema with `\"strict\": True` Example usage: ```py @@ -1797,10 +1749,10 @@ class MathResponse(BaseModel): client = AsyncOpenAI() completion = await client.chat.completions.parse( - model="gpt-4o-2024-08-06", + model=\"gpt-4o-2024-08-06\", messages=[ - {"role": "system", "content": "You are a helpful math tutor."}, - {"role": "user", "content": "solve 8x + 31 = 2"}, + {\"role\": \"system\", \"content\": \"You are a helpful math tutor.\"}, + {\"role\": \"user\", \"content\": \"solve 8x + 31 = 2\"}, ], response_format=MathResponse, ) @@ -1808,79 +1760,51 @@ class MathResponse(BaseModel): message = completion.choices[0].message if message.parsed: print(message.parsed.steps) - print("answer: ", message.parsed.final_answer) + print(\"answer: \", message.parsed.final_answer) ``` """ - chat_completion_tools = _validate_input_tools(tools) - - extra_headers = { - "X-Stainless-Helper-Method": "chat.completions.parse", - **(extra_headers or {}), - } - - def parser(raw_completion: ChatCompletion) -> ParsedChatCompletion[ResponseFormatT]: - return _parse_chat_completion( - response_format=response_format, - chat_completion=raw_completion, - input_tools=chat_completion_tools, - ) - - return await self._post( - "/chat/completions", - body=await async_maybe_transform( - { - "model": model, - "stream": False, - "service_tier": service_tier, - "messages": messages, - "audio": audio, - "frequency_penalty": frequency_penalty, - "function_call": function_call, - "functions": functions, - "logit_bias": logit_bias, - "logprobs": logprobs, - "max_completion_tokens": max_completion_tokens, - "max_tokens": max_tokens, - "metadata": metadata, - "modalities": modalities, - "moderation": moderation, - "n": n, - "parallel_tool_calls": parallel_tool_calls, - "prediction": prediction, - "presence_penalty": presence_penalty, - "prompt_cache_key": prompt_cache_key, - "prompt_cache_options": prompt_cache_options, - "prompt_cache_retention": prompt_cache_retention, - "reasoning_effort": reasoning_effort, - "response_format": _type_to_response_format(response_format), - "safety_identifier": safety_identifier, - "seed": seed, - "store": store, - "stop": stop, - "stream_options": stream_options, - "temperature": temperature, - "tool_choice": tool_choice, - "tools": chat_completion_tools, - "top_logprobs": top_logprobs, - "top_p": top_p, - "user": user, - "verbosity": verbosity, - "web_search_options": web_search_options, - }, - completion_create_params.CompletionCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - post_parser=parser, - security={"bearer_auth": True}, - ), - # we turn the `ChatCompletion` instance into a `ParsedChatCompletion` - # in the `parser` function above - cast_to=cast(Type[ParsedChatCompletion[ResponseFormatT]], ChatCompletion), - stream=False, + return await async_parse( + self, + messages=messages, + model=model, + audio=audio, + response_format=response_format, + frequency_penalty=frequency_penalty, + function_call=function_call, + functions=functions, + logit_bias=logit_bias, + logprobs=logprobs, + max_completion_tokens=max_completion_tokens, + max_tokens=max_tokens, + metadata=metadata, + modalities=modalities, + moderation=moderation, + n=n, + parallel_tool_calls=parallel_tool_calls, + prediction=prediction, + presence_penalty=presence_penalty, + prompt_cache_key=prompt_cache_key, + prompt_cache_options=prompt_cache_options, + prompt_cache_retention=prompt_cache_retention, + reasoning_effort=reasoning_effort, + safety_identifier=safety_identifier, + seed=seed, + service_tier=service_tier, + stop=stop, + store=store, + stream_options=stream_options, + temperature=temperature, + tool_choice=tool_choice, + tools=tools, + top_logprobs=top_logprobs, + top_p=top_p, + user=user, + verbosity=verbosity, + web_search_options=web_search_options, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) @overload @@ -3239,8 +3163,6 @@ def stream( user: str | Omit = omit, verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit, web_search_options: completion_create_params.WebSearchOptions | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -3255,12 +3177,12 @@ def stream( ```py async with client.chat.completions.stream( - model="gpt-4o-2024-08-06", + model=\"gpt-4o-2024-08-06\", messages=[...], ) as stream: async for event in stream: - if event.type == "content.delta": - print(event.delta, flush=True, end="") + if event.type == \"content.delta\": + print(event.delta, flush=True, end=\"\") ``` When the context manager is entered, an `AsyncChatCompletionStream` instance is returned which, like `.create(stream=True)` is an async iterator. The full list of events that are yielded by the iterator are outlined in [these docs](https://github.com/openai/openai-python/blob/main/helpers.md#chat-completions-events). @@ -3268,19 +3190,12 @@ def stream( When the context manager exits, the response will be closed, however the `stream` instance is still available outside the context manager. """ - chat_completion_tools = _materialize_input_tools(tools) - - extra_headers = { - "X-Stainless-Helper-Method": "chat.completions.stream", - **(extra_headers or {}), - } - - api_request = self.create( + return async_stream( + self, messages=messages, model=model, audio=audio, - stream=True, - response_format=_type_to_response_format(response_format), + response_format=response_format, frequency_penalty=frequency_penalty, function_call=function_call, functions=functions, @@ -3307,7 +3222,7 @@ def stream( stream_options=stream_options, temperature=temperature, tool_choice=tool_choice, - tools=chat_completion_tools, + tools=tools, top_logprobs=top_logprobs, top_p=top_p, user=user, @@ -3318,11 +3233,6 @@ def stream( extra_body=extra_body, timeout=timeout, ) - return AsyncChatCompletionStreamManager( - api_request, - response_format=response_format, - input_tools=chat_completion_tools, - ) class CompletionsWithRawResponse: @@ -3332,6 +3242,7 @@ def __init__(self, completions: Completions) -> None: self.parse = _legacy_response.to_raw_response_wrapper( completions.parse, ) + self.create = _legacy_response.to_raw_response_wrapper( completions.create, ) @@ -3363,6 +3274,7 @@ def __init__(self, completions: AsyncCompletions) -> None: self.parse = _legacy_response.async_to_raw_response_wrapper( completions.parse, ) + self.create = _legacy_response.async_to_raw_response_wrapper( completions.create, ) @@ -3394,6 +3306,7 @@ def __init__(self, completions: Completions) -> None: self.parse = to_streamed_response_wrapper( completions.parse, ) + self.create = to_streamed_response_wrapper( completions.create, ) @@ -3425,6 +3338,7 @@ def __init__(self, completions: AsyncCompletions) -> None: self.parse = async_to_streamed_response_wrapper( completions.parse, ) + self.create = async_to_streamed_response_wrapper( completions.create, ) @@ -3447,10 +3361,3 @@ def messages(self) -> AsyncMessagesWithStreamingResponse: Given a list of messages comprising a conversation, the model will return a response. """ return AsyncMessagesWithStreamingResponse(self._completions.messages) - - -def validate_response_format(response_format: object) -> None: - if inspect.isclass(response_format) and issubclass(response_format, pydantic.BaseModel): - raise TypeError( - "You tried to pass a `BaseModel` class to `chat.completions.create()`; You must use `chat.completions.parse()` instead" - ) diff --git a/src/openai/resources/conversations/api.md b/src/openai/resources/conversations/api.md index 9e9181a367..ff5e3e69e0 100644 --- a/src/openai/resources/conversations/api.md +++ b/src/openai/resources/conversations/api.md @@ -6,7 +6,6 @@ Types: from openai.types.conversations import ( ComputerScreenshotContent, Conversation, - ConversationDeleted, ConversationDeletedResource, Message, SummaryTextContent, diff --git a/src/openai/resources/embeddings.py b/src/openai/resources/embeddings.py index f244b00d9f..8a31689671 100644 --- a/src/openai/resources/embeddings.py +++ b/src/openai/resources/embeddings.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Union, Iterable -from functools import partial from typing_extensions import Literal import httpx2 @@ -11,13 +10,13 @@ from .. import _legacy_response from ..types import embedding_create_params from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from .._utils import is_given, maybe_transform +from .._utils import maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper from .._base_client import make_request_options from ..types.embedding_model import EmbeddingModel -from ..lib._parsing._embeddings import parse_embedding_response as _parse_embedding_response +from ..lib._parsing._embeddings import prepare_embedding_request from ..types.create_embedding_response import CreateEmbeddingResponse __all__ = ["Embeddings", "AsyncEmbeddings"] @@ -100,26 +99,27 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - params = { - "model": model, - "input": input, - "user": user, - "dimensions": dimensions, - "encoding_format": encoding_format, - } - if not is_given(encoding_format): - params["encoding_format"] = "base64" - + _runtime_context = prepare_embedding_request( + self, + { + "model": model, + "input": input, + "dimensions": dimensions, + "encoding_format": encoding_format, + "user": user, + }, + encoding_format=encoding_format, + ) return self._post( "/embeddings", - body=maybe_transform(params, embedding_create_params.EmbeddingCreateParams), + body=maybe_transform(_runtime_context.body, embedding_create_params.EmbeddingCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - post_parser=partial(_parse_embedding_response, encoding_format=encoding_format), security={"bearer_auth": True}, + post_parser=_runtime_context.post_parser, ), cast_to=CreateEmbeddingResponse, ) @@ -202,26 +202,27 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - params = { - "model": model, - "input": input, - "user": user, - "dimensions": dimensions, - "encoding_format": encoding_format, - } - if not is_given(encoding_format): - params["encoding_format"] = "base64" - + _runtime_context = prepare_embedding_request( + self, + { + "model": model, + "input": input, + "dimensions": dimensions, + "encoding_format": encoding_format, + "user": user, + }, + encoding_format=encoding_format, + ) return await self._post( "/embeddings", - body=maybe_transform(params, embedding_create_params.EmbeddingCreateParams), + body=maybe_transform(_runtime_context.body, embedding_create_params.EmbeddingCreateParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - post_parser=partial(_parse_embedding_response, encoding_format=encoding_format), security={"bearer_auth": True}, + post_parser=_runtime_context.post_parser, ), cast_to=CreateEmbeddingResponse, ) diff --git a/src/openai/resources/realtime/api.md b/src/openai/resources/realtime/api.md index 199ceaf91a..b4874ea226 100644 --- a/src/openai/resources/realtime/api.md +++ b/src/openai/resources/realtime/api.md @@ -77,6 +77,21 @@ from openai.types.realtime import ( RealtimeTranscriptionSessionAudioInput, RealtimeTranscriptionSessionAudioInputTurnDetection, RealtimeTranscriptionSessionCreateRequest, + RealtimeTranslationClientEvent, + RealtimeTranslationClientSecretCreateResponse, + RealtimeTranslationInputAudioBufferAppendEvent, + RealtimeTranslationInputTranscriptDeltaEvent, + RealtimeTranslationOutputAudioDeltaEvent, + RealtimeTranslationOutputTranscriptDeltaEvent, + RealtimeTranslationServerEvent, + RealtimeTranslationSession, + RealtimeTranslationSessionCloseEvent, + RealtimeTranslationSessionClosedEvent, + RealtimeTranslationSessionCreateRequestParam, + RealtimeTranslationSessionCreatedEvent, + RealtimeTranslationSessionUpdateEvent, + RealtimeTranslationSessionUpdateRequest, + RealtimeTranslationSessionUpdatedEvent, RealtimeTruncation, RealtimeTruncationRetentionRatio, ResponseAudioDeltaEvent, diff --git a/src/openai/resources/responses/api.md b/src/openai/resources/responses/api.md index b8122e99ae..df999f9a4b 100644 --- a/src/openai/resources/responses/api.md +++ b/src/openai/resources/responses/api.md @@ -50,7 +50,6 @@ from openai.types.responses import ( ResponseConfigurationUpdateItem, ResponseConfigurationUpdateItemParam, ResponseContainerReference, - ResponseContent, ResponseContentPartAddedEvent, ResponseContentPartDoneEvent, ResponseConversationParam, @@ -110,7 +109,6 @@ from openai.types.responses import ( ResponseMcpListToolsCompletedEvent, ResponseMcpListToolsFailedEvent, ResponseMcpListToolsInProgressEvent, - ResponseOutputAudio, ResponseOutputItem, ResponseOutputItemAddedEvent, ResponseOutputItemDoneEvent, @@ -182,6 +180,8 @@ Methods: - client.responses.delete(response_id) -> None - client.responses.cancel(response_id) -> Response - client.responses.compact(\*\*params) -> CompactedResponse +- client.responses.stream(\*, response_id=..., input=..., model=..., access_programs=..., background=..., context_management=..., text_format=..., tools=..., conversation=..., include=..., instructions=..., max_output_tokens=..., max_tool_calls=..., metadata=..., parallel_tool_calls=..., moderation=..., previous_response_id=..., prompt=..., prompt_cache_key=..., prompt_cache_options=..., prompt_cache_retention=..., reasoning=..., safety_identifier=..., service_tier=..., store=..., stream_options=..., temperature=..., text=..., tool_choice=..., top_logprobs=..., top_p=..., truncation=..., user=..., starting_after=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> ResponseStreamManager[TextFormatT] +- client.responses.parse(\*, text_format=..., access_programs=..., background=..., context_management=..., conversation=..., include=..., input=..., instructions=..., max_output_tokens=..., max_tool_calls=..., metadata=..., model=..., moderation=..., parallel_tool_calls=..., previous_response_id=..., prompt=..., prompt_cache_key=..., prompt_cache_options=..., prompt_cache_retention=..., reasoning=..., safety_identifier=..., service_tier=..., store=..., stream=..., stream_options=..., temperature=..., text=..., tool_choice=..., tools=..., top_logprobs=..., top_p=..., truncation=..., user=..., verbosity=..., extra_headers=..., extra_query=..., extra_body=..., timeout=...) -> ParsedResponse[TextFormatT] ## InputItems diff --git a/src/openai/resources/responses/responses.py b/src/openai/resources/responses/responses.py index 71daf59964..ca4fdc1002 100644 --- a/src/openai/resources/responses/responses.py +++ b/src/openai/resources/responses/responses.py @@ -4,28 +4,15 @@ import json import logging -from copy import copy from types import TracebackType -from typing import ( - TYPE_CHECKING, - Any, - List, - Type, - Union, - Callable, - Iterable, - Iterator, - Optional, - Awaitable, - AsyncIterator, - cast, -) -from functools import partial -from typing_extensions import Literal, overload +from typing import TYPE_CHECKING, Any, List, Union, Callable, Iterable, Iterator, Optional, Awaitable, cast +from typing_extensions import Literal, AsyncIterator, overload import httpx2 from pydantic import BaseModel +import openai.lib._response_resources + from ... import _legacy_response from ..._types import NOT_GIVEN, Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from ..._utils import is_given, path_template, maybe_transform, strip_not_given, async_maybe_transform @@ -43,7 +30,6 @@ AsyncInputItemsWithStreamingResponse, ) from ..._streaming import Stream, AsyncStream -from ...lib._tools import PydanticFunctionTool, ResponsesPydanticFunctionTool from .input_tokens import ( InputTokens, AsyncInputTokens, @@ -63,11 +49,8 @@ responses_client_event_param, ) from ..._websocket_connection import WebSocketConnectionBase, AsyncWebSocketConnectionBase -from ...lib._parsing._responses import ( - TextFormatT, - parse_response, - type_to_text_format_param as _type_to_text_format_param, -) +from ...lib._parsing._responses import TextFormatT +from ...lib._response_resources import parse, stream, async_parse, async_stream from ...types.responses.response import Response from ...types.responses.tool_param import ToolParam, ParseableToolParam from ...types.responses.service_tier import ServiceTier @@ -101,6 +84,13 @@ log: logging.Logger = logging.getLogger(__name__) +# Retained module entry point; callers may live outside generated resources. +def _make_tools( # pyright: ignore[reportUnusedFunction] + tools: Iterable[ParseableToolParam] | Omit, +) -> List[ToolParam] | Omit: + return openai.lib._response_resources._make_tools(tools) + + class Responses(SyncAPIResource): """Create and manage model responses.""" @@ -1112,7 +1102,6 @@ def stream( text_format: type[TextFormatT] | Omit = omit, starting_after: int | Omit = omit, tools: Iterable[ParseableToolParam] | Omit = omit, - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1155,8 +1144,6 @@ def stream( top_p: Optional[float] | Omit = omit, truncation: Optional[Literal["auto", "disabled"]] | Omit = omit, user: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1200,135 +1187,52 @@ def stream( truncation: Optional[Literal["auto", "disabled"]] | Omit = omit, user: str | Omit = omit, starting_after: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> ResponseStreamManager[TextFormatT]: - new_response_args = { - "access_programs": access_programs, - "input": input, - "model": model, - "context_management": context_management, - "conversation": conversation, - "include": include, - "instructions": instructions, - "max_output_tokens": max_output_tokens, - "max_tool_calls": max_tool_calls, - "metadata": metadata, - "moderation": moderation, - "parallel_tool_calls": parallel_tool_calls, - "previous_response_id": previous_response_id, - "prompt": prompt, - "prompt_cache_key": prompt_cache_key, - "prompt_cache_options": prompt_cache_options, - "prompt_cache_retention": prompt_cache_retention, - "reasoning": reasoning, - "safety_identifier": safety_identifier, - "service_tier": service_tier, - "store": store, - "stream_options": stream_options, - "temperature": temperature, - "text": text, - "tool_choice": tool_choice, - "top_logprobs": top_logprobs, - "top_p": top_p, - "truncation": truncation, - "user": user, - "background": background, - } - new_response_args_names = [k for k, v in new_response_args.items() if is_given(v)] - - if (is_given(response_id) or is_given(starting_after)) and len(new_response_args_names) > 0: - raise ValueError( - "Cannot provide both response_id/starting_after can't be provided together with " - + ", ".join(new_response_args_names) - ) - tools = _make_tools(tools) - if len(new_response_args_names) > 0: - if not is_given(input): - raise ValueError("input must be provided when creating a new response") - - if not is_given(model): - raise ValueError("model must be provided when creating a new response") - - if is_given(text_format): - if not text: - text = {} - - if "format" in text: - raise TypeError("Cannot mix and match text.format with text_format") - - text = copy(text) - text["format"] = _type_to_text_format_param(text_format) - - api_request: partial[Stream[ResponseStreamEvent]] = partial( - self.create, - access_programs=access_programs, - input=input, - model=model, - tools=tools, - context_management=context_management, - conversation=conversation, - include=include, - instructions=instructions, - max_output_tokens=max_output_tokens, - max_tool_calls=max_tool_calls, - metadata=metadata, - moderation=moderation, - parallel_tool_calls=parallel_tool_calls, - previous_response_id=previous_response_id, - prompt=prompt, - prompt_cache_key=prompt_cache_key, - prompt_cache_options=prompt_cache_options, - prompt_cache_retention=prompt_cache_retention, - store=store, - stream_options=stream_options, - stream=True, - temperature=temperature, - text=text, - tool_choice=tool_choice, - reasoning=reasoning, - safety_identifier=safety_identifier, - service_tier=service_tier, - top_logprobs=top_logprobs, - top_p=top_p, - truncation=truncation, - user=user, - background=background, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - return ResponseStreamManager( - api_request, - text_format=text_format, - input_tools=tools, - starting_after=None, - ) - else: - if not is_given(response_id): - raise ValueError("id must be provided when streaming an existing response") - - return ResponseStreamManager( - lambda: self.retrieve( - response_id=response_id, - stream=True, - include=include or [], - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - starting_after=omit, - timeout=timeout, - ), - text_format=text_format, - input_tools=tools, - starting_after=starting_after if is_given(starting_after) else None, - ) + return stream( + self, + response_id=response_id, + input=input, + model=model, + access_programs=access_programs, + background=background, + context_management=context_management, + text_format=text_format, + tools=tools, + conversation=conversation, + include=include, + instructions=instructions, + max_output_tokens=max_output_tokens, + max_tool_calls=max_tool_calls, + metadata=metadata, + parallel_tool_calls=parallel_tool_calls, + moderation=moderation, + previous_response_id=previous_response_id, + prompt=prompt, + prompt_cache_key=prompt_cache_key, + prompt_cache_options=prompt_cache_options, + prompt_cache_retention=prompt_cache_retention, + reasoning=reasoning, + safety_identifier=safety_identifier, + service_tier=service_tier, + store=store, + stream_options=stream_options, + temperature=temperature, + text=text, + tool_choice=tool_choice, + top_logprobs=top_logprobs, + top_p=top_p, + truncation=truncation, + user=user, + starting_after=starting_after, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) def parse( self, @@ -1367,82 +1271,51 @@ def parse( truncation: Optional[Literal["auto", "disabled"]] | Omit = omit, user: str | Omit = omit, verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> ParsedResponse[TextFormatT]: - if is_given(text_format): - if not text: - text = {} - - if "format" in text: - raise TypeError("Cannot mix and match text.format with text_format") - text = copy(text) - text["format"] = _type_to_text_format_param(text_format) - - tools = _make_tools(tools) - - def parser(raw_response: Response) -> ParsedResponse[TextFormatT]: - return parse_response( - input_tools=tools, - text_format=text_format, - response=raw_response, - ) - - return self._post( - "/responses", - body=maybe_transform( - { - "model": model, - "stream": stream, - "service_tier": service_tier, - "access_programs": access_programs, - "background": background, - "context_management": context_management, - "conversation": conversation, - "include": include, - "input": input, - "instructions": instructions, - "max_output_tokens": max_output_tokens, - "max_tool_calls": max_tool_calls, - "metadata": metadata, - "moderation": moderation, - "parallel_tool_calls": parallel_tool_calls, - "previous_response_id": previous_response_id, - "prompt": prompt, - "prompt_cache_key": prompt_cache_key, - "prompt_cache_options": prompt_cache_options, - "prompt_cache_retention": prompt_cache_retention, - "reasoning": reasoning, - "safety_identifier": safety_identifier, - "store": store, - "stream_options": stream_options, - "temperature": temperature, - "text": text, - "tool_choice": tool_choice, - "tools": tools, - "top_logprobs": top_logprobs, - "top_p": top_p, - "truncation": truncation, - "user": user, - "verbosity": verbosity, - }, - response_create_params.ResponseCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - post_parser=parser, - security={"bearer_auth": True}, - ), - # we turn the `Response` instance into a `ParsedResponse` - # in the `parser` function above - cast_to=cast("Type[ParsedResponse[TextFormatT]]", Response), + return parse( + self, + text_format=text_format, + access_programs=access_programs, + background=background, + context_management=context_management, + conversation=conversation, + include=include, + input=input, + instructions=instructions, + max_output_tokens=max_output_tokens, + max_tool_calls=max_tool_calls, + metadata=metadata, + model=model, + moderation=moderation, + parallel_tool_calls=parallel_tool_calls, + previous_response_id=previous_response_id, + prompt=prompt, + prompt_cache_key=prompt_cache_key, + prompt_cache_options=prompt_cache_options, + prompt_cache_retention=prompt_cache_retention, + reasoning=reasoning, + safety_identifier=safety_identifier, + service_tier=service_tier, + store=store, + stream=stream, + stream_options=stream_options, + temperature=temperature, + text=text, + tool_choice=tool_choice, + tools=tools, + top_logprobs=top_logprobs, + top_p=top_p, + truncation=truncation, + user=user, + verbosity=verbosity, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) @overload @@ -1454,8 +1327,6 @@ def retrieve( include_obfuscation: bool | Omit = omit, starting_after: int | Omit = omit, stream: Literal[False] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1470,8 +1341,6 @@ def retrieve( stream: Literal[True], include: List[ResponseIncludable] | Omit = omit, starting_after: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1486,8 +1355,6 @@ def retrieve( stream: bool, include: List[ResponseIncludable] | Omit = omit, starting_after: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1502,8 +1369,6 @@ def retrieve( stream: bool = False, include: List[ResponseIncludable] | Omit = omit, starting_after: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1540,7 +1405,6 @@ def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - ... @overload def retrieve( @@ -1551,8 +1415,6 @@ def retrieve( include: List[ResponseIncludable] | Omit = omit, include_obfuscation: bool | Omit = omit, starting_after: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1589,7 +1451,6 @@ def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - ... @overload def retrieve( @@ -1600,8 +1461,6 @@ def retrieve( include: List[ResponseIncludable] | Omit = omit, include_obfuscation: bool | Omit = omit, starting_after: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1638,7 +1497,6 @@ def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - ... def retrieve( self, @@ -1648,8 +1506,6 @@ def retrieve( include_obfuscation: bool | Omit = omit, starting_after: int | Omit = omit, stream: Literal[False] | Literal[True] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -3021,7 +2877,6 @@ def stream( text_format: type[TextFormatT] | Omit = omit, starting_after: int | Omit = omit, tools: Iterable[ParseableToolParam] | Omit = omit, - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -3064,8 +2919,6 @@ def stream( top_p: Optional[float] | Omit = omit, truncation: Optional[Literal["auto", "disabled"]] | Omit = omit, user: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -3109,134 +2962,52 @@ def stream( truncation: Optional[Literal["auto", "disabled"]] | Omit = omit, user: str | Omit = omit, starting_after: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> AsyncResponseStreamManager[TextFormatT]: - new_response_args = { - "access_programs": access_programs, - "input": input, - "model": model, - "context_management": context_management, - "conversation": conversation, - "include": include, - "instructions": instructions, - "max_output_tokens": max_output_tokens, - "max_tool_calls": max_tool_calls, - "metadata": metadata, - "moderation": moderation, - "parallel_tool_calls": parallel_tool_calls, - "previous_response_id": previous_response_id, - "prompt": prompt, - "prompt_cache_key": prompt_cache_key, - "prompt_cache_options": prompt_cache_options, - "prompt_cache_retention": prompt_cache_retention, - "reasoning": reasoning, - "safety_identifier": safety_identifier, - "service_tier": service_tier, - "store": store, - "stream_options": stream_options, - "temperature": temperature, - "text": text, - "tool_choice": tool_choice, - "top_logprobs": top_logprobs, - "top_p": top_p, - "truncation": truncation, - "user": user, - "background": background, - } - new_response_args_names = [k for k, v in new_response_args.items() if is_given(v)] - - if (is_given(response_id) or is_given(starting_after)) and len(new_response_args_names) > 0: - raise ValueError( - "Cannot provide both response_id/starting_after can't be provided together with " - + ", ".join(new_response_args_names) - ) - - tools = _make_tools(tools) - if len(new_response_args_names) > 0: - if isinstance(input, NotGiven): - raise ValueError("input must be provided when creating a new response") - - if not is_given(model): - raise ValueError("model must be provided when creating a new response") - - if is_given(text_format): - if not text: - text = {} - - if "format" in text: - raise TypeError("Cannot mix and match text.format with text_format") - text = copy(text) - text["format"] = _type_to_text_format_param(text_format) - - api_request = self.create( - access_programs=access_programs, - input=input, - model=model, - stream=True, - tools=tools, - context_management=context_management, - conversation=conversation, - include=include, - instructions=instructions, - max_output_tokens=max_output_tokens, - max_tool_calls=max_tool_calls, - metadata=metadata, - moderation=moderation, - parallel_tool_calls=parallel_tool_calls, - previous_response_id=previous_response_id, - prompt=prompt, - prompt_cache_key=prompt_cache_key, - prompt_cache_options=prompt_cache_options, - prompt_cache_retention=prompt_cache_retention, - store=store, - stream_options=stream_options, - temperature=temperature, - text=text, - tool_choice=tool_choice, - reasoning=reasoning, - safety_identifier=safety_identifier, - service_tier=service_tier, - top_logprobs=top_logprobs, - top_p=top_p, - truncation=truncation, - user=user, - background=background, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - - return AsyncResponseStreamManager( - api_request, - text_format=text_format, - input_tools=tools, - starting_after=None, - ) - else: - if isinstance(response_id, Omit): - raise ValueError("response_id must be provided when streaming an existing response") - - api_request = self.retrieve( - response_id, - stream=True, - include=include or [], - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - ) - return AsyncResponseStreamManager( - api_request, - text_format=text_format, - input_tools=tools, - starting_after=starting_after if is_given(starting_after) else None, - ) + return async_stream( + self, + response_id=response_id, + input=input, + model=model, + access_programs=access_programs, + background=background, + context_management=context_management, + text_format=text_format, + tools=tools, + conversation=conversation, + include=include, + instructions=instructions, + max_output_tokens=max_output_tokens, + max_tool_calls=max_tool_calls, + metadata=metadata, + parallel_tool_calls=parallel_tool_calls, + moderation=moderation, + previous_response_id=previous_response_id, + prompt=prompt, + prompt_cache_key=prompt_cache_key, + prompt_cache_options=prompt_cache_options, + prompt_cache_retention=prompt_cache_retention, + reasoning=reasoning, + safety_identifier=safety_identifier, + service_tier=service_tier, + store=store, + stream_options=stream_options, + temperature=temperature, + text=text, + tool_choice=tool_choice, + top_logprobs=top_logprobs, + top_p=top_p, + truncation=truncation, + user=user, + starting_after=starting_after, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ) async def parse( self, @@ -3275,82 +3046,51 @@ async def parse( truncation: Optional[Literal["auto", "disabled"]] | Omit = omit, user: str | Omit = omit, verbosity: Optional[Literal["low", "medium", "high"]] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx2.Timeout | None | NotGiven = NOT_GIVEN, ) -> ParsedResponse[TextFormatT]: - if is_given(text_format): - if not text: - text = {} - - if "format" in text: - raise TypeError("Cannot mix and match text.format with text_format") - text = copy(text) - text["format"] = _type_to_text_format_param(text_format) - - tools = _make_tools(tools) - - def parser(raw_response: Response) -> ParsedResponse[TextFormatT]: - return parse_response( - input_tools=tools, - text_format=text_format, - response=raw_response, - ) - - return await self._post( - "/responses", - body=maybe_transform( - { - "model": model, - "stream": stream, - "service_tier": service_tier, - "access_programs": access_programs, - "background": background, - "context_management": context_management, - "conversation": conversation, - "include": include, - "input": input, - "instructions": instructions, - "max_output_tokens": max_output_tokens, - "max_tool_calls": max_tool_calls, - "metadata": metadata, - "moderation": moderation, - "parallel_tool_calls": parallel_tool_calls, - "previous_response_id": previous_response_id, - "prompt": prompt, - "prompt_cache_key": prompt_cache_key, - "prompt_cache_options": prompt_cache_options, - "prompt_cache_retention": prompt_cache_retention, - "reasoning": reasoning, - "safety_identifier": safety_identifier, - "store": store, - "stream_options": stream_options, - "temperature": temperature, - "text": text, - "tool_choice": tool_choice, - "tools": tools, - "top_logprobs": top_logprobs, - "top_p": top_p, - "truncation": truncation, - "user": user, - "verbosity": verbosity, - }, - response_create_params.ResponseCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - post_parser=parser, - security={"bearer_auth": True}, - ), - # we turn the `Response` instance into a `ParsedResponse` - # in the `parser` function above - cast_to=cast("Type[ParsedResponse[TextFormatT]]", Response), + return await async_parse( + self, + text_format=text_format, + access_programs=access_programs, + background=background, + context_management=context_management, + conversation=conversation, + include=include, + input=input, + instructions=instructions, + max_output_tokens=max_output_tokens, + max_tool_calls=max_tool_calls, + metadata=metadata, + model=model, + moderation=moderation, + parallel_tool_calls=parallel_tool_calls, + previous_response_id=previous_response_id, + prompt=prompt, + prompt_cache_key=prompt_cache_key, + prompt_cache_options=prompt_cache_options, + prompt_cache_retention=prompt_cache_retention, + reasoning=reasoning, + safety_identifier=safety_identifier, + service_tier=service_tier, + store=store, + stream=stream, + stream_options=stream_options, + temperature=temperature, + text=text, + tool_choice=tool_choice, + tools=tools, + top_logprobs=top_logprobs, + top_p=top_p, + truncation=truncation, + user=user, + verbosity=verbosity, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) @overload @@ -3362,8 +3102,6 @@ async def retrieve( include_obfuscation: bool | Omit = omit, starting_after: int | Omit = omit, stream: Literal[False] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -3378,8 +3116,6 @@ async def retrieve( stream: Literal[True], include: List[ResponseIncludable] | Omit = omit, starting_after: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -3394,8 +3130,6 @@ async def retrieve( stream: bool, include: List[ResponseIncludable] | Omit = omit, starting_after: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -3410,8 +3144,6 @@ async def retrieve( stream: bool = False, include: List[ResponseIncludable] | Omit = omit, starting_after: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -3448,7 +3180,6 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - ... @overload async def retrieve( @@ -3459,8 +3190,6 @@ async def retrieve( include: List[ResponseIncludable] | Omit = omit, include_obfuscation: bool | Omit = omit, starting_after: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -3497,7 +3226,6 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - ... @overload async def retrieve( @@ -3508,8 +3236,6 @@ async def retrieve( include: List[ResponseIncludable] | Omit = omit, include_obfuscation: bool | Omit = omit, starting_after: int | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -3546,7 +3272,6 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - ... async def retrieve( self, @@ -3556,8 +3281,6 @@ async def retrieve( include_obfuscation: bool | Omit = omit, starting_after: int | Omit = omit, stream: Literal[False] | Literal[True] | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -3937,6 +3660,7 @@ def __init__(self, responses: Responses) -> None: self.compact = _legacy_response.to_raw_response_wrapper( responses.compact, ) + self.parse = _legacy_response.to_raw_response_wrapper( responses.parse, ) @@ -3971,6 +3695,7 @@ def __init__(self, responses: AsyncResponses) -> None: self.compact = _legacy_response.async_to_raw_response_wrapper( responses.compact, ) + self.parse = _legacy_response.async_to_raw_response_wrapper( responses.parse, ) @@ -4048,46 +3773,6 @@ def input_tokens(self) -> AsyncInputTokensWithStreamingResponse: return AsyncInputTokensWithStreamingResponse(self._responses.input_tokens) -def _make_tools(tools: Iterable[ParseableToolParam] | Omit) -> List[ToolParam] | Omit: - if not is_given(tools): - return omit - - converted_tools: List[ToolParam] = [] - for tool in tools: - if tool["type"] != "function": - converted_tools.append(tool) - continue - - if "function" not in tool: - # standard Responses API case - converted_tools.append(tool) - continue - - function = cast(Any, tool)["function"] # pyright: ignore[reportUnnecessaryCast] - if not isinstance(function, PydanticFunctionTool): - raise Exception( - "Expected Chat Completions function tool shape to be created using `openai.pydantic_function_tool()`" - ) - - assert "parameters" in function - new_tool = ResponsesPydanticFunctionTool( - { - "type": "function", - "name": function["name"], - "description": function.get("description"), - "parameters": function["parameters"], - "strict": function.get("strict") or False, - }, - function.model, - ) - - # Retain applicable options added to the compatibility definition. - new_tool.update({key: value for key, value in function.items() if key not in new_tool}) - converted_tools.append(new_tool.cast()) - - return converted_tools - - class AsyncResponsesConnection(AsyncWebSocketConnectionBase): """Represents a live WebSocket connection to the Responses API""" diff --git a/src/openai/resources/uploads/uploads.py b/src/openai/resources/uploads/uploads.py index 6b0d038180..d486723587 100644 --- a/src/openai/resources/uploads/uploads.py +++ b/src/openai/resources/uploads/uploads.py @@ -72,7 +72,7 @@ def upload_file_chunked( part_size: int | None = None, md5: str | Omit = omit, ) -> Upload: - "Splits a file into multiple 64MB parts and uploads them sequentially." + """Splits a file into multiple 64MB parts and uploads them sequentially.""" @overload def upload_file_chunked( @@ -86,7 +86,7 @@ def upload_file_chunked( part_size: int | None = None, md5: str | Omit = omit, ) -> Upload: - "Splits an in-memory file into multiple 64MB parts and uploads them sequentially." + """Splits an in-memory file into multiple 64MB parts and uploads them sequentially.""" def upload_file_chunked( self, @@ -99,7 +99,18 @@ def upload_file_chunked( part_size: int | None = None, md5: str | Omit = omit, ) -> Upload: - 'Splits the given file into multiple parts and uploads them sequentially.\n\n ```py\n from pathlib import Path\n\n client.uploads.upload_file(\n file=Path("my-paper.pdf"),\n mime_type="pdf",\n purpose="assistants",\n )\n ```\n' + """Splits the given file into multiple parts and uploads them sequentially. + + ```py + from pathlib import Path + + client.uploads.upload_file( + file=Path(\"my-paper.pdf\"), + mime_type=\"pdf\", + purpose=\"assistants\", + ) + ``` + """ return upload_file_chunked( self, file=file, @@ -341,7 +352,7 @@ async def upload_file_chunked( part_size: int | None = None, md5: str | Omit = omit, ) -> Upload: - "Splits a file into multiple 64MB parts and uploads them sequentially." + """Splits a file into multiple 64MB parts and uploads them sequentially.""" @overload async def upload_file_chunked( @@ -355,7 +366,7 @@ async def upload_file_chunked( part_size: int | None = None, md5: str | Omit = omit, ) -> Upload: - "Splits an in-memory file into multiple 64MB parts and uploads them sequentially." + """Splits an in-memory file into multiple 64MB parts and uploads them sequentially.""" async def upload_file_chunked( self, @@ -368,7 +379,18 @@ async def upload_file_chunked( part_size: int | None = None, md5: str | Omit = omit, ) -> Upload: - 'Splits the given file into multiple parts and uploads them sequentially.\n\n ```py\n from pathlib import Path\n\n client.uploads.upload_file(\n file=Path("my-paper.pdf"),\n mime_type="pdf",\n purpose="assistants",\n )\n ```\n' + """Splits the given file into multiple parts and uploads them sequentially. + + ```py + from pathlib import Path + + client.uploads.upload_file( + file=Path(\"my-paper.pdf\"), + mime_type=\"pdf\", + purpose=\"assistants\", + ) + ``` + """ return await async_upload_file_chunked( self, file=file, diff --git a/src/openai/resources/webhooks/__init__.py b/src/openai/resources/webhooks/__init__.py index 296bdb375d..a4f5fbdcf1 100644 --- a/src/openai/resources/webhooks/__init__.py +++ b/src/openai/resources/webhooks/__init__.py @@ -1,5 +1,6 @@ # File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details. + from .webhooks import ( Webhooks as _Webhooks, AsyncWebhooks as _AsyncWebhooks, diff --git a/src/openai/resources/webhooks/api.md b/src/openai/resources/webhooks/api.md index cc3d442135..0a2870cd1e 100644 --- a/src/openai/resources/webhooks/api.md +++ b/src/openai/resources/webhooks/api.md @@ -53,6 +53,8 @@ Methods: - client.webhooks.delete(webhook_endpoint_id) -> DeletedWebhookEndpoint - client.webhooks.rotate_secret(webhook_endpoint_id, \*\*params) -> WebhookEndpointWithSecret - client.webhooks.test(webhook_endpoint_id, \*\*params) -> WebhookEndpointTestResult +- client.webhooks.unwrap(payload, headers, *, secret=None) -> UnwrapWebhookEvent +- client.webhooks.verify_signature(payload, headers, *, secret=None, tolerance=300) -> None ## EventTypes diff --git a/src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py b/src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py index 6e528de19d..73d15e2271 100644 --- a/src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py +++ b/src/openai/types/realtime/conversation_item_input_audio_transcription_delta_event.py @@ -32,7 +32,7 @@ class ConversationItemInputAudioTranscriptionDeltaEvent(BaseModel): logprobs: Optional[List[LogProbProperties]] = None """The log probabilities of the transcription. - These can be enabled by configuring the session with + These can be enabled by configurating the session with `"include": ["item.input_audio_transcription.logprobs"]`. Each entry in the array corresponds a log probability of which token would be selected for this chunk of transcription. This can help to identify if it was possible there were diff --git a/src/openai/types/realtime/realtime_error_event.py b/src/openai/types/realtime/realtime_error_event.py index 1cd8636d0b..e9b6933df1 100644 --- a/src/openai/types/realtime/realtime_error_event.py +++ b/src/openai/types/realtime/realtime_error_event.py @@ -12,7 +12,7 @@ class RealtimeErrorEvent(BaseModel): """ Returned when an error occurs, which could be a client problem or a server problem. Most errors are recoverable and the session will stay open, we - recommend to implementers to monitor and log error messages by default. + recommend to implementors to monitor and log error messages by default. """ error: RealtimeError diff --git a/src/openai/types/responses/tool_param.py b/src/openai/types/responses/tool_param.py index d27ed01958..0f49f86883 100644 --- a/src/openai/types/responses/tool_param.py +++ b/src/openai/types/responses/tool_param.py @@ -391,6 +391,7 @@ class LocalShell(TypedDict, total=False): ParseableToolParam: TypeAlias = Union[ToolParam, ChatCompletionFunctionToolParam] + WebSearchTool: TypeAlias = _compat_WebSearchTool WebSearchToolFilters: TypeAlias = _compat_WebSearchToolFilters diff --git a/src/openai/types/webhooks/unwrap_webhook_event.py b/src/openai/types/webhooks/unwrap_webhook_event.py index 9c121925f3..a5429f046b 100644 --- a/src/openai/types/webhooks/unwrap_webhook_event.py +++ b/src/openai/types/webhooks/unwrap_webhook_event.py @@ -58,7 +58,7 @@ FineTuningJobCancelledWebhookEvent, FineTuningJobFailedWebhookEvent, FineTuningJobSucceededWebhookEvent, - LiveCallIncomingWebhookEvent, # pyright: ignore[reportDeprecated] # Preserve legacy payload parsing. + LiveCallIncomingWebhookEvent, # pyright: ignore[reportDeprecated] LiveTransportIncomingWebhookEvent, RealtimeCallIncomingWebhookEvent, ResponseCancelledWebhookEvent, diff --git a/tests/lib/test_assistants_helper_contracts.py b/tests/lib/test_assistants_helper_contracts.py new file mode 100644 index 0000000000..f0ea00f802 --- /dev/null +++ b/tests/lib/test_assistants_helper_contracts.py @@ -0,0 +1,171 @@ +from __future__ import annotations + +import json +import inspect +from typing import Any +from unittest import mock + +import httpx2 +import pytest + +from openai import OpenAI, AsyncOpenAI +from openai._types import omit +from openai.lib.streaming import AssistantEventHandler, AsyncAssistantEventHandler + +pytestmark = pytest.mark.filterwarnings("ignore::DeprecationWarning") + + +@pytest.mark.parametrize("async_", [False, True], ids=["sync", "async"]) +@pytest.mark.parametrize("terminal", ["requires_action", "cancelled", "completed", "failed", "expired", "incomplete"]) +@pytest.mark.parametrize( + ("interval", "server_headers", "expected_sleep"), + [(omit, {}, 1.0), (omit, {"openai-poll-after-ms": "125"}, 0.125), (275, {"openai-poll-after-ms": "125"}, 0.275)], + ids=["fallback", "server", "explicit"], +) +async def test_run_poll_http_options_and_terminal_states( + async_: bool, terminal: str, interval: Any, server_headers: dict[str, str], expected_sleep: float +) -> None: + requests: list[httpx2.Request] = [] + + def respond(request: httpx2.Request) -> httpx2.Response: + requests.append(request) + return httpx2.Response( + 200, + headers=server_headers, + json={ + "id": "run-synthetic", + "object": "thread.run", + "status": "in_progress" if len(requests) == 1 else terminal, + }, + ) + + transport = httpx2.MockTransport(respond) + client: Any = ( + AsyncOpenAI(api_key="synthetic-key", http_client=httpx2.AsyncClient(transport=transport, trust_env=False)) + if async_ + else OpenAI(api_key="synthetic-key", http_client=httpx2.Client(transport=transport, trust_env=False)) + ) + runs = client.beta.threads.runs + with mock.patch.object(runs, "_sleep") as sleep: + result = runs.poll( + "run-synthetic", + "thread-synthetic", + {"OpenAI-Beta": "caller-beta", "X-Stainless-Poll-Helper": "caller"}, + {"probe": "yes"}, + {"probe": "body"}, + 4.5, + interval, + ) + if inspect.isawaitable(result): + result = await result + assert result.status == terminal + sleep.assert_called_once_with(expected_sleep) + assert len(requests) == 2 + for request in requests: + assert request.url.path == "/v1/threads/thread-synthetic/runs/run-synthetic" + assert request.url.params["probe"] == "yes" + assert request.headers["OpenAI-Beta"] == "caller-beta" + assert request.headers["X-Stainless-Poll-Helper"] == "caller" + assert request.extensions["timeout"]["read"] == 4.5 + assert request.content == b"" + if interval is not omit: + assert request.headers["X-Stainless-Custom-Poll-Interval"] == "275" + closed = client.close() + if inspect.isawaitable(closed): + await closed + + +@pytest.mark.parametrize("async_", [False, True], ids=["sync", "async"]) +@pytest.mark.parametrize("family", ["runs", "threads", "submit"]) +async def test_assistant_stream_factory_preserves_request_and_cleanup(async_: bool, family: str) -> None: + requests: list[httpx2.Request] = [] + responses: list[httpx2.Response] = [] + + def respond(request: httpx2.Request) -> httpx2.Response: + requests.append(request) + response = httpx2.Response(200, headers={"content-type": "text/event-stream"}, content=b"data: [DONE]\n\n") + responses.append(response) + return response + + transport = httpx2.MockTransport(respond) + client: Any = ( + AsyncOpenAI(api_key="synthetic-key", http_client=httpx2.AsyncClient(transport=transport, trust_env=False)) + if async_ + else OpenAI(api_key="synthetic-key", http_client=httpx2.Client(transport=transport, trust_env=False)) + ) + handler: Any = AsyncAssistantEventHandler() if async_ else AssistantEventHandler() + kwargs: dict[str, Any] = { + "event_handler": handler, + "extra_headers": {"OpenAI-Beta": "caller-beta", "X-Stainless-Stream-Helper": "caller"}, + "extra_query": {"probe": "yes"}, + "extra_body": {"probe": "body"}, + "timeout": 3.5, + } + if family == "runs": + manager = client.beta.threads.runs.stream(thread_id="thread-synthetic", assistant_id="asst-synthetic", **kwargs) + expected_path = "/v1/threads/thread-synthetic/runs" + elif family == "threads": + manager = client.beta.threads.create_and_run_stream(assistant_id="asst-synthetic", **kwargs) + expected_path = "/v1/threads/runs" + else: + manager = client.beta.threads.runs.submit_tool_outputs_stream( + run_id="run-synthetic", + thread_id="thread-synthetic", + tool_outputs=[{"tool_call_id": "call-synthetic", "output": "ok"}], + **kwargs, + ) + expected_path = "/v1/threads/thread-synthetic/runs/run-synthetic/submit_tool_outputs" + assert not inspect.isawaitable(manager) + assert not requests + if async_: + async with manager as stream: + assert stream is handler + await stream.until_done() + else: + with manager as stream: + assert stream is handler + stream.until_done() + assert len(requests) == 1 + request = requests[0] + assert request.url.path == expected_path + assert request.url.params["probe"] == "yes" + assert request.headers["OpenAI-Beta"] == "caller-beta" + assert request.headers["X-Stainless-Stream-Helper"] == "caller" + assert request.headers["X-Stainless-Custom-Event-Handler"] == "true" + assert request.extensions["timeout"]["read"] == 3.5 + body = json.loads(request.content) + assert body["stream"] is True + assert body["probe"] == "body" + assert responses[0].is_closed + closed = client.close() + if inspect.isawaitable(closed): + await closed + + +@pytest.mark.parametrize("async_", [False, True], ids=["sync", "async"]) +@pytest.mark.parametrize("method", ["create", "submit_tool_outputs", "create_and_run"]) +async def test_released_deprecation_wrapper_count(async_: bool, method: str) -> None: + import warnings + + transport = httpx2.MockTransport(lambda _request: httpx2.Response(500)) + client: Any = ( + AsyncOpenAI(api_key="synthetic-key", http_client=httpx2.AsyncClient(transport=transport, trust_env=False)) + if async_ + else OpenAI(api_key="synthetic-key", http_client=httpx2.Client(transport=transport, trust_env=False)) + ) + resource = client.beta.threads if method == "create_and_run" else client.beta.threads.runs + kwargs: dict[str, Any] = {"assistant_id": "asst-synthetic"} + if method != "create_and_run": + kwargs["thread_id"] = "thread-synthetic" + if method == "submit_tool_outputs": + kwargs = {"thread_id": "thread-synthetic", "run_id": "run-synthetic", "tool_outputs": []} + with mock.patch.object(resource, "_post", return_value=None), warnings.catch_warnings(record=True) as captured: + warnings.simplefilter("always", DeprecationWarning) + result = getattr(resource, method)(**kwargs) + if inspect.isawaitable(result): + await result + assert len(captured) == (1 if method == "create" and not async_ else 2) + assert all(str(w.message) == "The Assistants API is deprecated in favor of the Responses API" for w in captured) + closed = client.close() + if inspect.isawaitable(closed): + await closed diff --git a/tests/lib/test_embedding_request_context.py b/tests/lib/test_embedding_request_context.py new file mode 100644 index 0000000000..279681bbe0 --- /dev/null +++ b/tests/lib/test_embedding_request_context.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from typing import cast + +import pytest + +from openai._types import Omit, NotGiven, omit, not_given +from openai.lib._parsing import _embeddings as embeddings_parser +from tests.lib.test_embeddings import VALUES, ENCODED, make_response +from openai.types.create_embedding_response import CreateEmbeddingResponse + + +@pytest.mark.parametrize( + ("encoding_format", "decode"), + [(omit, True), (not_given, True), ("float", False), ("base64", False)], + ids=["omit", "not-given", "float", "base64"], +) +def test_preparation_retains_original_encoding_for_lazy_response_parsing( + encoding_format: str | Omit | NotGiven, + decode: bool, + monkeypatch: pytest.MonkeyPatch, +) -> None: + checks: list[None] = [] + + def has_numpy() -> bool: + checks.append(None) + return False + + monkeypatch.setattr(embeddings_parser, "has_numpy", has_numpy) + body: dict[str, object] = { + "input": "hello", + "model": "text-embedding-3-small", + "encoding_format": encoding_format, + "future_option": {"enabled": True}, + } + original = body.copy() + context = embeddings_parser.prepare_embedding_request(object(), body, encoding_format=encoding_format) + prepared = cast(dict[str, object], context.body) + + assert body == original + assert prepared is not body + assert prepared["encoding_format"] == ("base64" if decode else encoding_format) + assert prepared["future_option"] is body["future_option"] + assert checks == [] + + response = make_response(ENCODED) + parsed = cast(CreateEmbeddingResponse, context.post_parser(response)) + assert parsed is response + assert parsed.data[0].embedding == (VALUES if decode else ENCODED) + assert len(checks) == (1 if decode else 0) diff --git a/tests/test_client_runtime_bindings.py b/tests/test_client_runtime_bindings.py new file mode 100644 index 0000000000..28de249e5a --- /dev/null +++ b/tests/test_client_runtime_bindings.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import importlib +from typing import cast + +import httpx2 +import pytest + +from openai import OpenAI, AsyncOpenAI +from openai._types import Headers + + +def test_header_omission_resolves_original_client_module_binding(monkeypatch: pytest.MonkeyPatch) -> None: + client_module = importlib.import_module("openai._client") + + class ReplacementOmit: + pass + + monkeypatch.setattr(client_module, "Omit", ReplacementOmit) + headers = cast(Headers, {"AUTHORIZATION": ReplacementOmit()}) + assert client_module._has_omitted_header(headers, "authorization") + assert not client_module._has_omitted_header(headers, "api-key") + assert not client_module._has_omitted_header({"Authorization": "bearer synthetic"}, "authorization") + + +@pytest.mark.anyio +@pytest.mark.parametrize("class_name", ["OpenAI", "AsyncOpenAI"]) +async def test_saved_client_class_retains_lexical_super_after_module_rebinding( + monkeypatch: pytest.MonkeyPatch, class_name: str +) -> None: + client_module = importlib.import_module("openai._client") + saved_class = OpenAI if class_name == "OpenAI" else AsyncOpenAI + monkeypatch.setattr(client_module, class_name, object) + if saved_class is OpenAI: + with OpenAI(api_key="synthetic-key", http_client=httpx2.Client(trust_env=False)) as client: + assert client.api_key == "synthetic-key" + else: + async_client = AsyncOpenAI(api_key="synthetic-key", http_client=httpx2.AsyncClient(trust_env=False)) + try: + assert async_client.api_key == "synthetic-key" + finally: + await async_client.close()