forked from boos/BoosAPI
Close protocol compatibility review gaps
Review feedback found narrow compatibility regressions in the web-search shim, Responses tool detection, service tier accounting, and update-center Docker candidate actions. The fix keeps synthetic search routed through the dedicated search model, preserves Responses text compatibility, distinguishes absent service_tier from filtered service_tier, and evaluates recent Docker tags per candidate. Constraint: Review fixes must stay on dev first, then flow into the main-based release PR branch. Rejected: Leave Responses web_search simulation on the request model | it has the same search-route selection risk as Anthropic simulation. Confidence: high Scope-risk: narrow Tested: npx vitest run --root . src/server/proxy-core/responsesPreflight.test.ts src/server/proxy-core/serviceTierPolicy.test.ts src/server/routes/proxy/chat.stream.test.ts src/web/pages/settings/UpdateCenterSection.test.tsx Tested: npm run typecheck:server Tested: npm run typecheck:web Tested: npm run repo:drift-check Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { validateExternalResponsesHttpRequest } from './responsesPreflight.js';
|
||||
import {
|
||||
hasResponsesWebSearchOnlyRequest,
|
||||
validateExternalResponsesHttpRequest,
|
||||
} from './responsesPreflight.js';
|
||||
|
||||
describe('validateExternalResponsesHttpRequest', () => {
|
||||
it('rejects external HTTP previous_response_id and explains msg ids separately', () => {
|
||||
@@ -63,3 +66,21 @@ describe('validateExternalResponsesHttpRequest', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasResponsesWebSearchOnlyRequest', () => {
|
||||
it('recognizes the current web_search_preview tool alias', () => {
|
||||
expect(hasResponsesWebSearchOnlyRequest({
|
||||
model: 'gpt-5',
|
||||
tools: [{ type: 'web_search_preview_2025_03_11' }],
|
||||
input: 'metapi protocol compatibility',
|
||||
})).toBe(true);
|
||||
|
||||
expect(hasResponsesWebSearchOnlyRequest({
|
||||
model: 'gpt-5',
|
||||
tools: [
|
||||
{ type: 'web_search_preview_2025_03_11' },
|
||||
{ type: 'function', name: 'lookup' },
|
||||
],
|
||||
input: 'metapi protocol compatibility',
|
||||
})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,6 +120,7 @@ function isWebSearchToolRecord(tool: Record<string, unknown>): boolean {
|
||||
const name = asTrimmedString(tool.name).toLowerCase();
|
||||
return type === 'web_search'
|
||||
|| type === 'web_search_preview'
|
||||
|| type === 'web_search_preview_2025_03_11'
|
||||
|| type === 'web_search_20250305'
|
||||
|| type === 'google_search'
|
||||
|| name === 'web_search'
|
||||
|
||||
@@ -3,7 +3,15 @@ import { describe, expect, it } from 'vitest';
|
||||
import { applyOpenAiServiceTierPolicy } from './serviceTierPolicy.js';
|
||||
|
||||
describe('applyOpenAiServiceTierPolicy', () => {
|
||||
it('normalizes fast to priority and drops unknown or non-string tiers', () => {
|
||||
it('normalizes fast to priority, leaves absent tiers alone, and drops unknown or non-string tiers', () => {
|
||||
expect(applyOpenAiServiceTierPolicy({
|
||||
body: { model: 'gpt-5' },
|
||||
})).toMatchObject({
|
||||
ok: true,
|
||||
body: { model: 'gpt-5' },
|
||||
action: 'pass',
|
||||
});
|
||||
|
||||
expect(applyOpenAiServiceTierPolicy({
|
||||
body: { model: 'gpt-5', service_tier: ' fast ' },
|
||||
})).toMatchObject({
|
||||
@@ -73,4 +81,3 @@ describe('applyOpenAiServiceTierPolicy', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ export function applyOpenAiServiceTierPolicy(input: {
|
||||
context?: ServiceTierRuleContext;
|
||||
rules?: unknown;
|
||||
}): ServiceTierPolicyResult {
|
||||
const hasRequestedServiceTier = Object.prototype.hasOwnProperty.call(input.body, 'service_tier');
|
||||
const rawTier = input.body.service_tier;
|
||||
const serviceTier = normalizeServiceTier(rawTier);
|
||||
const next = { ...input.body };
|
||||
@@ -135,7 +136,7 @@ export function applyOpenAiServiceTierPolicy(input: {
|
||||
return {
|
||||
ok: true,
|
||||
body: next,
|
||||
action: 'filter',
|
||||
action: hasRequestedServiceTier ? 'filter' : 'pass',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,4 +161,3 @@ export function applyOpenAiServiceTierPolicy(input: {
|
||||
action: 'pass',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ function asTrimmedString(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
const SEARCH_SIMULATION_MODEL = '__search';
|
||||
|
||||
function findSearchTool(body: Record<string, unknown>): Record<string, unknown> | null {
|
||||
const tools = Array.isArray(body.tools) ? body.tools : [];
|
||||
for (const tool of tools) {
|
||||
@@ -23,6 +25,7 @@ function findSearchTool(body: Record<string, unknown>): Record<string, unknown>
|
||||
if (
|
||||
type === 'web_search'
|
||||
|| type === 'web_search_preview'
|
||||
|| type === 'web_search_preview_2025_03_11'
|
||||
|| type === 'web_search_20250305'
|
||||
|| type === 'google_search'
|
||||
|| name === 'web_search'
|
||||
@@ -129,6 +132,7 @@ function buildSyntheticResponsesPayload(input: {
|
||||
const responseId = `resp_web_search_${randomUUID()}`;
|
||||
const searchCallId = `ws_${randomUUID()}`;
|
||||
const results = normalizeSearchResults(input.searchPayload);
|
||||
const outputText = results.length > 0 ? JSON.stringify(results) : '[]';
|
||||
|
||||
return {
|
||||
id: responseId,
|
||||
@@ -136,6 +140,7 @@ function buildSyntheticResponsesPayload(input: {
|
||||
created_at: createdAt,
|
||||
model: asTrimmedString(input.body.model) || 'unknown',
|
||||
status: 'completed',
|
||||
output_text: outputText,
|
||||
output: [
|
||||
{
|
||||
id: searchCallId,
|
||||
@@ -153,9 +158,7 @@ function buildSyntheticResponsesPayload(input: {
|
||||
status: 'completed',
|
||||
content: [{
|
||||
type: 'output_text',
|
||||
text: results.length > 0
|
||||
? JSON.stringify(results)
|
||||
: '[]',
|
||||
text: outputText,
|
||||
}],
|
||||
},
|
||||
],
|
||||
@@ -193,7 +196,7 @@ async function sendAnthropicSearchSimulation(input: {
|
||||
app: input.app,
|
||||
request: input.request,
|
||||
query,
|
||||
model: asTrimmedString(input.body.model) || asTrimmedString(input.openAiBody.model) || '__search',
|
||||
model: SEARCH_SIMULATION_MODEL,
|
||||
maxResults: toSearchMaxResults(tool),
|
||||
});
|
||||
if (search.statusCode < 200 || search.statusCode >= 300) {
|
||||
@@ -252,7 +255,7 @@ async function sendResponsesSearchSimulation(input: {
|
||||
app: input.app,
|
||||
request: input.request,
|
||||
query,
|
||||
model: asTrimmedString(input.body.model) || '__search',
|
||||
model: SEARCH_SIMULATION_MODEL,
|
||||
maxResults: toSearchMaxResults(tool),
|
||||
});
|
||||
if (search.statusCode < 200 || search.statusCode >= 300) {
|
||||
|
||||
@@ -4749,6 +4749,7 @@ describe('chat proxy stream behavior', () => {
|
||||
|
||||
expect(response.statusCode, response.body).toBe(200);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(selectChannelMock).toHaveBeenCalledWith('__search', expect.anything());
|
||||
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({
|
||||
@@ -4816,6 +4817,7 @@ describe('chat proxy stream behavior', () => {
|
||||
|
||||
expect(response.statusCode, response.body).toBe(200);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(selectChannelMock).toHaveBeenCalledWith('__search', expect.anything());
|
||||
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({
|
||||
@@ -4825,6 +4827,7 @@ describe('chat proxy stream behavior', () => {
|
||||
|
||||
const body = response.json();
|
||||
expect(body.object).toBe('response');
|
||||
expect(body.output_text).toContain('Metapi Responses');
|
||||
expect(body.output?.[0]).toMatchObject({
|
||||
type: 'web_search_call',
|
||||
status: 'completed',
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
fetchDockerHubTagCandidates,
|
||||
fetchLatestStableGitHubRelease,
|
||||
getCurrentRuntimeVersion,
|
||||
type DockerHubTagCandidates,
|
||||
type UpdateCenterVersionCandidate,
|
||||
} from './updateCenterVersionService.js';
|
||||
import {
|
||||
@@ -164,8 +163,8 @@ export async function buildUpdateCenterStatus(): Promise<UpdateCenterStatusResul
|
||||
loadUpdateCenterRuntimeState(),
|
||||
]);
|
||||
|
||||
const githubRelease = githubLookup.value as UpdateCenterVersionCandidate | null;
|
||||
const dockerHubCandidates = dockerLookup.value as DockerHubTagCandidates | null;
|
||||
const githubRelease = githubLookup.value;
|
||||
const dockerHubCandidates = dockerLookup.value;
|
||||
const dockerHubTag = dockerHubCandidates?.primary || null;
|
||||
const dockerHubRecentTags = dockerHubCandidates?.recentNonStable || [];
|
||||
const helper = (helperLookup.value as UpdateCenterHelperStatus | null) || buildUnavailableHelperStatus(helperLookup.error);
|
||||
|
||||
Reference in New Issue
Block a user