astra-preflight 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JinHyuk Sung
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ <a href="https://sjh9714.github.io/astra-preflight/"><img src="https://sjh9714.github.io/astra-preflight/social.png" alt="Astra Preflight. Your model changed. Did your payload? An offline GPT-6 Astra migration checker." width="100%"></a>
2
+
3
+ # Astra Preflight
4
+
5
+ **Catch GPT-6 Astra request incompatibilities before calling the API.**
6
+
7
+ [![CI](https://github.com/sjh9714/astra-preflight/actions/workflows/ci.yml/badge.svg)](https://github.com/sjh9714/astra-preflight/actions/workflows/ci.yml)
8
+ [![npm](https://img.shields.io/npm/v/astra-preflight.svg)](https://www.npmjs.com/package/astra-preflight)
9
+ [![MIT](https://img.shields.io/badge/license-MIT-316a4c.svg)](LICENSE)
10
+
11
+ [**Try the playground →**](https://sjh9714.github.io/astra-preflight/) · [Rule coverage](docs/rules.md) · [한국어](docs/README.ko.md)
12
+
13
+ Switching the model name can leave behind incompatible settings. Astra Preflight checks a **request JSON payload** locally, links findings to official documentation, and offers a reviewable draft.
14
+
15
+ - **No API key, server, telemetry, or runtime dependencies.** Node.js 20+ for the CLI; a browser for the playground.
16
+ - Check sampling parameters, reasoning effort, tool endpoints, cache settings, EU Fast mode, and token limits.
17
+ - Catch gateway effort changes with `--expect-effort max` against the **final outgoing payload**.
18
+ - Keep your source unchanged. `--draft` prints a copy; endpoint and tool-conversation migrations stay manual.
19
+
20
+ ## Try it in 30 seconds
21
+
22
+ ```sh
23
+ npx astra-preflight request.json
24
+ ```
25
+
26
+ Or [open the example](https://sjh9714.github.io/astra-preflight/?example=legacy) without installing anything.
27
+
28
+ Given this request:
29
+
30
+ ```json
31
+ {
32
+ "model": "gpt-6-astra",
33
+ "input": "Summarize the release notes.",
34
+ "temperature": 0.7,
35
+ "reasoning": { "effort": "none" }
36
+ }
37
+ ```
38
+
39
+ The CLI reports:
40
+
41
+ ```text
42
+ Astra Preflight BLOCKED
43
+ 2 errors · 0 warnings · rules 2026-09-05
44
+
45
+ ERROR AP002 /temperature Unsupported sampling parameter
46
+ ERROR AP003 /reasoning/effort Unsupported reasoning effort
47
+ ```
48
+
49
+ Output above is abbreviated; the actual report includes explanations and official source links.
50
+
51
+ ```sh
52
+ # Print a draft to stdout for review; do not redirect over your input file.
53
+ npx astra-preflight request.json --draft
54
+
55
+ # Check a Chat Completions payload (text-only Astra calls are supported).
56
+ npx astra-preflight request.json --api chat
57
+
58
+ # Migrate a payload that still names a different model.
59
+ npx astra-preflight request.json --target
60
+
61
+ # A gateway may silently rewrite max to high. Inspect what it actually sends.
62
+ npx astra-preflight outgoing.json --expect-effort max
63
+
64
+ # The 272K threshold counts total input, including cached tokens.
65
+ npx astra-preflight request.json --input-tokens 272001 --strict
66
+
67
+ # JSON diagnostics for CI. Your prompt and payload are omitted.
68
+ npx astra-preflight request.json --format json
69
+ ```
70
+
71
+ Use `-` to read stdin. `--region eu` enables the EU Fast mode check. `--format md` emits Markdown. Full options: `npx astra-preflight --help`.
72
+
73
+ Exit codes: **0** no blocking documented finding; **1** errors (or warnings with `--strict`); **2** invalid input, invalid usage, or an unassessed model. A clear report is not a complete API validation.
74
+
75
+ ## Use in code
76
+
77
+ ```sh
78
+ npm install astra-preflight
79
+ ```
80
+
81
+ ```js
82
+ import { inspectRequest } from 'astra-preflight';
83
+
84
+ const request = {
85
+ model: 'gpt-6-astra',
86
+ input: 'Summarize the release notes.',
87
+ reasoning: { effort: 'max' }
88
+ };
89
+
90
+ const report = inspectRequest(request, { expectedEffort: 'max' });
91
+ if (!report.assessed || report.errors) {
92
+ throw new Error('Review the preflight diagnostics before sending.');
93
+ }
94
+ // Send request through your existing SDK here.
95
+ ```
96
+
97
+ `inspectRequest` and `draftRequest` are pure functions with TypeScript declarations. They do not contact OpenAI or inspect your SDK's internals. Check **after** transformations to detect gateway changes; checking only the original caller input cannot detect a later rewrite.
98
+
99
+ ## Why these checks?
100
+
101
+ These are motivated by launch-period integration reports, not an invented benchmark:
102
+
103
+ | Reported problem | What you can check |
104
+ | --- | --- |
105
+ | [Docker Agent #4162](https://github.com/docker/docker-agent/issues/4162), [QM #951](https://github.com/yc-software/qm/issues/951): tool-enabled Astra requests fail on Chat Completions | `--api chat` diagnoses tool workflow incompatibility |
106
+ | [Bifrost #6880](https://github.com/maximhq/bifrost/issues/6880): requested `max` becomes `high` | `--expect-effort max` checks an outgoing payload |
107
+ | [TeXRA #11878](https://github.com/LionSR/TeXRA/issues/11878): long-context accounting and saved `minimal` effort | Supplied-token threshold and effort checks |
108
+
109
+ The examples are **synthetic reproductions of the reported payload patterns**, not live tests of those projects. Issue status can change. Technical rules come from linked official OpenAI documentation, checked on **September 5, 2026**.
110
+
111
+ ## Scope and privacy
112
+
113
+ This release checks individual JSON requests for 14 rule families. It does **not** scan repositories, tokenize input, validate every API field, execute tools, monitor WebSocket steering, check billing, or verify model access. These are API checks; they do not calculate Codex subscription credits. A successful report is not a runtime guarantee.
114
+
115
+ The playground runs locally after loading its static files. It does not store or upload entered JSON. The CLI reports contain fixed diagnostics and JSON pointers, not prompts or keys. **`--draft` and “Copy draft” intentionally output a copy of your payload**; keep that copy private.
116
+
117
+ ## Development
118
+
119
+ ```sh
120
+ git clone https://github.com/sjh9714/astra-preflight.git
121
+ cd astra-preflight
122
+ npm test
123
+ npm run check
124
+ npm run build
125
+ npm run dev
126
+ # http://127.0.0.1:4173
127
+ ```
128
+
129
+ There is nothing to install for development: tests use Node's built-in runner, and the static build copies the shared checker. Tests cover request boundaries, draft preservation, gateway drift, and CLI exit/privacy behavior. No live OpenAI API tests are included.
130
+
131
+ Contribute a [redacted reproduction](https://github.com/sjh9714/astra-preflight/issues/new?template=bug_report.yml) or a documented rule with positive and negative tests. See [CONTRIBUTING.md](CONTRIBUTING.md).
132
+
133
+ If this saves you a debugging session, a GitHub star helps other Astra developers find it.
134
+
135
+ MIT · Independent community project; not affiliated with OpenAI.
package/bin/cli.mjs ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import { inspectRequest, draftRequest, RULESET_DATE } from '../src/preflight.mjs';
4
+
5
+ const HELP = `Astra Preflight — offline GPT-6 Astra request checks
6
+
7
+ Usage: astra-preflight <request.json | -> [options]
8
+
9
+ --api responses|chat Request endpoint (default: responses)
10
+ --region global|eu Your API data residency (default: global)
11
+ --target Assess a different model's payload for Astra
12
+ --input-tokens N Explicit total input count; enables limit/pricing checks
13
+ --expect-effort EFFORT Detect missing or changed reasoning effort
14
+ --format text|json|md Diagnostics format (default: text)
15
+ --draft Print a reviewable JSON payload to stdout
16
+ --strict Exit 1 on warnings as well as errors
17
+ --help Show this help
18
+ --version Show version and ruleset date
19
+
20
+ Exit codes: 0 = no blocking documented finding; 1 = findings; 2 = invalid input,
21
+ usage, or a request that was not assessed. No API calls or telemetry.
22
+ Diagnostics omit your payload. --draft intentionally prints a copy of it;
23
+ keep that output private. Existing files are never modified by the CLI.
24
+ `;
25
+
26
+ async function readStdin() {
27
+ const chunks = [];
28
+ let size = 0;
29
+ for await (const chunk of process.stdin) {
30
+ size += chunk.length;
31
+ if (size > 2 * 1024 * 1024) throw new Error('Input exceeds the 2 MiB limit.');
32
+ chunks.push(chunk);
33
+ }
34
+ return Buffer.concat(chunks).toString('utf8');
35
+ }
36
+
37
+ function render(report, format) {
38
+ if (format === 'json') return JSON.stringify(report, null, 2);
39
+ if (format === 'md') return [
40
+ `## Astra Preflight: ${report.status}`,
41
+ '', `${report.errors} errors · ${report.warnings} warnings · Rules checked ${report.ruleset}`, '',
42
+ ...report.diagnostics.flatMap(d => [
43
+ `- **${d.id} ${d.severity}: ${d.title}** — \`${d.path}\``,
44
+ ` ${d.detail} [Official source](${d.source})`,
45
+ ]), '', report.coverage,
46
+ ].join('\n');
47
+ return [
48
+ `Astra Preflight ${report.status.toUpperCase()}`,
49
+ `${report.errors} errors · ${report.warnings} warnings · rules ${report.ruleset}`, '',
50
+ ...report.diagnostics.flatMap(d => [
51
+ `${d.severity.toUpperCase()} ${d.id} ${d.path} ${d.title}`,
52
+ ` ${d.detail}`, ` ${d.source}`, '',
53
+ ]), report.coverage,
54
+ ].join('\n');
55
+ }
56
+
57
+ async function main(args) {
58
+ if (args.includes('--help') || args.includes('-h')) return void console.log(HELP);
59
+ if (args.includes('--version')) return void console.log(`0.1.0 (rules ${RULESET_DATE})`);
60
+ const options = {};
61
+ let file, format = 'text', draft = false, strict = false;
62
+ const valueFlags = { '--api': 'api', '--region': 'region', '--input-tokens': 'inputTokens', '--expect-effort': 'expectedEffort' };
63
+ for (let i = 0; i < args.length; i++) {
64
+ const arg = args[i];
65
+ if (arg === '--target') options.target = true;
66
+ else if (arg === '--draft') draft = true;
67
+ else if (arg === '--strict') strict = true;
68
+ else if (arg === '--format' || Object.hasOwn(valueFlags, arg)) {
69
+ const value = args[++i];
70
+ if (value === undefined || value.startsWith('--')) throw new Error('An option is missing its value. Use --help.');
71
+ if (arg === '--format') format = value;
72
+ else if (arg === '--input-tokens') {
73
+ if (!/^\d+$/.test(value)) throw new Error('Input tokens must be a nonnegative integer.');
74
+ options.inputTokens = Number(value);
75
+ } else options[valueFlags[arg]] = value;
76
+ } else if (arg.startsWith('-') && arg !== '-') throw new Error('Unknown option. Use --help.');
77
+ else if (file !== undefined) throw new Error('Provide exactly one request file, or - for stdin.');
78
+ else file = arg;
79
+ }
80
+ if (!file) throw new Error('Provide a request JSON file, or - for stdin. Use --help.');
81
+ if (!['text', 'json', 'md'].includes(format)) throw new Error('Format must be text, json, or md.');
82
+ if (draft && format !== 'text') throw new Error('--draft already emits JSON; do not combine it with --format.');
83
+ let content;
84
+ try { content = file === '-' ? await readStdin() : await readFile(file, 'utf8'); }
85
+ catch { throw new Error('Could not read input. Check the file path or stdin size.'); }
86
+ if (Buffer.byteLength(content) > 2 * 1024 * 1024) throw new Error('Input exceeds the 2 MiB limit.');
87
+ let request;
88
+ try { request = JSON.parse(content); }
89
+ catch { throw new Error('Input is not valid JSON. Parse details are omitted to keep payload content private.'); }
90
+ let report;
91
+ if (draft) {
92
+ const result = draftRequest(request, options);
93
+ report = result.report;
94
+ console.log(JSON.stringify(result.request, null, 2));
95
+ console.error(`Review draft: ${result.changes.length} edits; ${report.errors} errors, ${report.warnings} warnings remain. Original input unchanged.`);
96
+ } else {
97
+ report = inspectRequest(request, options);
98
+ console.log(render(report, format));
99
+ }
100
+ process.exitCode = !report.assessed ? 2 : (report.errors || (strict && report.warnings)) ? 1 : 0;
101
+ }
102
+
103
+ main(process.argv.slice(2)).catch(error => {
104
+ console.error(`Astra Preflight: ${error.message}`);
105
+ process.exitCode = 2;
106
+ });
@@ -0,0 +1,10 @@
1
+ {
2
+ "model": "gpt-6-astra",
3
+ "messages": [{ "role": "user", "content": "Check the build status." }],
4
+ "reasoning_effort": "medium",
5
+ "tools": [{ "type": "function", "function": {
6
+ "name": "get_build_status",
7
+ "description": "Read the current build status.",
8
+ "parameters": { "type": "object", "properties": {}, "additionalProperties": false }
9
+ } }]
10
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "model": "gpt-6-astra",
3
+ "input": "Summarize the release notes in three bullets.",
4
+ "temperature": 0.7,
5
+ "top_p": 0.9,
6
+ "reasoning": { "effort": "none" },
7
+ "prompt_cache_retention": "24h",
8
+ "max_output_tokens": 2000
9
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "model": "gpt-6-astra",
3
+ "input": "Summarize the release notes in three bullets.",
4
+ "reasoning": { "effort": "low" },
5
+ "prompt_cache_options": { "ttl": "30m" },
6
+ "max_output_tokens": 2000
7
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "astra-preflight",
3
+ "version": "0.1.0",
4
+ "description": "Catch GPT-6 Astra request incompatibilities before calling the API. Offline CLI, JavaScript library, and browser playground.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "JinHyuk Sung",
8
+ "repository": { "type": "git", "url": "git+https://github.com/sjh9714/astra-preflight.git" },
9
+ "homepage": "https://sjh9714.github.io/astra-preflight/",
10
+ "bugs": { "url": "https://github.com/sjh9714/astra-preflight/issues" },
11
+ "keywords": ["gpt-6", "astra", "openai", "migration", "responses-api", "validator", "cli"],
12
+ "engines": { "node": ">=20" },
13
+ "bin": { "astra-preflight": "bin/cli.mjs" },
14
+ "exports": { ".": { "types": "./src/index.d.ts", "import": "./src/preflight.mjs", "default": "./src/preflight.mjs" } },
15
+ "files": ["bin/", "src/", "examples/", "LICENSE", "README.md"],
16
+ "scripts": {
17
+ "test": "node --test",
18
+ "check": "node --check src/preflight.mjs && node --check bin/cli.mjs && node --check site/app.mjs",
19
+ "build": "node scripts/build.mjs",
20
+ "dev": "node scripts/serve.mjs",
21
+ "prepublishOnly": "npm test && npm run check"
22
+ }
23
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ export const MODEL: 'gpt-6-astra';
2
+ export const RULESET_DATE: string;
3
+ export const SOURCES: Readonly<Record<string, string>>;
4
+ export interface Options {
5
+ api?: 'responses' | 'chat';
6
+ region?: 'global' | 'eu';
7
+ target?: boolean;
8
+ inputTokens?: number;
9
+ expectedEffort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
10
+ }
11
+ export interface Diagnostic {
12
+ id: string;
13
+ severity: 'error' | 'warning' | 'info';
14
+ path: string;
15
+ title: string;
16
+ detail: string;
17
+ source: string;
18
+ edit?: { op: 'remove' | 'replace' | 'add'; path: string; value?: unknown };
19
+ }
20
+ export interface Report {
21
+ ruleset: string;
22
+ api: 'responses' | 'chat';
23
+ region: 'global' | 'eu';
24
+ targetModel: string;
25
+ assessed: boolean;
26
+ status: 'blocked' | 'review' | 'clear' | 'not-applicable';
27
+ errors: number;
28
+ warnings: number;
29
+ diagnostics: Diagnostic[];
30
+ coverage: string;
31
+ }
32
+ export function inspectRequest(request: Record<string, unknown>, options?: Options): Report;
33
+ export function draftRequest(request: Record<string, unknown>, options?: Options): {
34
+ request: Record<string, unknown>;
35
+ changes: { id: string; path: string }[];
36
+ report: Report;
37
+ };
@@ -0,0 +1,154 @@
1
+ /** Offline rules for documented GPT-6 Astra request incompatibilities. */
2
+ export const MODEL = 'gpt-6-astra';
3
+ export const RULESET_DATE = '2026-09-05';
4
+ export const SOURCES = Object.freeze({
5
+ migration: 'https://developers.openai.com/api/docs/guides/latest-model?model=gpt-6-astra',
6
+ responses: 'https://developers.openai.com/api/docs/guides/migrate-to-responses',
7
+ model: 'https://developers.openai.com/api/docs/models/gpt-6-astra',
8
+ reasoning: 'https://developers.openai.com/api/docs/guides/reasoning',
9
+ caching: 'https://developers.openai.com/api/docs/guides/prompt-caching',
10
+ async: 'https://developers.openai.com/api/docs/guides/async-tool-calling',
11
+ fast: 'https://developers.openai.com/api/docs/guides/fast-mode',
12
+ });
13
+ const efforts = ['low', 'medium', 'high', 'xhigh', 'max'];
14
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
15
+ const has = (value, key) => Object.hasOwn(value, key);
16
+
17
+ export function inspectRequest(request, options = {}) {
18
+ if (!object(request)) throw new TypeError('Request must be a JSON object.');
19
+ const { api = 'responses', region = 'global', target = false, inputTokens, expectedEffort } = options;
20
+ if (!['responses', 'chat'].includes(api)) throw new TypeError('API must be responses or chat.');
21
+ if (typeof target !== 'boolean') throw new TypeError('Migration target must be a boolean.');
22
+ if (!['global', 'eu'].includes(region)) throw new TypeError('Region must be global or eu.');
23
+ if (inputTokens !== undefined && (!Number.isSafeInteger(inputTokens) || inputTokens < 0)) {
24
+ throw new TypeError('Input tokens must be a nonnegative safe integer.');
25
+ }
26
+ if (expectedEffort !== undefined && !efforts.includes(expectedEffort)) {
27
+ throw new TypeError('Expected effort must be low, medium, high, xhigh, or max.');
28
+ }
29
+ const diagnostics = [];
30
+ const add = (id, severity, path, title, detail, source = 'migration', edit) => {
31
+ diagnostics.push({ id, severity, path, title, detail, source: SOURCES[source], ...(edit ? { edit } : {}) });
32
+ };
33
+ const result = () => ({
34
+ ruleset: RULESET_DATE, api, region, targetModel: MODEL, assessed: target || request.model === MODEL,
35
+ status: diagnostics.some(d => d.severity === 'error') ? 'blocked'
36
+ : !(target || request.model === MODEL) ? 'not-applicable'
37
+ : diagnostics.some(d => d.severity === 'warning') ? 'review' : 'clear',
38
+ errors: diagnostics.filter(d => d.severity === 'error').length,
39
+ warnings: diagnostics.filter(d => d.severity === 'warning').length,
40
+ diagnostics,
41
+ coverage: 'Documented request checks only. No API call, account-access check, full schema validation, or runtime guarantee.',
42
+ });
43
+ if (!target && request.model !== MODEL) {
44
+ add('AP001', 'warning', '/model', 'Astra request required',
45
+ 'Set model to gpt-6-astra, or explicitly enable migration preview to assess this payload for Astra.', 'model');
46
+ return result();
47
+ }
48
+ if (target && request.model !== MODEL) {
49
+ add('AP001', 'info', '/model', 'Migration preview targets Astra',
50
+ 'The draft will use gpt-6-astra. Evaluate output quality and account access separately.', 'model',
51
+ { op: has(request, 'model') ? 'replace' : 'add', path: '/model', value: MODEL });
52
+ }
53
+ for (const key of ['temperature', 'top_p', 'top_logprobs', ...(api === 'chat' ? ['logprobs'] : [])]) {
54
+ if (has(request, key)) add('AP002', request[key] === null ? 'warning' : 'error', `/${key}`,
55
+ 'Unsupported sampling parameter', `Remove ${key} from the request. Express style preferences in instructions.`,
56
+ 'migration', { op: 'remove', path: `/${key}` });
57
+ }
58
+ const effortPath = api === 'responses' ? '/reasoning/effort' : '/reasoning_effort';
59
+ const effort = api === 'responses' ? request.reasoning?.effort : request.reasoning_effort;
60
+ if (effort !== undefined && !efforts.includes(effort)) {
61
+ add('AP003', 'error', effortPath, 'Unsupported reasoning effort',
62
+ 'Astra supports low, medium, high, xhigh, and max. Review low as a starting point when migrating none or minimal.', 'reasoning',
63
+ ['none', 'minimal'].includes(effort) ? { op: 'replace', path: effortPath, value: 'low' } : undefined);
64
+ }
65
+ if (expectedEffort !== undefined && effort !== expectedEffort) {
66
+ add('AP004', 'error', effortPath, 'Reasoning differs from your expectation',
67
+ 'The explicit effort in this payload does not match --expect-effort. Inspect the final serialized request after gateway or SDK transformations.', 'reasoning');
68
+ }
69
+ if (api === 'chat' && ((Array.isArray(request.tools) && request.tools.length > 0)
70
+ || has(request, 'functions') || has(request, 'function_call') || has(request, 'tool_choice'))) {
71
+ add('AP005', 'error', '/tools', 'Astra tool calling needs Responses',
72
+ 'Move this tool workflow to /v1/responses and translate messages, tool definitions, and tool results. Changing the URL alone is insufficient. This step requires review.');
73
+ }
74
+ if (Array.isArray(request.include) && request.include.includes('message.output_text.logprobs')) {
75
+ add('AP006', 'error', '/include', 'Log probabilities are unavailable',
76
+ 'Remove message.output_text.logprobs from include.', 'migration');
77
+ }
78
+ if (has(request, 'prompt_cache_retention')) {
79
+ add('AP007', 'error', '/prompt_cache_retention', 'Legacy cache retention field',
80
+ 'Replace prompt_cache_retention with prompt_cache_options.ttl set to 30m. Review the different retention and cache-write billing.', 'caching');
81
+ }
82
+ if (object(request.prompt_cache_options) && has(request.prompt_cache_options, 'ttl') && request.prompt_cache_options.ttl !== '30m') {
83
+ add('AP008', 'error', '/prompt_cache_options/ttl', 'Unsupported cache lifetime',
84
+ 'The documented cache lifetime option for Astra is 30m.', 'caching',
85
+ { op: 'replace', path: '/prompt_cache_options/ttl', value: '30m' });
86
+ }
87
+ if (region === 'eu' && ['fast', 'priority'].includes(request.service_tier)) {
88
+ add('AP009', 'error', '/service_tier', 'Fast mode is unavailable with EU residency',
89
+ 'Use standard processing for Astra with EU data residency. Verify your organization and endpoint configuration.', 'fast',
90
+ { op: 'remove', path: '/service_tier' });
91
+ }
92
+ const wrongFields = api === 'responses' ? ['messages', 'reasoning_effort', 'max_completion_tokens', 'max_tokens', 'response_format', 'n']
93
+ : ['input', 'reasoning', 'max_output_tokens', 'previous_response_id', 'text'];
94
+ for (const field of wrongFields) {
95
+ if (has(request, field)) add('AP010', 'error', `/${field}`, 'Field belongs to a different API shape',
96
+ `Review ${field} for the selected ${api === 'chat' ? 'Chat Completions' : 'Responses'} endpoint. Payload and tool-result formats differ; this tool does not convert conversations.`, 'responses');
97
+ }
98
+ const limitKey = api === 'responses' ? 'max_output_tokens' : 'max_completion_tokens';
99
+ if (has(request, limitKey) && (!Number.isSafeInteger(request[limitKey]) || request[limitKey] < 1 || request[limitKey] > 128000)) {
100
+ add('AP011', 'error', `/${limitKey}`, 'Output budget is outside the model limit',
101
+ 'Use an integer output budget within the documented 128,000-token maximum. Endpoint-specific minimums are not checked.', 'model');
102
+ }
103
+ if (Array.isArray(request.tools)) request.tools.forEach((tool, index) => {
104
+ if (!object(tool)) return;
105
+ if (tool.async === true && !['function', 'custom'].includes(tool.type)) {
106
+ add('AP012', 'error', `/tools/${index}/async`, 'Async applies to client function or custom tools',
107
+ 'Hosted built-in tools cannot be marked async. Your application owns execution and pending results for client tools.', 'async');
108
+ }
109
+ if (tool.type === 'function' && api === 'responses' && object(tool.function)) {
110
+ add('AP013', 'error', `/tools/${index}/function`, 'Chat-style function wrapper',
111
+ 'Responses function tools put name, description, and parameters directly on the tool object. Review strict schemas and your result handling before migration.', 'responses');
112
+ }
113
+ });
114
+ if (inputTokens !== undefined) {
115
+ if (inputTokens > 922000) add('AP014', 'error', '/input', 'Input exceeds the documented maximum',
116
+ 'Astra accepts at most 922,000 input tokens. This check uses your supplied count, not a tokenizer.', 'model');
117
+ else if (inputTokens > 272000) add('AP014', 'warning', '/input', 'Long-context pricing applies to the whole request',
118
+ 'Above 272,000 input tokens, input and cache rates double and output rates rise by 1.5× for the full request. This is API pricing, not Codex subscription usage.', 'model');
119
+ if (inputTokens + (Number.isSafeInteger(request[limitKey]) ? request[limitKey] : 0) > 1050000) {
120
+ add('AP014', 'error', `/${limitKey}`, 'Input and output budget exceed the context window',
121
+ 'The documented context window is 1,050,000 tokens. Reduce the supplied input count or requested output budget.', 'model');
122
+ }
123
+ }
124
+ return result();
125
+ }
126
+
127
+ /** Produces a reviewable copy. Never writes files or converts tool conversations. */
128
+ export function draftRequest(request, options = {}) {
129
+ const report = inspectRequest(request, options);
130
+ const draft = JSON.parse(JSON.stringify(request));
131
+ const changes = [];
132
+ for (const diagnostic of report.diagnostics) {
133
+ const edit = diagnostic.edit;
134
+ if (!edit) continue;
135
+ const keys = edit.path.slice(1).split('/');
136
+ let parent = draft;
137
+ for (const key of keys.slice(0, -1)) parent = parent[key];
138
+ const key = keys.at(-1);
139
+ if (edit.op === 'remove') delete parent[key];
140
+ else parent[key] = edit.value;
141
+ changes.push({ id: diagnostic.id, path: edit.path });
142
+ }
143
+ if (report.assessed && has(draft, 'prompt_cache_retention') &&
144
+ (!has(draft, 'prompt_cache_options') || object(draft.prompt_cache_options))) {
145
+ delete draft.prompt_cache_retention;
146
+ draft.prompt_cache_options = { ...draft.prompt_cache_options, ttl: '30m' };
147
+ changes.push({ id: 'AP007', path: '/prompt_cache_options/ttl' });
148
+ }
149
+ if (report.assessed && Array.isArray(draft.include) && draft.include.includes('message.output_text.logprobs')) {
150
+ draft.include = draft.include.filter(value => value !== 'message.output_text.logprobs');
151
+ changes.push({ id: 'AP006', path: '/include' });
152
+ }
153
+ return { request: draft, changes, report: inspectRequest(draft, options) };
154
+ }