Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions open-sse/config/anthropicBeta.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export const ANTHROPIC_BETA_HEADER = "anthropic-beta";
export const CLAUDE_SESSION_HEADER = "x-claude-code-session-id";
export const CONTEXT_MANAGEMENT_BETA = "context-management-2025-06-27";
export const CLAUDE_CODE_BETA = "claude-code-20250219";
export const VALID_ANTHROPIC_BETA = /^[a-z0-9][a-z0-9-]{0,127}$/i;
export const MAX_CLAUDE_SESSION_ID_LENGTH = 256;

// Keep body-field-to-beta coupling in one table so future features need one mapping.
export const ANTHROPIC_BETA_FEATURES = Object.freeze([
{
flag: CONTEXT_MANAGEMENT_BETA,
present: body => Object.prototype.hasOwnProperty.call(body || {}, "context_management"),
},
{
flag: "effort-2025-11-24",
present: body => Object.prototype.hasOwnProperty.call(body?.output_config || {}, "effort"),
},
{
flag: "advanced-tool-use-2025-11-20",
present: body => Array.isArray(body?.tools)
&& body.tools.some(tool => Object.prototype.hasOwnProperty.call(tool || {}, "input_examples")),
},
{
flag: "structured-outputs-2025-12-15",
present: body => Object.prototype.hasOwnProperty.call(body?.output_config || {}, "format"),
},
]);
36 changes: 36 additions & 0 deletions open-sse/config/errorConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,42 @@ export const DEFAULT_ERROR_MESSAGES = {
504: "Gateway timeout"
};

export const CODEX_REQUEST_SCHEMA_ERROR_CODES = new Set([
"unknown_parameter",
"unsupported_value",
]);

export const CODEX_REQUEST_SCHEMA_MESSAGE_PATTERN = /\b(?:unknown[_ ]parameter|unsupported[_ ]value)\b/i;
export const CODEX_REQUEST_SCHEMA_PARAM_ROOTS = new Set([
"input",
"instructions",
"tools",
"tool_choice",
"parallel_tool_calls",
"stream",
"store",
"reasoning",
"service_tier",
"include",
"prompt_cache_key",
"client_metadata",
"text",
]);
export const CODEX_ITEM_ID_PARAM_PATTERN = /^input\[\d+\]\.id$/;
export const CODEX_ITEM_ID_MESSAGE_PATTERN = /expected an id that begins with ["'`]\w+["'`]/i;

export const CLAUDE_SCHEMA_FIELD_MESSAGE_PATTERN = /(?:^|[\s"'`])(?:[a-z_]\w*(?:\.\w+|\[\d+\])*)["'`]?\s*:\s*extra inputs are not permitted\b/i;
export const CLAUDE_BETA_HEADER_MESSAGE_PATTERN = /(?:\banthropic[-_ ]beta\b(?:\s+header)?\s*(?::|contains?|has|includes?)\s*(?:an?\s+)?(?:invalid|unsupported|unknown|unrecognized)\s+(?:beta\s+)?(?:value|flag|token|feature|version|name)\b|\b(?:invalid|unsupported|unknown|unrecognized)\s+(?:(?:value|flag|token|feature|version|name)\s+(?:for|in)\s+)?(?:the\s+)?anthropic[-_ ]beta(?:\s+header)?(?:\s+(?:value|flag|token|feature|version|name))?\b)/i;
export const CLAUDE_INVALID_PROMPT_MESSAGE_PATTERN = /\binvalid[_ ]prompt\b/i;
export const CLAUDE_PERMISSION_MESSAGE_PATTERN = /(?:\b(?:unauthorized|unauthorised|forbidden|permission|entitlement)\b|\b(?:account|org(?:anization|anisation)?|workspace|subscription|plan|model)\b.{0,80}\b(?:access|permission|entitlement|unsupported|does\s+not\s+have|not\s+(?:allowed|available|enabled|entitled|supported))\b|\b(?:access|permission|entitlement|unsupported|does\s+not\s+have|not\s+(?:allowed|available|enabled|entitled|supported))\b.{0,80}\b(?:account|org(?:anization|anisation)?|workspace|subscription|plan|model)\b)/i;

export const REQUEST_SCHEMA_CLASSIFICATION = Object.freeze({
category: "request_schema",
accountFallback: false,
cooldownMs: 0,
comboScope: "provider",
});

// Exponential backoff config for rate limits
export const BACKOFF_CONFIG = {
base: 2000,
Expand Down
49 changes: 45 additions & 4 deletions open-sse/executors/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { shouldRefreshCredentials } from "../services/oauthCredentialManager.js"
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { dbg } from "../utils/debugLog.js";
import { ANTHROPIC_API_VERSION, OPENAI_COMPAT_BASE, ANTHROPIC_COMPAT_BASE } from "../providers/shared.js";
import { FORMATS } from "../translator/formats.js";
import { finalizeAnthropicOutboundRequest } from "../utils/anthropicOutbound.js";

function removeBetaFlag(headers, flag) {
for (const key of ["anthropic-beta", "Anthropic-Beta"]) {
Expand Down Expand Up @@ -93,6 +95,30 @@ export class BaseExecutor {
return body;
}

getOutboundFormat(model, credentials) {
if (credentials?.requestTargetFormat) return credentials.requestTargetFormat;
if (credentials?.runtimeTransport?.format) return credentials.runtimeTransport.format;
if (this.provider?.startsWith?.("anthropic-compatible-")) return FORMATS.CLAUDE;
return this.config?.format || FORMATS.OPENAI;
}

// Final URL, body, headers, and current request headers are normalized together.
finalizeOutboundRequest({ url, headers, transformedBody, credentials, model, targetFormat }) {
const runtimeTransport = credentials?.runtimeTransport;
const featurePolicy = runtimeTransport != null
? runtimeTransport.quirks?.anthropicBetaFeatures
: this.config?.quirks?.anthropicBetaFeatures;
return finalizeAnthropicOutboundRequest({
url,
headers,
transformedBody,
credentials,
provider: this.provider,
targetFormat: targetFormat || this.getOutboundFormat(model, credentials),
featurePolicy,
});
}

shouldRetry(status, urlIndex) {
return status === HTTP_STATUS.RATE_LIMITED && urlIndex + 1 < this.getFallbackCount();
}
Expand Down Expand Up @@ -138,12 +164,27 @@ export class BaseExecutor {
};

for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
const url = this.buildUrl(model, stream, urlIndex, credentials);
const transformedBody = this.transformRequest(model, body, stream, credentials);
const headers = this.buildHeaders(credentials, stream, url);
let url = this.buildUrl(model, stream, urlIndex, credentials);
let transformedBody = this.transformRequest(model, body, stream, credentials);
let headers = this.buildHeaders(credentials, stream, url);
const requestFormat = this.getOutboundFormat(model, credentials);
if (transformedBody?.thinking?.display === "summarized") {
removeBetaFlag(headers, "redact-thinking-2026-02-12");
}
const finalized = this.finalizeOutboundRequest({
url,
headers,
transformedBody,
credentials,
model,
stream,
targetFormat: requestFormat,
});
if (finalized) {
url = finalized.url ?? url;
headers = finalized.headers ?? headers;
transformedBody = finalized.transformedBody ?? transformedBody;
}

if (!retryAttemptsByUrl[urlIndex]) retryAttemptsByUrl[urlIndex] = 0;

Expand Down Expand Up @@ -176,7 +217,7 @@ export class BaseExecutor {
continue;
}

return { response, url, headers, transformedBody };
return { response, url, headers, transformedBody, requestFormat };
} catch (error) {
clearTimeout(connectTimer);
lastError = error;
Expand Down
65 changes: 41 additions & 24 deletions open-sse/executors/codex.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
refreshProviderCredentials,
shouldRefreshCredentials,
} from "../services/oauthCredentialManager.js";
import { normalizeResponsesInput } from "../translator/formats/responsesApi.js";
import { normalizeResponsesInput, normalizeStatelessResponseInput } from "../translator/formats/responsesApi.js";
import { fetchImageAsBase64 } from "../translator/concerns/image.js";
import { getModelUpstreamId } from "../config/providerModels.js";
import { DEFAULT_RETRY_CONFIG, HTTP_STATUS, resolveRetryEntry } from "../config/runtimeConfig.js";
Expand All @@ -24,9 +24,6 @@ const CODEX_SSE_USER_OUTPUT_PATTERNS = [
const CODEX_SSE_PEEK_BYTES = 256 * 1024;
const CODEX_MODEL_CAPACITY_MESSAGE = "Selected model is at capacity. Please try a different model.";

// Server-generated item id prefixes that Codex /responses cannot resolve when store=false
const SERVER_ID_PATTERN = /^(rs|fc|resp|msg)_/;

// Hosted tool types that Codex/OpenAI Responses executes server-side
const CODEX_HOSTED_TOOL_TYPES = new Set([
"image_generation", "web_search", "web_search_preview", "file_search",
Expand Down Expand Up @@ -54,19 +51,31 @@ function convertSystemToDeveloperRole(body) {
}
}

// Strip invalid or stored item IDs before sending a store=false request.
function stripStoredItemReferences(body) {
if (!Array.isArray(body.input)) return;
body.input = body.input.filter((item) => {
if (typeof item === "string" && SERVER_ID_PATTERN.test(item)) return false;
if (item && typeof item === "object" && !Array.isArray(item)) {
if (item.type === "item_reference") return false;
// function_call.id is optional input metadata; call_id carries tool-result correlation.
if (item.type === "function_call") delete item.id;
if (typeof item.id === "string" && SERVER_ID_PATTERN.test(item.id)) delete item.id;
}
return true;
});
function cloneCodexRequestBody(body) {
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
const cloned = { ...body };
if (Array.isArray(body.input)) {
cloned.input = body.input.map((item) => {
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
return {
...item,
...(Array.isArray(item.content)
? { content: item.content.map((part) => (
part && typeof part === "object" && !Array.isArray(part) ? { ...part } : part
)) }
: {}),
};
});
}
if (Array.isArray(body.tools)) {
cloned.tools = body.tools.map((tool) => (
tool && typeof tool === "object" && !Array.isArray(tool) ? { ...tool } : tool
));
}
if (body.reasoning && typeof body.reasoning === "object" && !Array.isArray(body.reasoning)) {
cloned.reasoning = { ...body.reasoning };
}
return cloned;
}

// Flatten Chat-Completions tool shape into Responses flat format + filter unsupported tools
Expand Down Expand Up @@ -253,15 +262,16 @@ export class CodexExecutor extends BaseExecutor {
}

async execute(args) {
const imgCount = Array.isArray(args.body?.input) ? args.body.input.reduce((n, it) => n + (Array.isArray(it.content) ? it.content.filter(c => c.type === "image_url").length : 0), 0) : 0;
const inputLen = Array.isArray(args.body?.input) ? args.body.input.length : 0;
const requestArgs = { ...args, body: cloneCodexRequestBody(args.body) };
const imgCount = Array.isArray(requestArgs.body?.input) ? requestArgs.body.input.reduce((n, it) => n + (Array.isArray(it.content) ? it.content.filter(c => c.type === "image_url").length : 0), 0) : 0;
const inputLen = Array.isArray(requestArgs.body?.input) ? requestArgs.body.input.length : 0;
dbg("CODEX", `execute start | inputItems=${inputLen} | images=${imgCount} | sessionId=${this._currentSessionId || "pending"}`);
if (imgCount > 0) {
const t0 = Date.now();
await this.prefetchImages(args.body);
await this.prefetchImages(requestArgs.body);
dbg("CODEX", `prefetchImages done | ${Date.now() - t0}ms`);
} else {
await this.prefetchImages(args.body);
await this.prefetchImages(requestArgs.body);
}

// Retry loop for SSE-level overloaded errors (200 OK body contains event: error)
Expand All @@ -270,7 +280,7 @@ export class CodexExecutor extends BaseExecutor {
const { attempts, delayMs } = resolveRetryEntry(retryConfig[503]);
let attempt = 0;
while (true) {
const result = await super.execute(args);
const result = await super.execute(requestArgs);
const peek = await this._peekSseTransientError(result.response);
if (!peek.matched) {
// Replace body with re-assembled stream (prefix bytes already read + rest)
Expand Down Expand Up @@ -388,6 +398,7 @@ export class CodexExecutor extends BaseExecutor {
* Image fetching is handled separately in prefetchImages() so this stays sync.
*/
transformRequest(model, body, stream, credentials) {
body = cloneCodexRequestBody(body);
this._isCompact = !!body._compact;
delete body._compact;
// Resolve conversation-stable session_id (priority: body → assistant-text → workspace → machine)
Expand All @@ -403,8 +414,14 @@ export class CodexExecutor extends BaseExecutor {

// Keep system prompts in body.input as role=developer so they stay in the cacheable prefix
convertSystemToDeveloperRole(body);
// Strip invalid function-call IDs and stored references that Codex cannot resolve with store=false
stripStoredItemReferences(body);
// Strip optional tool item IDs and stored references that store=false cannot resolve.
const normalizedInput = normalizeStatelessResponseInput(body.input);
body.input = normalizedInput.input;
const strippedIds = normalizedInput.strippedIds;
if (Object.keys(strippedIds).length > 0) {
const counts = Object.entries(strippedIds).map(([type, count]) => `${type}=${count}`).join(" ");
dbg("CODEX", `normalized stateless item ids | ${counts}`);
}
// Flatten function tools + drop unsupported types
normalizeCodexTools(body);

Expand Down
41 changes: 1 addition & 40 deletions open-sse/executors/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,6 @@ const HEADER_HOOKS = {
if (!cached) return;
for (const lcKey of Object.keys(cached)) {
const titleKey = lcKey.replace(/(^|-)([a-z])/g, (_, sep, ch) => sep + ch.toUpperCase());
if (lcKey === "anthropic-beta") {
const staticBetaStr = h[titleKey] || h[lcKey] || "";
const flags = new Set(staticBetaStr.split(",").map(f => f.trim()).filter(Boolean));
for (const f of cached[lcKey].split(",").map(f => f.trim()).filter(Boolean)) flags.add(f);
cached[lcKey] = Array.from(flags).join(",");
}
if (titleKey !== lcKey && h[titleKey] !== undefined) delete h[titleKey];
}
Object.assign(h, cached);
Expand Down Expand Up @@ -83,7 +77,7 @@ export class DefaultExecutor extends BaseExecutor {
}

transformRequest(model, body) {
const transformed = this.applyJsonSchemaFallback(body);
let transformed = this.applyJsonSchemaFallback(body);

if (transformed && typeof transformed === "object") {
// quirk: some openai-compatible providers reject Anthropic's client_metadata field
Expand Down Expand Up @@ -169,39 +163,6 @@ export class DefaultExecutor extends BaseExecutor {
for (const hook of desc.hooks || []) HEADER_HOOKS[hook]?.(headers, credentials);
applyAuth(headers, desc, credentials);

// Strip first-party Claude Code identity headers for non-Anthropic anthropic-compatible upstreams
if (this.provider?.startsWith?.("anthropic-compatible-")) {
const baseUrl = credentials?.providerSpecificData?.baseUrl || "";
const isOfficialAnthropic = baseUrl === "" || baseUrl.includes("api.anthropic.com");
if (!isOfficialAnthropic) {
// Some third-party Anthropic-compatible gateways require Bearer auth in
// addition to x-api-key. Send both (x-api-key already set above) so
// gateways that read either header succeed.
if (credentials.apiKey && !headers["Authorization"]) {
headers["Authorization"] = `Bearer ${credentials.apiKey}`;
}
delete headers["anthropic-dangerous-direct-browser-access"];
delete headers["Anthropic-Dangerous-Direct-Browser-Access"];
delete headers["x-app"];
delete headers["X-App"];
// Strip claude-code-20250219 from Anthropic-Beta / anthropic-beta
for (const betaKey of ["anthropic-beta", "Anthropic-Beta"]) {
if (headers[betaKey]) {
const filtered = headers[betaKey]
.split(",")
.map(s => s.trim())
.filter(f => f && f !== "claude-code-20250219")
.join(",");
if (filtered) {
headers[betaKey] = filtered;
} else {
delete headers[betaKey];
}
}
}
}
}

if (stream) headers["Accept"] = "text/event-stream";
return headers;
}
Expand Down
Loading