ci(upstream-merge): add verbose OpenAI proxy with streaming-safe pass-through and rich JSON logs; upload/tail logs\n\n- Forward exact client signature (Accept: text/event-stream, OpenAI-Beta: responses=experimental, originator, UA)\n- Replace only Authorization with secret API key; preserve all other headers\n- Stream SSE verbatim; avoid buffering; generous timeouts\n- Structured JSON logs: request/response headers (redacted), status, x-request-id, byte counts\n- Print log tail in job summary and upload full log as artifact\n\nContext: diagnose intermittent 5xx seen in Code runs; ensure proxy cannot mask origin responses and provides correlation data.

This commit is contained in:
James Peter
2025-09-12 17:26:56 +10:00
parent 0e71ef8394
commit 43e6afe2d7
+89 -13
View File
@@ -319,7 +319,7 @@ jobs:
restore-keys: |
npm-cache-${{ runner.os }}-node20-
- name: Start local OpenAI proxy (no key to agent)
- name: Start local OpenAI proxy (no key to agent; verbose logging)
if: steps.check_upstream.outputs.skip != 'true'
id: openai_proxy
env:
@@ -334,35 +334,103 @@ jobs:
cat > .github/auto/openai-proxy.js <<'JS'
const http = require('http');
const https = require('https');
const crypto = require('crypto');
const { URL } = require('url');
const PORT = process.env.PORT || 5055;
const API_KEY = process.env.OPENAI_API_KEY || '';
const UPSTREAM = new URL(process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1');
const ALLOWED = ['/v1/chat/completions','/v1/responses'];
if (!API_KEY) { console.error('OPENAI_API_KEY missing'); process.exit(1); }
if (!API_KEY) { console.error('[fatal] OPENAI_API_KEY missing'); process.exit(1); }
function redactHeaders(h) {
const out = {};
for (const [k, v] of Object.entries(h || {})) {
const key = k.toLowerCase();
if (key === 'authorization' || key === 'proxy-authorization' || key === 'cookie' || key === 'set-cookie') {
out[key] = 'REDACTED';
} else {
out[key] = Array.isArray(v) ? v.map(() => '<omitted>') : String(v);
}
}
return out;
}
function log(ev) { try { console.log(JSON.stringify({ ts: new Date().toISOString(), ...ev })); } catch { /* noop */ } }
const server = http.createServer((req, res) => {
if (req.method !== 'POST' || !ALLOWED.some(p => req.url.startsWith(p))) {
const rid = crypto.randomUUID();
if (!ALLOWED.some(p => req.url.startsWith(p))) {
res.writeHead(403, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'blocked' }));
res.end(JSON.stringify({ error: 'blocked', path: req.url }));
return;
}
const chunks = [];
req.on('data', c => { chunks.push(c); if (Buffer.concat(chunks).length > 1024*1024) req.destroy(); });
let size = 0;
req.on('data', c => { chunks.push(c); size += c.length; if (size > 2*1024*1024) { req.destroy(new Error('body too large')); } });
req.on('error', err => {
log({ level: 'error', rid, msg: 'client request error', err: String(err) });
if (!res.headersSent) res.writeHead(400, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'bad_request' }));
});
req.on('end', () => {
const up = new URL('https://api.openai.com' + req.url);
const fw = https.request({
method: 'POST',
const body = Buffer.concat(chunks);
const up = new URL(req.url, UPSTREAM);
// Prepare upstream headers: start with incoming, then override auth & ensure SSE headers are preserved.
const incoming = { ...req.headers };
if (!incoming['content-type']) incoming['content-type'] = 'application/json';
if (!incoming['accept']) incoming['accept'] = 'text/event-stream';
incoming['authorization'] = `Bearer ${API_KEY}`; // replace with real API key
if (up.pathname.startsWith('/v1/responses')) {
if (!incoming['openai-beta']) incoming['openai-beta'] = 'responses=experimental';
}
if (!incoming['originator']) incoming['originator'] = 'codex_cli_rs';
incoming['connection'] = 'keep-alive';
log({ level: 'info', rid, phase: 'request', method: req.method, url: up.toString(), headers: redactHeaders(incoming), body_bytes: body.length });
const opts = {
protocol: up.protocol,
hostname: up.hostname,
port: up.port || (up.protocol === 'https:' ? 443 : 80),
path: up.pathname + up.search,
headers: { 'content-type': 'application/json', 'authorization': `Bearer ${API_KEY}` }
}, r => { res.writeHead(r.statusCode || 500, r.headers); r.pipe(res); });
fw.on('error', e => { res.writeHead(502, {'content-type':'application/json'}); res.end(JSON.stringify({ error:'upstream', message: e.message })); });
fw.end(Buffer.concat(chunks));
method: req.method,
headers: incoming,
};
const upstream = (up.protocol === 'https:' ? https : http).request(opts, (upr) => {
const resHeaders = { ...upr.headers };
res.writeHead(upr.statusCode || 500, resHeaders);
log({ level: 'info', rid, phase: 'response_head', status: upr.statusCode, headers: redactHeaders(resHeaders) });
let total = 0;
upr.on('data', (chunk) => { total += chunk.length; });
upr.on('end', () => { log({ level: 'info', rid, phase: 'response_end', status: upr.statusCode, bytes: total, request_id: resHeaders['x-request-id'] || null }); });
upr.on('error', (e) => { log({ level: 'error', rid, phase: 'response_error', err: String(e) }); });
upr.pipe(res);
});
upstream.setTimeout(600000, () => { upstream.destroy(new Error('upstream timeout')); });
upstream.on('error', (e) => {
log({ level: 'error', rid, phase: 'upstream_error', err: String(e) });
if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'upstream_error' }));
});
upstream.end(body);
});
});
server.listen(PORT, '127.0.0.1', () => { console.log('proxy listening on 127.0.0.1:'+PORT); });
server.headersTimeout = 650000;
server.keepAliveTimeout = 650000;
server.listen(PORT, '127.0.0.1', () => { log({ level: 'info', msg: 'proxy listening', addr: '127.0.0.1', port: PORT }); });
JS
node .github/auto/openai-proxy.js > .github/auto/openai-proxy.log 2>&1 &
echo "pid=$!" >> "$GITHUB_OUTPUT"
# Wait briefly for readiness
for i in {1..30}; do if nc -z 127.0.0.1 5055; then break; else sleep 0.2; fi; done || true
- name: Print proxy startup log tail
if: steps.check_upstream.outputs.skip != 'true'
run: |
set -euo pipefail
echo '### openai-proxy.log (tail)' >> "$GITHUB_STEP_SUMMARY"
{ echo '```'; tail -n 80 .github/auto/openai-proxy.log || true; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
- name: Prepare agent context (commit range, deleted paths, histogram)
id: prep
@@ -610,6 +678,14 @@ jobs:
--skip-git-repo-check \
- | tee .github/auto/AGENT_STDOUT.txt
- name: Upload artifact - openai-proxy.log
if: steps.check_upstream.outputs.skip != 'true'
uses: actions/upload-artifact@v4
with:
name: openai-proxy.log
if-no-files-found: warn
path: .github/auto/openai-proxy.log
- name: Post-merge verify (build-fast + API compile)
id: verify
if: steps.check_upstream.outputs.skip != 'true'