Skip to content
Draft
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
4 changes: 3 additions & 1 deletion src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2458,7 +2458,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
// Last, so promoted namespace children are also cleared of Codex-private fields.
outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
}
if (!forward) outBody = normalizeOpenCodeGoAdditionalTools(outBody, url);
if (!forward) {
outBody = normalizeOpenCodeGoAdditionalTools(outBody, url, parsed._replayPrefixLen);
}
// Same predicate as the routedCompaction gate in handleResponses(): an authMode check would
// let a noncanonical custom forward provider skip this rewrite while the server still routes
// it as a summarizer turn (#422). The compaction body build removes the tool surface and must
Expand Down
14 changes: 12 additions & 2 deletions src/adapters/opencode-go-additional-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
}

/** Console Go accepts public tools but rejects the private additional_tools input wrapper. */
export function normalizeOpenCodeGoAdditionalTools(body: unknown, responseUrl: string): unknown {
export function normalizeOpenCodeGoAdditionalTools(
body: unknown,
responseUrl: string,
replayPrefixLength = 0,
): unknown {
let destination: URL;
try {
destination = new URL(responseUrl);
Expand All @@ -20,10 +24,16 @@ export function normalizeOpenCodeGoAdditionalTools(body: unknown, responseUrl: s

const input: unknown[] = [];
const promoted: unknown[] = [];
const currentTurnStart = Number.isFinite(replayPrefixLength)
? Math.min(body.input.length, Math.max(0, Math.trunc(replayPrefixLength)))
: 0;
Comment on lines +27 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the replay boundary aligned after stateless repairs

When a replay prefix contains a dangling function call before an additional_tools item, the OpenCode Go stateless path first runs repairOrphanedInputItems at src/adapters/openai-responses.ts:2387-2389, inserting a synthetic output and shifting that historical wrapper right while _replayPrefixLen remains unchanged. This index comparison then treats a wrapper shifted to the boundary as current and promotes its tools, leaving the historical-tool authority bug reachable; carry or adjust the provenance through length-changing transforms, or normalize before them.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

let changed = false;
for (const item of body.input) {
for (const [index, item] of body.input.entries()) {
if (isRecord(item) && item.type === "additional_tools" && Array.isArray(item.tools)) {
changed = true;
// Replayed wrappers are conversation history, not authority for the current request.
// Go cannot accept the wrapper itself, so remove it without promoting its catalog.
if (index < currentTurnStart) continue;
// Custom/search/namespace lowering already owns identity and deduplication. This pass
// only moves declarations, including hosted tools that intentionally have no name.
for (const tool of item.tools) promoted.push(tool);
Expand Down
22 changes: 22 additions & 0 deletions tests/providers/opencode-go-grok46-responses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ function buildRequest(
modelId: string,
rawBody: Record<string, unknown>,
configuredProvider = provider(),
replayPrefixLength?: number,
) {
return createResponsesPassthroughAdapter(configuredProvider).buildRequest({
modelId,
context: { messages: [] },
stream: true,
options: {},
_rawBody: { model: modelId, input: "ping", ...rawBody },
...(replayPrefixLength === undefined ? {} : { _replayPrefixLen: replayPrefixLength }),
}, { headers: new Headers() });
}

Expand Down Expand Up @@ -201,6 +203,26 @@ describe("OpenCode Go additional_tools placement", () => {
.toMatchObject({ input: [], tools: [], tool_choice: "none" });
});

test("does not promote additional tools restored from continuation history", () => {
const historicalWeb = { type: "web_search" };
const request = buildRequest("gpt-5.6-luna", {
tools: [],
input: [
{ type: "additional_tools", tools: [historicalWeb] },
{ type: "message", role: "assistant", content: "history" },
{ type: "message", role: "user", content: "continue" },
],
}, provider(), 2);

expect(JSON.parse(request.body)).toMatchObject({
input: [
{ type: "message", role: "assistant", content: "history" },
{ type: "message", role: "user", content: "continue" },
],
tools: [],
});
});

test("activates tools loaded by tool search before moving their catalog", () => {
const sent = build("gpt-5.6-luna", { input: [
{ type: "additional_tools", tools: [{ ...lookup, defer_loading: true }] },
Expand Down
Loading