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
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
## Commit Rules

- Use English conventional commits, for example `feat: reorganize source layout`.
- Use the Codex identity for Codex-authored commits: `codex <codex@users.noreply.github.com>`.
- Do not commit until `bun run lint`, `bun run format:check`, `bun run typecheck`, `bun run test:coverage`, and `bun run build` all pass.
- Unit test coverage must stay at or above 90%; do not commit code below the enforced coverage threshold.

Expand Down
13 changes: 12 additions & 1 deletion lib/server/proxy/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface ResponsesInputItem {
output?: unknown;
name?: string;
call_id?: string;
Comment thread
orangeboyChen marked this conversation as resolved.
tools?: Array<{ type?: string; name?: string } & Record<string, unknown>>;
}

interface SupportedChatTool {
Expand Down Expand Up @@ -969,14 +970,23 @@ const prepareTranscript = async (
typeof body.model === 'string' && body.model.trim()
? body.model
: (resolvedPreviousSession?.model ?? (await getDefaultModel()));
const additionalTools = Array.isArray(body.input)
? body.input.flatMap((item) =>
item?.type === 'additional_tools' && Array.isArray(item.tools)
? item.tools
: [],
)
: [];
const baseTools = body.tools ?? resolvedPreviousSession?.defaults.tools;
const requestTools = [...(baseTools ?? []), ...additionalTools];
const defaults = {
instructions:
body.instructions ??
resolvedPreviousSession?.defaults.instructions ??
undefined,
metadata:
body.metadata ?? resolvedPreviousSession?.defaults.metadata ?? undefined,
tools: body.tools ?? resolvedPreviousSession?.defaults.tools ?? undefined,
tools: requestTools.length > 0 ? requestTools : baseTools,
tool_choice:
body.tool_choice ??
resolvedPreviousSession?.defaults.tool_choice ??
Expand All @@ -994,6 +1004,7 @@ const prepareTranscript = async (
transcript.push({ role: 'user', content: body.input });
} else if (Array.isArray(body.input)) {
body.input.forEach((item) => {
if (item.type === 'additional_tools') return;
transcript.push(mapInputItemToMessage(item));
});
}
Expand Down
120 changes: 120 additions & 0 deletions tests/server/units.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4717,6 +4717,126 @@ describe('server units', () => {
});
});

it('extracts additional_tools input items before proxying responses', async () => {
Comment thread
orangeboyChen marked this conversation as resolved.
process.env.CODEBUDDY_AUTH_MODE = 'api_key';
process.env.CODEBUDDY_API_KEY = 'cb-key';

const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
makeJsonResponse({
choices: [{ message: { content: 'done' } }],
}),
);

await handleResponsesRequest(
makeNextRequest('http://localhost/v1/responses', { method: 'POST' }),
{
input: [
{
role: 'developer',
type: 'additional_tools',
tools: [
{
name: 'workspace',
tools: [
{
name: 'read_file',
parameters: { type: 'object', properties: {} },
type: 'function',
},
],
type: 'namespace',
},
],
},
{ role: 'user', content: 'read the file' },
],
model: 'gpt-5.5',
},
);

const upstreamBody = JSON.parse(
String((fetchMock.mock.calls[0]?.[1] as RequestInit).body),
) as {
messages: Array<{ content: string; role: string }>;
tools: Array<{ function: { name: string } }>;
};

expect(upstreamBody.tools).toEqual([
{
type: 'function',
function: {
name: 'workspace__read_file',
parameters: { type: 'object', properties: {} },
},
},
]);
expect(upstreamBody.messages).toEqual([
{ role: 'user', content: 'read the file' },
]);
});

it('appends additional tools to tools inherited from a response session', async () => {
process.env.CODEBUDDY_AUTH_MODE = 'api_key';
process.env.CODEBUDDY_API_KEY = 'cb-key';

const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockResolvedValueOnce(
makeJsonResponse({ choices: [{ message: { content: 'first' } }] }),
)
.mockResolvedValueOnce(
makeJsonResponse({ choices: [{ message: { content: 'second' } }] }),
);
const request = makeNextRequest('http://localhost/v1/responses', {
method: 'POST',
});

const firstResponse = await handleResponsesRequest(request, {
input: 'start',
model: 'gpt-5.5',
tools: [
{
name: 'tool_search',
type: 'tool_search',
},
],
});
const firstPayload = (await firstResponse.json()) as { id: string };

await handleResponsesRequest(request, {
input: [
{
role: 'developer',
type: 'additional_tools',
tools: [
{
name: 'workspace',
tools: [
{
name: 'read_file',
parameters: { type: 'object', properties: {} },
type: 'function',
},
],
type: 'namespace',
},
],
},
{ role: 'user', content: 'continue' },
],
previous_response_id: firstPayload.id,
});

const upstreamBody = JSON.parse(
String((fetchMock.mock.calls[1]?.[1] as RequestInit).body),
) as { tools: Array<{ function: { name: string } }> };

expect(upstreamBody.tools.map((tool) => tool.function.name)).toEqual([
'tool_search',
'workspace__read_file',
]);
});

it('flattens tools with function semantics into chat function tools', () => {
const result = translateResponsesToolsToChat([
{ type: 'file_search' },
Expand Down
Loading