Skip to content
Merged
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
17 changes: 16 additions & 1 deletion containers/api-proxy/codex-hosted-web.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const SEARCH_PATHS = new Set(['/v1/alpha/search', '/alpha/search']);
const SEARCH_COMMANDS = new Set(['search_query', 'image_query', 'open', 'click', 'find', 'screenshot']);
const URL_COMMANDS = new Set(['open', 'find', 'screenshot']);
const FILTER_FIELDS = new Set(['allowed_domains', 'blocked_domains']);
const STANDALONE_FIELDS = new Set(['id', 'model', 'settings', 'commands']);
const STANDALONE_FIELDS = new Set(['id', 'model', 'settings', 'commands', 'max_output_tokens', 'input']);
const TOOL_FIELDS = new Set([
'type', 'external_web_access', 'indexed_web_access', 'filters', 'user_location',
'search_context_size', 'search_content_types', 'image_settings', 'max_uses',
Expand Down Expand Up @@ -271,6 +271,21 @@ function enforceStandalone(body, policy) {
'codex_hosted_web_shape_invalid',
'Codex standalone hosted search body contains an unrecognized field.',
);
if (hasField(body, 'max_output_tokens') &&
(!Number.isInteger(body.max_output_tokens) || body.max_output_tokens < 0)) {
throw new CodexHostedWebPolicyError(
'codex_hosted_web_shape_invalid',
'Codex standalone hosted search "max_output_tokens" must be a non-negative integer.',
400,
);
}
if (hasField(body, 'input') && typeof body.input !== 'string' && !Array.isArray(body.input)) {
throw new CodexHostedWebPolicyError(
'codex_hosted_web_shape_invalid',
'Codex standalone hosted search "input" must be a string or an array of items.',
400,
);
}
const settings = body.settings === undefined ? {} : body.settings;
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
throw new CodexHostedWebPolicyError(
Expand Down
87 changes: 69 additions & 18 deletions containers/api-proxy/codex-hosted-web.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,49 @@ describe('Codex hosted-web policy', () => {
.toEqual([['docs.github.com'], ['docs.github.com']]);
});

it.each([
'Find the latest documentation',
[{ role: 'user', content: [{ type: 'input_text', text: 'Find the latest documentation' }] }],
[],
])('preserves standalone conversation input and output limits while applying filters: %j', input => {
const body = {
id: 'search-request',
model: 'gpt-5.6-terra',
max_output_tokens: 2048,
input,
settings: { filters: { allowed_domains: ['github.com'] } },
commands: { search_query: [{ q: 'documentation', domains: ['github.com'] }] },
};
expect(enforceStandalone(body, { ...allow, maxUses: undefined })).toEqual({
...body,
settings: { filters: { allowed_domains: ['docs.github.com'] } },
commands: { search_query: [{ q: 'documentation', domains: ['docs.github.com'] }] },
});
expect(body.settings.filters.allowed_domains).toEqual(['github.com']);
expect(body.commands.search_query[0].domains).toEqual(['github.com']);
});

it('accepts a zero standalone output limit', () => {
expect(enforceStandalone({ max_output_tokens: 0 }, { ...allow, maxUses: undefined })
.max_output_tokens).toBe(0);
});

it.each([-1, 1.5, '2048', null, true, {}, []])(
'rejects invalid standalone max_output_tokens: %j',
maxOutputTokens => {
expect(() => enforceStandalone({
max_output_tokens: maxOutputTokens,
}, { ...allow, maxUses: undefined })).toThrow(expect.objectContaining({
code: 'codex_hosted_web_shape_invalid', statusCode: 400,
}));
},
);

it.each([null, 42, true, {}])('rejects invalid standalone input: %j', input => {
expect(() => enforceStandalone({ input }, { ...allow, maxUses: undefined }))
.toThrow(expect.objectContaining({ code: 'codex_hosted_web_shape_invalid', statusCode: 400 }));
});

it('unions standalone blocklists and prevents query scopes from removing blocks', () => {
const result = enforceStandalone({
settings: { filters: { blocked_domains: ['ads.example'] } },
Expand Down Expand Up @@ -159,22 +202,30 @@ describe('Codex hosted-web policy', () => {
.toThrow(expect.objectContaining({ code: 'codex_hosted_web_max_uses_unsupported' }));
});

it('uses the request path to select the standalone body shape', () => {
const transform = makeCodexHostedWebTransform({ ...allow, maxUses: undefined });
const transformed = transform(
Buffer.from(JSON.stringify({ commands: {} })),
{ url: '/v1/alpha/search' },
);
expect(JSON.parse(transformed)).toEqual({
commands: {},
settings: { filters: { allowed_domains: ['docs.github.com'] } },
});
expect(JSON.parse(transform(
Buffer.from(JSON.stringify({ commands: {} })),
{ url: '/v1/alpha/search/' },
))).toEqual({
commands: {},
settings: { filters: { allowed_domains: ['docs.github.com'] } },
});
});
it.each(['/alpha/search', '/v1/alpha/search'])(
'uses the request path to select the standalone body shape: %s',
pathname => {
const transform = makeCodexHostedWebTransform({ ...allow, maxUses: undefined });
const body = {
commands: { search_query: [{ q: 'documentation' }] },
input: [{ role: 'user', content: 'Find documentation' }],
max_output_tokens: 2048,
};
const transformed = transform(
Buffer.from(JSON.stringify(body)),
{ url: pathname },
);
expect(JSON.parse(transformed)).toEqual({
...body,
settings: { filters: { allowed_domains: ['docs.github.com'] } },
});
expect(JSON.parse(transform(
Buffer.from(JSON.stringify(body)),
{ url: `${pathname}/` },
))).toEqual({
...body,
settings: { filters: { allowed_domains: ['docs.github.com'] } },
});
},
);
});
Loading