Refactor proxy-core surfaces and transformer boundaries (#163)
* refactor: add canonical request contracts * test: harden transformer boundary contracts * refactor: adapt protocol transformers to canonical contract * refactor: add cli profile registry * refactor: extract provider profiles * refactor: add conductor and provider executors * refactor: add models surface * refactor: migrate responses surface * refactor: migrate chat surface * refactor: migrate gemini surface * refactor: migrate file surfaces * test: align runtime executor undici mock * fix: preserve compact SSE closeout payloads * feat: support all-api-hub v2 backup imports * fix: aggregate codex oauth SSE final payloads * fix: harden responses SSE failure handling
This commit is contained in:
@@ -11,6 +11,66 @@ function parseSsePayloads(lines: string[]): Array<Record<string, unknown>> {
|
||||
}
|
||||
|
||||
describe('openAiChatTransformer.inbound', () => {
|
||||
it('parses chat requests into canonical envelopes', () => {
|
||||
const result = openAiChatTransformer.parseRequest({
|
||||
model: 'gpt-5',
|
||||
stream: true,
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
prompt_cache_key: 'cache-key',
|
||||
reasoning_effort: 'high',
|
||||
reasoning_budget: 1024,
|
||||
});
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.value).toMatchObject({
|
||||
operation: 'generate',
|
||||
surface: 'openai-chat',
|
||||
cliProfile: 'generic',
|
||||
requestedModel: 'gpt-5',
|
||||
stream: true,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ type: 'text', text: 'hello' }],
|
||||
},
|
||||
],
|
||||
continuation: {
|
||||
promptCacheKey: 'cache-key',
|
||||
},
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
budgetTokens: 1024,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('builds chat requests from canonical envelopes', () => {
|
||||
const body = openAiChatTransformer.buildProtocolRequest({
|
||||
operation: 'generate',
|
||||
surface: 'openai-chat',
|
||||
cliProfile: 'codex',
|
||||
requestedModel: 'gpt-5',
|
||||
stream: true,
|
||||
messages: [{ role: 'user', parts: [{ type: 'text', text: 'hello' }] }],
|
||||
reasoning: {
|
||||
effort: 'medium',
|
||||
budgetTokens: 512,
|
||||
},
|
||||
continuation: {
|
||||
promptCacheKey: 'cache-key',
|
||||
},
|
||||
});
|
||||
|
||||
expect(body).toMatchObject({
|
||||
model: 'gpt-5',
|
||||
stream: true,
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
prompt_cache_key: 'cache-key',
|
||||
reasoning_effort: 'medium',
|
||||
reasoning_budget: 512,
|
||||
});
|
||||
});
|
||||
|
||||
it('captures chat request metadata fields without changing upstream body', () => {
|
||||
const result = openAiChatTransformer.transformRequest({
|
||||
model: 'gpt-5',
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { canonicalRequestFromOpenAiBody, canonicalRequestToOpenAiChatBody } from '../../canonical/request.js';
|
||||
import type { CanonicalRequestEnvelope } from '../../canonical/types.js';
|
||||
import type { ProtocolBuildContext, ProtocolParseContext } from '../../contracts.js';
|
||||
import { type NormalizedFinalResponse, type NormalizedStreamEvent, type StreamTransformContext } from '../../shared/normalized.js';
|
||||
import { createChatEndpointStrategy } from '../../shared/chatEndpointStrategy.js';
|
||||
import { openAiChatInbound } from './inbound.js';
|
||||
@@ -28,6 +31,46 @@ export const openAiChatTransformer = {
|
||||
proxyStream: {
|
||||
createSession: createChatProxyStreamSession,
|
||||
},
|
||||
parseRequest(
|
||||
body: unknown,
|
||||
ctx?: ProtocolParseContext,
|
||||
): { value?: CanonicalRequestEnvelope; error?: { statusCode: number; payload: unknown } } {
|
||||
const parsed = openAiChatInbound.parse(body);
|
||||
if (parsed.error) {
|
||||
return { error: parsed.error };
|
||||
}
|
||||
if (!parsed.value) {
|
||||
return {
|
||||
error: {
|
||||
statusCode: 400,
|
||||
payload: {
|
||||
error: {
|
||||
message: 'invalid chat request',
|
||||
type: 'invalid_request_error',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: canonicalRequestFromOpenAiBody({
|
||||
body: parsed.value.parsed.upstreamBody,
|
||||
surface: 'openai-chat',
|
||||
cliProfile: ctx?.cliProfile,
|
||||
operation: ctx?.operation,
|
||||
metadata: ctx?.metadata,
|
||||
passthrough: ctx?.passthrough,
|
||||
continuation: ctx?.continuation,
|
||||
}),
|
||||
};
|
||||
},
|
||||
buildProtocolRequest(
|
||||
request: CanonicalRequestEnvelope,
|
||||
_ctx?: ProtocolBuildContext,
|
||||
): Record<string, unknown> {
|
||||
return canonicalRequestToOpenAiChatBody(request);
|
||||
},
|
||||
transformRequest(body: unknown): ReturnType<typeof openAiChatInbound.parse> {
|
||||
return openAiChatInbound.parse(body);
|
||||
},
|
||||
|
||||
@@ -3,6 +3,75 @@ import { describe, expect, it } from 'vitest';
|
||||
import { openAiResponsesTransformer } from './index.js';
|
||||
|
||||
describe('openAiResponsesTransformer.inbound', () => {
|
||||
it('parses responses requests into canonical envelopes', () => {
|
||||
const result = openAiResponsesTransformer.parseRequest({
|
||||
model: 'gpt-5',
|
||||
input: 'hello',
|
||||
previous_response_id: 'resp_prev_1',
|
||||
prompt_cache_key: 'cache-key',
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.value).toMatchObject({
|
||||
operation: 'generate',
|
||||
surface: 'openai-responses',
|
||||
cliProfile: 'generic',
|
||||
requestedModel: 'gpt-5',
|
||||
stream: false,
|
||||
continuation: {
|
||||
previousResponseId: 'resp_prev_1',
|
||||
promptCacheKey: 'cache-key',
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ type: 'text', text: 'hello' }],
|
||||
},
|
||||
],
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('builds responses requests from canonical envelopes', () => {
|
||||
const body = openAiResponsesTransformer.buildProtocolRequest({
|
||||
operation: 'generate',
|
||||
surface: 'openai-responses',
|
||||
cliProfile: 'codex',
|
||||
requestedModel: 'gpt-5',
|
||||
stream: true,
|
||||
messages: [{ role: 'user', parts: [{ type: 'text', text: 'hello' }] }],
|
||||
continuation: {
|
||||
previousResponseId: 'resp_prev_1',
|
||||
promptCacheKey: 'cache-key',
|
||||
},
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
},
|
||||
});
|
||||
|
||||
expect(body).toMatchObject({
|
||||
model: 'gpt-5',
|
||||
stream: true,
|
||||
previous_response_id: 'resp_prev_1',
|
||||
prompt_cache_key: 'cache-key',
|
||||
input: [
|
||||
{
|
||||
type: 'message',
|
||||
role: 'user',
|
||||
content: [{ type: 'input_text', text: 'hello' }],
|
||||
},
|
||||
],
|
||||
reasoning: {
|
||||
effort: 'high',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a protocol request envelope with a normalized responses body', () => {
|
||||
const result = openAiResponsesTransformer.transformRequest({
|
||||
model: 'gpt-5',
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { canonicalRequestFromOpenAiBody, canonicalRequestToOpenAiChatBody } from '../../canonical/request.js';
|
||||
import type { CanonicalRequestEnvelope } from '../../canonical/types.js';
|
||||
import type { ProtocolBuildContext, ProtocolParseContext } from '../../contracts.js';
|
||||
import { type StreamTransformContext } from '../../shared/normalized.js';
|
||||
import {
|
||||
convertOpenAiBodyToResponsesBody,
|
||||
@@ -61,6 +64,64 @@ export const openAiResponsesTransformer = {
|
||||
proxyStream: {
|
||||
createSession: createResponsesProxyStreamSession,
|
||||
},
|
||||
parseRequest(
|
||||
body: unknown,
|
||||
ctx?: ProtocolParseContext,
|
||||
): { value?: CanonicalRequestEnvelope; error?: { statusCode: number; payload: unknown } } {
|
||||
const parsed = openAiResponsesInbound.parse(body, {
|
||||
defaultEncryptedReasoningInclude: ctx?.defaultEncryptedReasoningInclude,
|
||||
});
|
||||
if (parsed.error) {
|
||||
return { error: parsed.error };
|
||||
}
|
||||
if (!parsed.value) {
|
||||
return {
|
||||
error: {
|
||||
statusCode: 400,
|
||||
payload: {
|
||||
error: {
|
||||
message: 'invalid responses request',
|
||||
type: 'invalid_request_error',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const responsesBody = parsed.value.parsed.normalizedBody;
|
||||
const openAiBody = convertResponsesBodyToOpenAiBody(
|
||||
responsesBody,
|
||||
typeof responsesBody.model === 'string' ? responsesBody.model : parsed.value.model,
|
||||
responsesBody.stream === true,
|
||||
{ defaultEncryptedReasoningInclude: ctx?.defaultEncryptedReasoningInclude },
|
||||
);
|
||||
|
||||
return {
|
||||
value: canonicalRequestFromOpenAiBody({
|
||||
body: openAiBody,
|
||||
surface: 'openai-responses',
|
||||
cliProfile: ctx?.cliProfile,
|
||||
operation: ctx?.operation,
|
||||
metadata: ctx?.metadata,
|
||||
passthrough: ctx?.passthrough,
|
||||
continuation: ctx?.continuation,
|
||||
}),
|
||||
};
|
||||
},
|
||||
buildProtocolRequest(
|
||||
request: CanonicalRequestEnvelope,
|
||||
_ctx?: ProtocolBuildContext,
|
||||
): Record<string, unknown> {
|
||||
const openAiBody = canonicalRequestToOpenAiChatBody(request);
|
||||
if (request.reasoning) {
|
||||
openAiBody.reasoning = {
|
||||
...(request.reasoning.effort ? { effort: request.reasoning.effort } : {}),
|
||||
...(request.reasoning.budgetTokens !== undefined ? { budget_tokens: request.reasoning.budgetTokens } : {}),
|
||||
...(request.reasoning.summary ? { summary: request.reasoning.summary } : {}),
|
||||
};
|
||||
}
|
||||
return convertOpenAiBodyToResponsesBody(openAiBody, request.requestedModel, request.stream);
|
||||
},
|
||||
transformRequest(
|
||||
body: unknown,
|
||||
options?: { defaultEncryptedReasoningInclude?: boolean },
|
||||
|
||||
Reference in New Issue
Block a user