Prepare protocol compatibility release

This commit is contained in:
cita-777
2026-05-08 00:32:50 +08:00
28 changed files with 2205 additions and 108 deletions
+194 -55
View File
@@ -112,10 +112,12 @@ describe('chat proxy stream behavior', () => {
beforeAll(async () => {
const { chatProxyRoute, claudeMessagesProxyRoute } = await import('./chat.js');
const { responsesProxyRoute } = await import('./responses.js');
const { searchProxyRoute } = await import('./search.js');
app = Fastify();
await app.register(chatProxyRoute);
await app.register(claudeMessagesProxyRoute);
await app.register(responsesProxyRoute);
await app.register(searchProxyRoute);
});
beforeEach(() => {
@@ -156,6 +158,7 @@ describe('chat proxy stream behavior', () => {
filter: [],
};
(config as any).disableCrossProtocolFallback = false;
(config as any).openAiServiceTierRules = undefined;
config.proxyEmptyContentFailEnabled = false;
config.proxyErrorKeywords = [];
});
@@ -193,7 +196,7 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.statusCode, response.body).toBe(200);
expect(response.headers['content-type']).toContain('text/event-stream');
expect(response.body).toContain('data: ');
expect(response.body).toContain('"chat.completion.chunk"');
@@ -233,7 +236,7 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.statusCode, response.body).toBe(200);
expect(response.json()?.choices?.[0]?.message?.content).toBe('你好,来自 zstd 非流式响应');
});
@@ -268,7 +271,7 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.statusCode, response.body).toBe(200);
expect(response.headers['content-type']).toContain('text/event-stream');
expect(response.body).toContain('"chat.completion.chunk"');
expect(response.body).toContain('你好,来自 zstd 流式回退');
@@ -300,7 +303,7 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.statusCode, response.body).toBe(200);
expect(response.headers['content-type']).toContain('text/event-stream');
expect(response.body).toContain('"chat.completion.chunk"');
expect(response.body).toContain('你好,来自 zstd 原生 SSE');
@@ -513,7 +516,7 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.statusCode, response.body).toBe(200);
expect(response.headers['content-type']).toContain('text/event-stream');
expect(response.body).toContain('visible answer despite zero output usage');
expect(response.body).toContain('data: [DONE]');
@@ -563,7 +566,7 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.statusCode, response.body).toBe(200);
expect(response.headers['content-type']).toContain('text/event-stream');
expect(response.headers['cache-control']).toContain('no-transform');
expect(response.headers['x-accel-buffering']).toBe('no');
@@ -600,7 +603,7 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.statusCode, response.body).toBe(200);
expect(response.body).toContain('"reasoning_content":"plan quietly"');
expect(response.body).toContain('"content":"visible answer"');
expect(response.body).not.toContain('<think>');
@@ -639,7 +642,7 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.statusCode, response.body).toBe(200);
expect(response.body).toContain('"reasoning_content":"plan "');
expect(response.body).toContain('"reasoning_content":"quietly"');
expect(response.body).toContain('"content":"visible "');
@@ -710,7 +713,7 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.statusCode, response.body).toBe(200);
expect(response.headers['content-type']).toContain('text/event-stream');
expect(response.body).toContain('"chat.completion.chunk"');
expect(response.body).toContain('"delta":{"content":"hello"}');
@@ -777,7 +780,7 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.statusCode, response.body).toBe(200);
const body = response.json();
expect(body.type).toBe('message');
expect(body.role).toBe('assistant');
@@ -815,7 +818,7 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.statusCode, response.body).toBe(200);
expect(response.headers['content-type']).toContain('text/event-stream');
expect(response.body).toContain('event: message_start');
expect(response.body).toContain('event: content_block_delta');
@@ -2018,7 +2021,7 @@ describe('chat proxy stream behavior', () => {
});
});
it('preserves sub2api responses semantics across compatibility retries without using a strict field-dropping body', async () => {
it('rejects external HTTP previous_response_id before sub2api compatibility retries', async () => {
selectChannelMock.mockReturnValue({
channel: { id: 11, routeId: 22 },
site: { name: 'sub2api-site', url: 'https://sub2api.example.com', platform: 'sub2api' },
@@ -2076,48 +2079,9 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(fetchMock).toHaveBeenCalledTimes(2);
const [, firstOptions] = fetchMock.mock.calls[0] as [string, any];
const [, secondOptions] = fetchMock.mock.calls[1] as [string, any];
const firstBody = JSON.parse(firstOptions.body);
const secondBody = JSON.parse(secondOptions.body);
expect(firstOptions.headers.accept).toBe('text/event-stream');
expect(secondOptions.headers.accept).toBe('text/event-stream');
expect(firstOptions.headers['openai-beta']).toBe('responses-2025-03-11');
expect(secondOptions.headers['openai-beta']).toBe('responses-2025-03-11');
expect(firstOptions.headers.originator).toBe('codex_cli_rs');
expect(secondOptions.headers.originator).toBe('codex_cli_rs');
expect(secondOptions.headers['accept-language']).toBe('zh-CN');
expect(secondOptions.headers.session_id).toBe('session-123');
expect(secondOptions.headers.conversation_id).toBe('conversation-123');
expect(secondOptions.headers['x-codex-turn-state']).toBe('turn-state');
expect(secondOptions.headers['x-codex-turn-metadata']).toBe('turn-metadata');
expect(secondOptions.headers['x-stainless-lang']).toBeUndefined();
expect(secondOptions.headers.version).toBeUndefined();
expect(secondOptions.headers['user-agent']).toBe('lightMyRequest');
expect(firstBody).toMatchObject({
stream: true,
store: false,
previous_response_id: 'resp_prev_1',
include: ['reasoning.encrypted_content'],
reasoning: { effort: 'high' },
prompt_cache_key: 'cache-key-1',
service_tier: 'priority',
background: true,
});
expect(secondBody).toMatchObject({
stream: true,
store: false,
previous_response_id: 'resp_prev_1',
include: ['reasoning.encrypted_content'],
reasoning: { effort: 'high' },
prompt_cache_key: 'cache-key-1',
service_tier: 'priority',
background: true,
});
expect(response.statusCode).toBe(400);
expect(response.json().error.message).toContain('HTTP /v1/responses');
expect(fetchMock).not.toHaveBeenCalled();
});
it('retries generic 400 /v1/responses with minimal headers for strict compatibility fallback', async () => {
@@ -2863,7 +2827,7 @@ describe('chat proxy stream behavior', () => {
},
});
expect(response.statusCode).toBe(200);
expect(response.statusCode, response.body).toBe(200);
expect(fetchMock).toHaveBeenCalledTimes(1);
const [targetUrl] = fetchMock.mock.calls[0] as [string, any];
expect(targetUrl).toContain('/v1/responses');
@@ -4697,6 +4661,181 @@ describe('chat proxy stream behavior', () => {
expect(forwardedBody.tool_choice).toEqual({ type: 'function', name: 'Glob' });
});
it('forwards legacy functions/function_call when /v1/chat/completions is routed to /v1/responses upstream', async () => {
fetchModelPricingCatalogMock.mockResolvedValue({
models: [
{
modelName: 'upstream-gpt',
supportedEndpointTypes: ['/v1/responses'],
},
],
groupRatio: {},
});
fetchMock.mockResolvedValue(new Response(JSON.stringify({
id: 'resp_legacy_chat',
object: 'response',
model: 'upstream-gpt',
status: 'completed',
output: [],
output_text: 'ok',
usage: { input_tokens: 3, output_tokens: 2, total_tokens: 5 },
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}));
const response = await app.inject({
method: 'POST',
url: '/v1/chat/completions',
payload: {
model: 'gpt-4o-mini',
stream: false,
functions: [{
name: 'legacy_lookup',
parameters: { type: 'object' },
}],
function_call: { name: 'legacy_lookup' },
messages: [
{
role: 'function',
name: 'legacy_lookup',
content: '{"ok":true}',
},
{
role: 'user',
content: 'continue',
},
],
},
});
expect(response.statusCode).toBe(200);
const [targetUrl, options] = fetchMock.mock.calls[0] as [string, any];
expect(targetUrl).toContain('/v1/responses');
const forwardedBody = JSON.parse(options.body);
expect(forwardedBody.tools).toEqual([
{ type: 'function', name: 'legacy_lookup', parameters: { type: 'object' } },
]);
expect(forwardedBody.tool_choice).toEqual({ type: 'function', name: 'legacy_lookup' });
expect(forwardedBody.input).toContainEqual({
type: 'function_call_output',
call_id: 'legacy_lookup',
output: '{"ok":true}',
});
});
it('returns synthetic Anthropic web_search server tool results without adding a new search dependency', async () => {
fetchMock.mockResolvedValue(new Response(JSON.stringify({
object: 'search.result',
data: [{ title: 'Metapi', url: 'https://example.com/metapi' }],
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}));
const response = await app.inject({
method: 'POST',
url: '/v1/messages',
payload: {
model: 'claude-opus-4-6',
max_tokens: 256,
stream: false,
tools: [{ type: 'web_search_20250305', max_uses: 2 }],
messages: [{ role: 'user', content: 'metapi protocol compatibility' }],
},
});
expect(response.statusCode, response.body).toBe(200);
expect(fetchMock).toHaveBeenCalledTimes(1);
const [targetUrl, options] = fetchMock.mock.calls[0] as [string, any];
expect(targetUrl).toBe('https://upstream.example.com/v1/search');
expect(JSON.parse(options.body)).toMatchObject({
query: 'metapi protocol compatibility',
max_results: 2,
});
const body = response.json();
expect(body.content?.[0]).toMatchObject({
type: 'server_tool_use',
name: 'web_search',
});
expect(body.content?.[1]).toMatchObject({
type: 'web_search_tool_result',
});
});
it('streams synthetic Anthropic web_search server tool results over SSE', async () => {
fetchMock.mockResolvedValue(new Response(JSON.stringify({
object: 'search.result',
data: [{ title: 'Metapi SSE' }],
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}));
const response = await app.inject({
method: 'POST',
url: '/v1/messages',
payload: {
model: 'claude-opus-4-6',
max_tokens: 256,
stream: true,
tools: [{ type: 'web_search_20250305' }],
messages: [{ role: 'user', content: 'metapi sse search' }],
},
});
expect(response.statusCode, response.body).toBe(200);
expect(response.headers['content-type']).toContain('text/event-stream');
expect(response.body).toContain('"type":"server_tool_use"');
expect(response.body).toContain('"type":"web_search_tool_result"');
expect(response.body).toContain('message_stop');
});
it('returns synthetic Responses web_search results without touching completions upstreams', async () => {
fetchMock.mockResolvedValue(new Response(JSON.stringify({
object: 'search.result',
data: [{ title: 'Metapi Responses', url: 'https://example.com/responses' }],
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}));
const response = await app.inject({
method: 'POST',
url: '/v1/responses',
payload: {
model: 'gpt-4.1',
stream: false,
tools: [{ type: 'web_search', name: 'web_search', max_results: 3 }],
input: 'metapi responses web search',
},
});
expect(response.statusCode, response.body).toBe(200);
expect(fetchMock).toHaveBeenCalledTimes(1);
const [targetUrl, options] = fetchMock.mock.calls[0] as [string, any];
expect(targetUrl).toBe('https://upstream.example.com/v1/search');
expect(JSON.parse(options.body)).toMatchObject({
query: 'metapi responses web search',
max_results: 3,
});
const body = response.json();
expect(body.object).toBe('response');
expect(body.output?.[0]).toMatchObject({
type: 'web_search_call',
status: 'completed',
action: {
type: 'search',
query: 'metapi responses web search',
},
});
expect(body.output?.[1]?.content?.[0]?.text).toContain('Metapi Responses');
});
it('routes gemini platform to OpenAI-compatible upstream endpoint path', async () => {
selectChannelMock.mockReturnValue({
channel: { id: 11, routeId: 22 },
@@ -190,6 +190,7 @@ describe('responses proxy codex oauth refresh', () => {
config.proxyStickySessionEnabled = originalProxyStickySessionEnabled;
config.proxySessionChannelConcurrencyLimit = originalProxySessionChannelConcurrencyLimit;
config.proxySessionChannelQueueWaitMs = originalProxySessionChannelQueueWaitMs;
(config as any).openAiServiceTierRules = undefined;
fetchMock.mockReset();
selectChannelMock.mockReset();
selectNextChannelMock.mockReset();
@@ -685,6 +686,137 @@ describe('responses proxy codex oauth refresh', () => {
]);
});
it('rejects external HTTP previous_response_id without touching upstream', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/responses',
payload: {
model: 'gpt-5.2-codex',
previous_response_id: 'msg_wrong_1',
input: 'hello',
},
});
expect(response.statusCode).toBe(400);
expect(response.json().error.message).toContain('msg_*');
expect(response.json().error.message).toContain('HTTP /v1/responses');
expect(fetchMock).not.toHaveBeenCalled();
});
it('rejects orphan external HTTP function_call_output but keeps codex session inference allowed', async () => {
const rejected = await app.inject({
method: 'POST',
url: '/v1/responses',
payload: {
model: 'gpt-5.2-codex',
input: [
{
type: 'function_call_output',
call_id: 'call_orphan',
output: '{"ok":true}',
},
],
},
});
expect(rejected.statusCode).toBe(400);
expect(rejected.json().error.message).toContain('Responses WebSocket v2');
expect(fetchMock).not.toHaveBeenCalled();
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
id: 'resp_codex_preflight_ok',
object: 'response',
model: 'gpt-5.2-codex',
status: 'completed',
output_text: 'accepted',
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}));
const allowed = await app.inject({
method: 'POST',
url: '/v1/responses',
headers: {
session_id: 'session-preflight-codex',
'user-agent': 'CodexClient/1.0',
},
payload: {
model: 'gpt-5.2-codex',
input: [
{
type: 'function_call_output',
call_id: 'call_codex',
output: '{"ok":true}',
},
],
},
});
expect(allowed.statusCode).toBe(200);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('filters HTTP service_tier by selected account policy before forwarding upstream', async () => {
(config as any).openAiServiceTierRules = [{
action: 'filter',
tiers: ['priority'],
platforms: ['codex'],
accountTypes: ['plus'],
}];
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
id: 'resp_codex_tier_filtered',
object: 'response',
model: 'gpt-5.2-codex',
status: 'completed',
output_text: 'tier filtered',
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}));
const response = await app.inject({
method: 'POST',
url: '/v1/responses',
payload: {
model: 'gpt-5.2-codex',
service_tier: 'fast',
input: 'hello',
},
});
expect(response.statusCode).toBe(200);
expect(fetchMock).toHaveBeenCalledTimes(1);
const [, options] = fetchMock.mock.calls[0] as [string, any];
const forwardedBody = JSON.parse(options.body);
expect(forwardedBody.service_tier).toBeUndefined();
});
it('blocks HTTP service_tier by selected account policy without touching upstream', async () => {
(config as any).openAiServiceTierRules = [{
action: 'block',
tiers: ['priority'],
platforms: ['codex'],
accountTypes: ['plus'],
}];
const response = await app.inject({
method: 'POST',
url: '/v1/responses',
payload: {
model: 'gpt-5.2-codex',
service_tier: 'fast',
input: 'hello',
},
});
expect(response.statusCode).toBe(400);
expect(response.json().error.message).toContain('service_tier');
expect(fetchMock).not.toHaveBeenCalled();
});
it('infers previous_response_id for codex tool-output follow-up turns when the client only sends conversation_id', async () => {
fetchMock
.mockResolvedValueOnce(new Response(JSON.stringify({
@@ -927,17 +1059,9 @@ describe('responses proxy codex oauth refresh', () => {
},
});
expect(response.statusCode).toBe(200);
expect(fetchMock).toHaveBeenCalledTimes(2);
const [, firstOptions] = fetchMock.mock.calls[0] as [string, any];
const [, secondOptions] = fetchMock.mock.calls[1] as [string, any];
const firstBody = JSON.parse(firstOptions.body);
const secondBody = JSON.parse(secondOptions.body);
expect(firstBody.previous_response_id).toBe('resp_stale');
expect(secondBody.previous_response_id).toBeUndefined();
expect(secondBody.input).toEqual(firstBody.input);
expect(response.statusCode).toBe(400);
expect(response.json().error.message).toContain('HTTP /v1/responses');
expect(fetchMock).not.toHaveBeenCalled();
});
it('strips generic downstream headers before forwarding codex responses upstream', async () => {
@@ -395,6 +395,7 @@ describe('responses websocket transport', () => {
upstreamRequests = [];
(config as any).codexResponsesWebsocketBeta = originalCodexResponsesWebsocketBeta;
(config as any).codexUpstreamWebsocketEnabled = true;
(config as any).openAiServiceTierRules = undefined;
rejectedUpgradeStatus = 426;
rejectedUpgradeStatusText = 'Upgrade Required';
rejectedUpgradeBody = 'Upgrade Required';
@@ -1556,6 +1557,79 @@ describe('responses websocket transport', () => {
});
});
it('applies service_tier policy to websocket frames before upstream dispatch', async () => {
(config as any).openAiServiceTierRules = [{
action: 'filter',
tiers: ['priority'],
platforms: ['openai'],
}];
const selectedChannel = createSelectedChannel({
sitePlatform: 'openai',
actualModel: 'gpt-4.1',
});
selectChannelMock.mockReturnValue(selectedChannel);
previewSelectedChannelMock.mockResolvedValue(selectedChannel);
fetchMock.mockResolvedValueOnce(createSseResponse([
'event: response.completed\n',
'data: {"type":"response.completed","response":{"id":"resp_ws_tier","model":"gpt-4.1","status":"completed","output":[],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}\n\n',
'data: [DONE]\n\n',
]));
const socket = createClientSocket(baseUrl);
await waitForSocketOpen(socket);
const responsePromise = waitForSocketMessageMatching(
socket,
(message) => message?.type === 'response.completed',
);
socket.send(JSON.stringify({
type: 'response.create',
model: 'gpt-4.1',
service_tier: 'fast',
input: [],
}));
await responsePromise;
socket.close();
const [, options] = fetchMock.mock.calls[0] as [string, RequestInit];
const forwardedBody = JSON.parse(String(options.body));
expect(forwardedBody.service_tier).toBeUndefined();
(config as any).openAiServiceTierRules = undefined;
});
it('blocks websocket service_tier before upstream dispatch', async () => {
(config as any).openAiServiceTierRules = [{
action: 'block',
tiers: ['priority'],
platforms: ['openai'],
}];
const selectedChannel = createSelectedChannel({
sitePlatform: 'openai',
actualModel: 'gpt-4.1',
});
selectChannelMock.mockReturnValue(selectedChannel);
previewSelectedChannelMock.mockResolvedValue(selectedChannel);
const socket = createClientSocket(baseUrl);
await waitForSocketOpen(socket);
const errorPromise = waitForSocketMessageMatching(
socket,
(message) => message?.type === 'error',
);
socket.send(JSON.stringify({
type: 'response.create',
model: 'gpt-4.1',
service_tier: 'fast',
input: [],
}));
const message = await errorPromise;
socket.close();
expect(message.status).toBe(400);
expect(message.error.message).toContain('service_tier');
expect(fetchMock).not.toHaveBeenCalled();
(config as any).openAiServiceTierRules = undefined;
});
it('keeps streamed output items for follow-up turns when the terminal HTTP fallback payload has an empty output array', async () => {
const selectedChannel = createSelectedChannel({
sitePlatform: 'openai',
+54 -1
View File
@@ -14,9 +14,11 @@ import {
import { runWithSiteApiEndpointPool, SiteApiEndpointRequestError } from '../../services/siteApiEndpointService.js';
import { tokenRouter } from '../../services/tokenRouter.js';
import { buildOauthProviderHeaders } from '../../services/oauth/service.js';
import { getOauthInfoFromAccount } from '../../services/oauth/oauthAccount.js';
import { openAiResponsesTransformer } from '../../transformers/openai/responses/index.js';
import { buildUpstreamEndpointRequest } from './upstreamEndpoint.js';
import { config } from '../../config.js';
import { applyOpenAiServiceTierPolicy } from '../../proxy-core/serviceTierPolicy.js';
const installedApps = new WeakSet<FastifyInstance>();
const WS_TURN_STATE_HEADER = 'x-codex-turn-state';
@@ -43,6 +45,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function getServiceTierPolicyRules(): unknown {
return (config as typeof config & { openAiServiceTierRules?: unknown }).openAiServiceTierRules;
}
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
@@ -592,6 +598,24 @@ async function handleResponsesWebsocketConnection(
writeResponsesWebsocketError(socket, 403, 'model is not allowed for this downstream key');
return;
}
const serviceTierPolicy = applyOpenAiServiceTierPolicy({
body: parsed,
context: {
requestedModel: requestModel,
},
rules: getServiceTierPolicyRules(),
});
if (!serviceTierPolicy.ok) {
writeResponsesWebsocketError(
socket,
serviceTierPolicy.statusCode,
serviceTierPolicy.payload.error.message,
serviceTierPolicy.payload,
);
return;
}
parsed.service_tier = serviceTierPolicy.body.service_tier;
if (serviceTierPolicy.body.service_tier === undefined) delete parsed.service_tier;
const supportsIncrementalInput = selectedChannelSupportsIncrementalInput(selectedChannel, requestModel)
|| await supportsResponsesWebsocketIncrementalInput(parsed, lastRequest, authContext);
const shouldHandleLocalPrewarm = shouldHandleResponsesWebsocketPrewarmLocally(
@@ -614,8 +638,8 @@ async function handleResponsesWebsocketConnection(
await consumeManagedKeyRequest(authContext.key.id);
}
lastRequest = normalized.nextRequestSnapshot;
if (shouldHandleLocalPrewarm) {
lastRequest = normalized.nextRequestSnapshot;
lastResponseOutput = [];
for (const payload of synthesizePrewarmResponsePayloads(normalized.request)) {
socket.send(JSON.stringify(payload));
@@ -629,6 +653,35 @@ async function handleResponsesWebsocketConnection(
: null;
}
const selectedServiceTierPolicy = applyOpenAiServiceTierPolicy({
body: normalized.request,
context: {
requestedModel: requestModel,
actualModel: asTrimmedString(selectedChannel?.actualModel),
sitePlatform: asTrimmedString(selectedChannel?.site?.platform),
accountType: getOauthInfoFromAccount(selectedChannel?.account)?.planType,
},
rules: getServiceTierPolicyRules(),
});
if (!selectedServiceTierPolicy.ok) {
writeResponsesWebsocketError(
socket,
selectedServiceTierPolicy.statusCode,
selectedServiceTierPolicy.payload.error.message,
selectedServiceTierPolicy.payload,
);
return;
}
normalized.request = selectedServiceTierPolicy.body;
normalized.nextRequestSnapshot = {
...normalized.nextRequestSnapshot,
service_tier: selectedServiceTierPolicy.body.service_tier,
};
if (selectedServiceTierPolicy.body.service_tier === undefined) {
delete normalized.nextRequestSnapshot.service_tier;
}
lastRequest = normalized.nextRequestSnapshot;
const codexWebsocketChannel = selectedChannelSupportsCodexWebsocketTransport(selectedChannel, requestModel)
? selectedChannel
: null;