fix(openai): stop policy rejection retries and support Astra context

Adapt structured policy-error handling and Astra model metadata from sub2api's recent compatibility updates. Preserve rejection codes through HTTP and SSE, stop retry and non-streaming replay, and add Astra's effective context to the official runtime.

Reference: https://github.com/Wei-Shaw/sub2api/pull/6636 and https://github.com/Wei-Shaw/sub2api/pull/6572. Implementation uses the existing TypeScript runtime and local regression harnesses.
This commit is contained in:
程序员阿江(Relakkes)
2026-09-08 11:23:56 +08:00
parent 6c6c4d08a2
commit 5c48991967
19 changed files with 354 additions and 16 deletions
+1
View File
@@ -585,6 +585,7 @@ describe('ProviderService', () => {
'gpt-5.4': 950_000,
'gpt-5.5': 258_400,
'gpt-5.4-mini': 258_400,
'gpt-6-astra': 997_500,
})
expect(env.ANTHROPIC_BASE_URL).toBeUndefined()
expect(env.ANTHROPIC_API_KEY).toBeUndefined()
@@ -47,6 +47,40 @@ describe('proxy network settings', () => {
beforeEach(setup)
afterEach(teardown)
for (const apiFormat of ['openai_chat', 'openai_responses'] as const) {
for (const upstreamStatus of [503, 200, 'thrown'] as const) {
test(`${apiFormat} preserves policy refusal despite upstream ${upstreamStatus}`, async () => {
const provider = await new ProviderService().addProvider({
presetId: 'custom', name: 'Policy fixture', baseUrl: 'https://api.example.com',
apiKey: 'sk-test', apiFormat,
models: { main: 'test-model', haiku: 'test-model', sonnet: 'test-model', opus: 'test-model' },
})
const originalFetch = globalThis.fetch
const upstreamFetch = mock(async () => {
if (upstreamStatus === 'thrown') {
throw Object.assign(new Error('Request denied'), { code: 'cyber_policy' })
}
return Response.json({
error: { code: 'cyber_policy', message: 'Request denied' },
}, { status: upstreamStatus })
})
globalThis.fetch = upstreamFetch as unknown as typeof fetch
try {
const req = new Request(`http://localhost:3456/proxy/providers/${provider.id}/v1/messages`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'test-model', max_tokens: 64, messages: [{ role: 'user', content: 'hello' }] }),
})
const res = await handleProxyRequest(req, new URL(req.url))
expect(res.status).toBe(403)
expect(await res.json()).toMatchObject({ error: { type: 'permission_error', code: 'cyber_policy' } })
expect(upstreamFetch).toHaveBeenCalledTimes(1)
} finally {
globalThis.fetch = originalFetch
}
})
}
}
test('uses configured AI request timeout for non-stream upstream requests', async () => {
await fs.writeFile(
path.join(tmpDir, 'settings.json'),
@@ -698,3 +698,35 @@ describe('openaiResponsesStreamToAnthropicResponse', () => {
expect(error.code).toBe('ERR_STREAM_PREMATURE_CLOSE')
})
})
describe('OpenAI policy failures', () => {
for (const code of ['cyber_policy', 'content_policy', 'content_policy_violation']) {
for (const oauth of [true, false]) {
test(`preserves ${code} as a terminal permission error (OAuth=${oauth})`, async () => {
const chunk = `event: response.failed\ndata: ${JSON.stringify({ response: { error: { code, message: 'Request denied' } } })}\n\n`
const events = await collectSse(openaiResponsesStreamToAnthropic(
makeStream([chunk]), 'test-model', { openAICodexOAuth: oauth },
))
expect(events.find((event) => event.event === 'error')?.data).toEqual({
type: 'error', error: { type: 'permission_error', code, message: 'Request denied' },
})
expect(events.some((event) => event.event === 'message_stop')).toBe(false)
await expect(openaiResponsesStreamToAnthropicResponse(
makeStream([chunk]), 'test-model', { openAICodexOAuth: oauth },
)).rejects.toMatchObject({ code, type: 'permission_error', status: 403 })
})
}
}
})
test('Chat SSE policy errors stop without a successful completion', async () => {
const events = await collectSse(openaiChatStreamToAnthropic(makeStream([
'data: {"error":{"code":"cyber_policy","message":"Request denied"}}\n\n',
'data: [DONE]\n\n',
]), 'test-model'))
expect(events).toEqual([{ event: 'error', data: {
type: 'error', error: { type: 'permission_error', code: 'cyber_policy', message: 'Request denied' },
} }])
})
+1
View File
@@ -1155,6 +1155,7 @@ describe('Models API', () => {
'gpt-5.4',
'gpt-5.5',
'gpt-5.4-mini',
'gpt-6-astra',
])
expect(body.models[0]).toMatchObject({
id: 'gpt-5.6-sol',
+30 -10
View File
@@ -9,6 +9,7 @@
* Original work by Jason Young, MIT License
*/
import { getOpenAIPolicyError } from '../../services/openaiAuth/policyError.js'
import { createGunzip, createInflate } from 'node:zlib'
import { ProviderService } from '../services/providerService.js'
@@ -267,15 +268,16 @@ export async function handleProxyRequest(req: Request, url: URL): Promise<Respon
}).catch(() => {})
}
console.error('[Proxy] Upstream request failed:', err)
const policyError = getOpenAIPolicyError(err)
return Response.json(
{
type: 'error',
error: {
error: policyError ? { type: 'permission_error', ...policyError } : {
type: 'api_error',
message: err instanceof Error ? err.message : String(err),
},
},
{ status: 502 },
{ status: policyError ? 403 : 502 },
)
}
}
@@ -707,9 +709,15 @@ async function handleOpenaiChat(
if (!upstream.ok) {
const errText = await upstream.text().catch(() => '')
let policyError = null
try {
policyError = getOpenAIPolicyError(JSON.parse(errText))
} catch {
// Unstructured upstream failures keep their existing error classification.
}
const errorBody = {
type: 'error',
error: {
error: policyError ? { type: 'permission_error', ...policyError } : {
type: 'api_error',
message: `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`,
},
@@ -732,7 +740,7 @@ async function handleOpenaiChat(
}
return Response.json(
errorBody,
{ status: upstream.status },
{ status: policyError ? 403 : upstream.status },
)
}
@@ -788,7 +796,10 @@ async function handleOpenaiChat(
// Non-streaming
const responseBody = await upstream.json()
const anthropicResponse = openaiChatToAnthropic(responseBody, body.model)
const policyError = getOpenAIPolicyError(responseBody)
const anthropicResponse = policyError
? { type: 'error', error: { type: 'permission_error', ...policyError } }
: openaiChatToAnthropic(responseBody, body.model)
if (traceContext) {
recordProxyTraceInBackground({
callId: traceCallId,
@@ -805,7 +816,7 @@ async function handleOpenaiChat(
responseHeaders: upstream.headers,
})
}
return Response.json(anthropicResponse)
return Response.json(anthropicResponse, { status: policyError ? 403 : 200 })
}
function shouldUseDeepSeekReasoningCompat(baseUrl: string): boolean {
@@ -892,9 +903,15 @@ async function handleOpenaiResponses(
if (!upstream.ok) {
const errText = await upstream.text().catch(() => '')
let policyError = null
try {
policyError = getOpenAIPolicyError(JSON.parse(errText))
} catch {
// Unstructured upstream failures keep their existing error classification.
}
const errorBody = {
type: 'error',
error: {
error: policyError ? { type: 'permission_error', ...policyError } : {
type: 'api_error',
message: `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`,
},
@@ -917,7 +934,7 @@ async function handleOpenaiResponses(
}
return Response.json(
errorBody,
{ status: upstream.status },
{ status: policyError ? 403 : upstream.status },
)
}
@@ -973,7 +990,10 @@ async function handleOpenaiResponses(
// Non-streaming
const responseBody = await upstream.json()
const anthropicResponse = openaiResponsesToAnthropic(responseBody, body.model)
const policyError = getOpenAIPolicyError(responseBody)
const anthropicResponse = policyError
? { type: 'error', error: { type: 'permission_error', ...policyError } }
: openaiResponsesToAnthropic(responseBody, body.model)
if (traceContext) {
recordProxyTraceInBackground({
callId: traceCallId,
@@ -990,7 +1010,7 @@ async function handleOpenaiResponses(
responseHeaders: upstream.headers,
})
}
return Response.json(anthropicResponse)
return Response.json(anthropicResponse, { status: policyError ? 403 : 200 })
}
function buildProxyTraceContext(
@@ -20,6 +20,7 @@
* - delta.reasoning (GLM-5, Cerebras, Groq — mapped to reasoning_content)
*/
import { getOpenAIPolicyError } from '../../../services/openaiAuth/policyError.js'
import type { OpenAIChatStreamChunk } from '../transform/types.js'
import { stringifyOpenAIToolArguments } from '../transform/toolArguments.js'
import { openaiUsageToAnthropic } from '../transform/usage.js'
@@ -104,6 +105,7 @@ export function openaiChatStreamToAnthropic(
async start(controller) {
const reader = upstream.getReader()
let errored = false
let policyRejected = false
try {
while (true) {
@@ -134,6 +136,15 @@ export function openaiChatStreamToAnthropic(
continue
}
const policyError = getOpenAIPolicyError(chunk)
if (policyError) {
policyRejected = true
enqueue(state, 'error', { type: 'error', error: { type: 'permission_error', ...policyError } })
flushQueue(state, controller, encoder)
await reader.cancel().catch(() => {})
return
}
processChunk(chunk, state)
flushQueue(state, controller, encoder)
}
@@ -143,7 +154,7 @@ export function openaiChatStreamToAnthropic(
controller.error(err)
} finally {
if (!errored) {
finalizeStream(state)
if (!policyRejected) finalizeStream(state)
flushQueue(state, controller, encoder)
controller.close()
}
@@ -4,6 +4,7 @@
* Original work by Jason Young, MIT License
*/
import { getOpenAIPolicyError } from '../../../services/openaiAuth/policyError.js'
import { encodeOpenAIReasoningEnvelope } from '../transform/openaiReasoning.js'
import { stringifyOpenAIToolArguments } from '../transform/toolArguments.js'
import { openaiUsageToAnthropic } from '../transform/usage.js'
@@ -412,7 +413,7 @@ function processEvent(
}
case 'response.incomplete':
if (!options.openAICodexOAuth) break
if (!options.openAICodexOAuth && !getOpenAIPolicyError(data)) break
state.terminalSeen = true
if (readIncompleteReason(asRecord(data.response)) === 'max_output_tokens') {
const response = asRecord(data.response)
@@ -434,7 +435,7 @@ function processEvent(
case 'response.failed':
case 'response.cancelled':
case 'error': {
if (!options.openAICodexOAuth) break
if (!options.openAICodexOAuth && !getOpenAIPolicyError(data)) break
state.terminalSeen = true
const streamError = readStreamError(event, data)
controller.enqueue(encoder.encode(formatSse('error', {
@@ -539,7 +540,9 @@ function closeAllReasoningBlocks(
function readStreamError(
event: string,
data: Record<string, unknown>,
): { type: 'api_error' | 'overloaded_error'; message: string } {
): { type: 'api_error' | 'overloaded_error' | 'permission_error'; message: string; code?: string } {
const policyError = getOpenAIPolicyError(data)
if (policyError) return { type: 'permission_error', ...policyError }
const response = asRecord(data.response)
const error = asRecord(response?.error) ?? asRecord(data.error) ?? data
const code = typeof error?.code === 'string' ? error.code : ''
@@ -1,3 +1,4 @@
import { getOpenAIPolicyError } from '../../../services/openaiAuth/policyError.js'
import { openaiResponsesToAnthropic } from '../transform/openaiResponsesToAnthropic.js'
import type {
AnthropicResponse,
@@ -92,7 +93,7 @@ export async function openaiResponsesStreamToAnthropicResponse(
completedResponse = response
}
} else if (
options.openAICodexOAuth &&
(options.openAICodexOAuth || getOpenAIPolicyError(data)) &&
(
currentEvent === 'response.failed' ||
currentEvent === 'response.incomplete' ||
@@ -100,7 +101,10 @@ export async function openaiResponsesStreamToAnthropicResponse(
currentEvent === 'error'
)
) {
terminalError = new Error(readTerminalError(currentEvent, data))
const policyError = getOpenAIPolicyError(data)
terminalError = policyError
? Object.assign(new Error(policyError.message), { code: policyError.code, type: 'permission_error', status: 403 })
: new Error(readTerminalError(currentEvent, data))
} else {
updateFallbackState(currentEvent, data, fallback)
}
@@ -0,0 +1,14 @@
import { describe, expect, test } from 'bun:test'
import { MODEL_CONTEXT_WINDOWS_ENV_KEY } from '../../utils/model/modelContextWindows.js'
import { buildOpenAIOfficialRuntimeEnv } from './openaiOfficialProvider.js'
describe('ChatGPT Official runtime environment', () => {
test('includes the Astra effective context window without changing the default model', () => {
const env = buildOpenAIOfficialRuntimeEnv()
const windows = JSON.parse(env[MODEL_CONTEXT_WINDOWS_ENV_KEY]!) as Record<string, number>
expect(windows['gpt-6-astra']).toBe(997_500)
expect(windows['gpt-5.6-sol']).toBe(353_400)
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.6-sol')
})
})
+5
View File
@@ -223,6 +223,7 @@ import {
startSessionActivity,
stopSessionActivity,
} from "../../utils/sessionActivity.js";
import { isOpenAIPolicyError } from "../openaiAuth/policyError.js"
import { shouldTriggerNonStreamingFallbackForEmptyStream } from "./streamFallback.js";
import { StreamAssistantCommitBuffer } from "./streamAssistantCommitBuffer.js";
import {
@@ -2774,6 +2775,9 @@ async function* queryModel(
streamMaxDurationTimer = null;
}
// A safety rejection is terminal, including for non-streaming fallback.
if (isOpenAIPolicyError(streamingError)) throw streamingError
// Instrumentation: if the watchdog had already fired and the for-await
// threw (rather than exiting cleanly), record that the loop DID exit and
// how long after the watchdog. Distinguishes true hangs from error exits.
@@ -3068,6 +3072,7 @@ async function* queryModel(
// with raw streams, 404s are thrown during creation (caught here).
const is404StreamCreationError =
!didFallBackToNonStreaming &&
!isOpenAIPolicyError(errorFromRetry) &&
errorFromRetry instanceof CannotRetryError &&
errorFromRetry.originalError instanceof APIError &&
errorFromRetry.originalError.status === 404;
@@ -467,6 +467,25 @@ async function captureQueryRequest({
}
}
test('does not replay a policy-blocked stream through non-streaming fallback', async () => {
let calls = 0
const result = await captureQueryRequest({
model: 'gpt-6-astra',
continuationSystemPrompts: [],
responseFactory: model => {
calls += 1
return new Response(calls === 1 ? sseEvent('error', {
type: 'error',
error: { type: 'permission_error', code: 'cyber_policy', message: 'Request blocked by safety policy' },
}) : successfulResponse(model), {
headers: { 'content-type': 'text/event-stream' },
})
},
})
expect(result.requests).toHaveLength(1)
expect(JSON.stringify(result.content)).toContain('Request blocked by safety policy')
})
test('keeps required-thinking models enabled when the caller requests disabled thinking', async () => {
const { content, requests } = await captureQueryRequest({
model: 'k3',
+30
View File
@@ -399,3 +399,33 @@ describe('RetriableStreamError', () => {
expect(wrapped.message).toContain('boom')
})
})
describe('policy rejection retry boundaries', () => {
test.each([401, 429, 503, 529])('stops HTTP %s policy errors before retry, refresh or model fallback', async status => {
const body = { error: { type: 'overloaded_error', code: 'cyber_policy', message: 'Rejected' } }
const rejection = new APIError(status, body, undefined, new Headers({ 'x-should-retry': 'true', 'retry-after': '0' }))
let clientCalls = 0
let attempts = 0
const generator = withRetry(
async () => { clientCalls++; return {} as Anthropic },
async () => { attempts++; throw rejection },
{ model: 'gpt-6', fallbackModel: 'fallback', initialConsecutive529Errors: 2, thinkingConfig: { type: 'disabled' }, maxRetries: 3 },
)
// The first next must fail; even a retry status yield would mean replay was scheduled.
let caught: unknown
try { await generator.next() } catch (error) { caught = error }
expect(caught).toBeInstanceOf(CannotRetryError)
expect((caught as CannotRetryError).originalError).toBe(rejection)
expect(attempts).toBe(1)
expect(clientCalls).toBe(1)
})
test('does not retry policy errors disguised as transient SSE or transport errors', () => {
for (const type of ['api_error', 'overloaded_error']) {
const body = { error: { type, code: 'cyber_policy' } }
expect(isRetryableStreamError(new APIError(undefined, body, undefined, undefined))).toBe(false)
}
const error = Object.assign(new Error('Disconnected'), { code: 'ECONNRESET', cause: { error: { code: 'cyber_policy' } } })
expect(isRetryableStreamTransportError(error)).toBe(false)
})
})
+9
View File
@@ -51,6 +51,7 @@ import {
REPEATED_529_ERROR_MESSAGE,
} from './errors.js'
import { extractConnectionErrorDetails } from './errorUtils.js'
import { isOpenAIPolicyError } from '../openaiAuth/policyError.js'
const abortError = () => new APIUserAbortError()
@@ -212,6 +213,7 @@ export class RetriableStreamError extends Error {
* technique the overloaded-error check has always used (see shouldRetry).
*/
export function isRetryableStreamError(error: unknown): boolean {
if (isOpenAIPolicyError(error)) return false
if (!(error instanceof APIError)) {
return false
}
@@ -254,6 +256,7 @@ const STREAM_TRANSPORT_DISCONNECT_CODES = new Set([
* shouldRetryStreamAfterTransportDisconnect.
*/
export function isRetryableStreamTransportError(error: unknown): boolean {
if (isOpenAIPolicyError(error)) return false
const code = extractConnectionErrorDetails(error)?.code
return code !== undefined && STREAM_TRANSPORT_DISCONNECT_CODES.has(code)
}
@@ -377,6 +380,11 @@ export async function* withRetry<T>(
return await operation(client, attempt, retryContext)
} catch (error) {
// Policy rejection is final even when a gateway labels it 401/429/5xx.
// Stop before auth refresh, fast-mode changes or persistent/model fallback.
if (isOpenAIPolicyError(error)) {
throw new CannotRetryError(error, retryContext)
}
lastError = error
logForDebugging(
`API error (attempt ${attempt}/${maxRetries + 1}): ${error instanceof APIError ? `${error.status} ${error.message}` : errorMessage(error)}`,
@@ -816,6 +824,7 @@ function handleGcpCredentialError(error: unknown): boolean {
}
function shouldRetry(error: APIError): boolean {
if (isOpenAIPolicyError(error)) return false
// Never retry mock errors - they're from /mock-limits command for testing
if (isMockRateLimitError(error)) {
return false
+17
View File
@@ -48,6 +48,23 @@ describe('buildOpenAICodexFetch', () => {
await fs.rm(tmpDir, { recursive: true, force: true })
})
test('preserves a structured policy rejection even when upstream wraps it in HTTP 503', async () => {
const codexFetch = buildOpenAICodexFetch(async () => Response.json({
error: { code: 'cyber_policy', message: 'Request blocked by safety policy' },
}, { status: 503, headers: { 'x-request-id': 'policy-request-id' } }), 'test')!
const response = await codexFetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
body: JSON.stringify({ model: 'gpt-6-astra', max_tokens: 64, messages: [{ role: 'user', content: 'Hello' }] }),
})
expect(response.status).toBe(403)
expect(response.headers.get('x-should-retry')).toBe('false')
expect(response.headers.get('x-request-id')).toBe('policy-request-id')
expect(await response.json()).toEqual({
type: 'error',
error: { type: 'permission_error', code: 'cyber_policy', message: 'Request blocked by safety policy' },
})
})
test('maps Anthropic messages to ChatGPT Codex responses endpoint with account header', async () => {
const upstreamCalls: Array<{
url: string
+16
View File
@@ -1,5 +1,6 @@
import type { ClientOptions } from '@anthropic-ai/sdk'
import { randomUUID } from 'crypto'
import { getOpenAIPolicyError } from './policyError.js'
import {
OPENAI_CODEX_API_ENDPOINT,
OPENAI_CODEX_ORIGINATOR,
@@ -119,6 +120,21 @@ export function buildOpenAICodexFetch(
const errorText = await upstream.text().catch(() => '').finally(() => {
upstreamAbort?.dispose()
})
const policyError = getOpenAIPolicyError({ message: errorText })
if (policyError) {
return Response.json({
type: 'error',
error: { type: 'permission_error', ...policyError },
}, {
status: 403,
headers: {
'x-should-retry': 'false',
...(upstream.headers.get('x-request-id')
? { 'x-request-id': upstream.headers.get('x-request-id')! }
: {}),
},
})
}
return Response.json(
{
type: 'error',
+17
View File
@@ -15,6 +15,23 @@ import {
} from './models.js'
describe('openai auth model resolution', () => {
test('provides Astra fallback metadata and effective runtime context for its public alias', () => {
const astra = OPENAI_CODEX_MODEL_CATALOG.find((model) => model.value === 'gpt-6-astra')
expect(astra).toMatchObject({
label: 'GPT-6 Astra',
defaultReasoningEffort: 'medium',
supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
contextWindow: 997_500,
})
for (const model of ['gpt-6-astra', 'gpt-6']) {
expect(getOpenAICodexContextWindowForModel(model)).toBe(997_500)
expect(getOpenAIModelDisplayName(model)).toBe('GPT-6 Astra')
expect(resolveOpenAIReasoningEffort(model, 'max')).toBe('max')
expect(resolveOpenAIReasoningEffort(model, 'ultra')).toBe('medium')
}
expect(OPENAI_DEFAULT_MAIN_MODEL).toBe('gpt-5.6-sol')
})
test('does not treat opus as an OpenAI Responses model', () => {
expect(isOpenAIResponsesModel('opus')).toBe(false)
})
+21
View File
@@ -7,6 +7,7 @@ export const OPENAI_CODEX_EFFECTIVE_CONTEXT_PERCENT = 95
export const OPENAI_CODEX_STANDARD_CONTEXT_WINDOW = 272_000
export const OPENAI_CODEX_FRONTIER_CONTEXT_WINDOW = 372_000
export const OPENAI_CODEX_LARGE_CONTEXT_WINDOW = 1_000_000
export const OPENAI_CODEX_ASTRA_CONTEXT_WINDOW = 1_050_000
export const OPENAI_CODEX_SPARK_CONTEXT_WINDOW = 128_000
export const OPENAI_CODEX_STANDARD_EFFECTIVE_CONTEXT_WINDOW = Math.floor(
(OPENAI_CODEX_STANDARD_CONTEXT_WINDOW * OPENAI_CODEX_EFFECTIVE_CONTEXT_PERCENT) /
@@ -24,6 +25,10 @@ export const OPENAI_CODEX_SPARK_EFFECTIVE_CONTEXT_WINDOW = Math.floor(
(OPENAI_CODEX_SPARK_CONTEXT_WINDOW * OPENAI_CODEX_EFFECTIVE_CONTEXT_PERCENT) /
100,
)
export const OPENAI_CODEX_ASTRA_EFFECTIVE_CONTEXT_WINDOW = Math.floor(
(OPENAI_CODEX_ASTRA_CONTEXT_WINDOW * OPENAI_CODEX_EFFECTIVE_CONTEXT_PERCENT) /
100,
)
export type OpenAIModelCatalogEntry = {
value: string
@@ -127,6 +132,15 @@ export const OPENAI_CODEX_MODEL_CATALOG: OpenAIModelCatalogEntry[] = [
supportedReasoningEfforts: GPT_5_5_REASONING_EFFORTS,
contextWindow: OPENAI_CODEX_STANDARD_EFFECTIVE_CONTEXT_WINDOW,
},
{
value: 'gpt-6-astra',
label: 'GPT-6 Astra',
description: 'Frontier model for complex reasoning and agentic work',
descriptionForModel: 'GPT-6 Astra - complex reasoning and agentic work',
defaultReasoningEffort: 'medium',
supportedReasoningEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
contextWindow: OPENAI_CODEX_ASTRA_EFFECTIVE_CONTEXT_WINDOW,
},
]
export function isOpenAIReasoningEffort(
@@ -215,6 +229,9 @@ export function resolveOpenAICodexModel(model: string): string {
export function getOpenAIModelDisplayName(model: string): string | null {
switch (model.trim().toLowerCase()) {
case 'gpt-6':
case 'gpt-6-astra':
return 'GPT-6 Astra'
case 'gpt-5.3-codex':
return 'GPT-5.3 Codex'
case 'gpt-5.6-sol':
@@ -254,6 +271,10 @@ export function getOpenAICodexContextWindowForModel(
// Codex OAuth follows the Codex app model catalog, not the public API model
// context limits. The catalog applies effective_context_window_percent=95,
// and the runtime /context display reports this effective window.
if (normalized === 'gpt-6-astra' || normalized === 'gpt-6') {
return OPENAI_CODEX_ASTRA_EFFECTIVE_CONTEXT_WINDOW
}
if (
normalized === 'gpt-5.6-sol' ||
normalized === 'gpt-5.6-terra' ||
@@ -0,0 +1,39 @@
import { describe, expect, test } from 'bun:test'
import { APIError } from '@anthropic-ai/sdk'
import { getOpenAIPolicyError, isOpenAIPolicyError } from './policyError.js'
describe('isOpenAIPolicyError', () => {
test.each(['cyber_policy', 'content_policy', 'content_policy_violation'])('recognizes structured %s across response and SDK wrappers', code => {
const body = { response: { error: { code, message: 'Rejected' } } }
expect(isOpenAIPolicyError(body)).toBe(true)
expect(isOpenAIPolicyError(new APIError(503, body, undefined, undefined))).toBe(true)
expect(isOpenAIPolicyError({ originalError: new Error(`503 ${JSON.stringify(body)}`) })).toBe(true)
expect(isOpenAIPolicyError(new Error('failed', { cause: body }))).toBe(true)
})
test('preserves upstream code and message for the caller', () => {
expect(getOpenAIPolicyError({ error: { code: 'cyber_policy', message: 'Request blocked' } })).toEqual({
code: 'cyber_policy',
message: 'Request blocked',
})
expect(getOpenAIPolicyError({ code: 'content_policy' })).toEqual({
code: 'content_policy',
message: 'Request rejected by the upstream safety policy.',
})
})
test('does not infer policy from prose, status, or unrelated payload fields', () => {
for (const input of [
new Error('cyber_policy: overloaded'),
{ error: { message: 'content_policy_violation', type: 'api_error' } },
{ status: 403 },
{ data: { code: 'cyber_policy' } },
{ code: 'invalid_prompt' },
{ message: 'prefix {"code":"cyber_policy"}' },
null,
]) expect(isOpenAIPolicyError(input)).toBe(false)
const cyclic: { cause?: unknown } = {}
cyclic.cause = cyclic
expect(isOpenAIPolicyError(cyclic)).toBe(false)
})
})
+45
View File
@@ -0,0 +1,45 @@
const POLICY_ERROR_CODES = new Set([
'cyber_policy',
'content_policy',
'content_policy_violation',
])
/** Match explicit upstream policy codes, never infer a rejection from prose. */
export function getOpenAIPolicyError(input: unknown): { code: string; message: string } | null {
const visited = new Set<object>()
function visit(value: unknown, depth: number): { code: string; message: string } | null {
if (depth > 12 || value === null || typeof value !== 'object' || visited.has(value)) return null
visited.add(value)
const record = value as Record<string, unknown>
if (typeof record.code === 'string' && POLICY_ERROR_CODES.has(record.code)) {
return {
code: record.code,
message: typeof record.message === 'string' && record.message.trim()
? record.message
: 'Request rejected by the upstream safety policy.',
}
}
// SDK errors, Responses events and our retry wrappers use these boundaries.
for (const key of ['error', 'response', 'originalError', 'cause']) {
const match = visit(record[key], depth + 1)
if (match) return match
}
if (typeof record.message === 'string') {
// Anthropic SDK prefixes a serialized error body with its HTTP status.
const serialized = record.message.replace(/^\d{3}\s+/, '').trim()
if (serialized.startsWith('{')) {
try {
return visit(JSON.parse(serialized), depth + 1)
} catch {
// Unstructured messages must not become policy classifications.
}
}
}
return null
}
return visit(input, 0)
}
export function isOpenAIPolicyError(input: unknown): boolean {
return getOpenAIPolicyError(input) !== null
}