feat(opencode-go): add muse-spark-1.3-contributor and fix parallel tool calls on Responses paths (#3819)

- Add muse-spark-1.3-contributor as responses-only model on OpenCode Go with dedicated executor
- Key Responses→chat streaming tool calls by item_id to prevent parallel tool calls merging into index 0
- Standardize tool coercions and call_id clamping in Responses API translation
This commit is contained in:
Sina Sadeghi
2026-09-05 21:49:00 +07:00
parent 77e6a227fe
commit e74db4d0a6
9 changed files with 541 additions and 24 deletions

View File

@@ -446,6 +446,13 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
state.created = Math.floor(Date.now() / 1000);
state.toolCallIndex = 0;
state.currentToolCallId = null;
// item_id → chat tool_calls index. Deltas carry item_id; keying on it (not
// stream position) keeps parallel calls separate when upstream emits all
// output_item.added events before any done/delta. Lazily created so callers
// that build their own state object (stream.js) need no changes.
state.respToolChatIndex ??= new Map();
// Indices that already received argument deltas (guards done-with-args).
state.respToolArgsEmitted ??= new Set();
}
// Text content delta
@@ -464,16 +471,29 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
return null;
}
// Function call started (standard function_call or custom_tool_call)
// Function call started (standard function_call or custom_tool_call).
// Index is assigned here (not on done): attributing deltas by stream position
// merges parallel calls into index 0 whenever upstream emits all addeds
// before dones — the client then concatenates N JSON payloads into one
// tool input and fails validation. The server item id is the correlator.
if (eventType === "response.output_item.added" && (data.item?.type === RESPONSES_ITEM.FUNCTION_CALL || data.item?.type === "custom_tool_call")) {
const item = data.item;
state.currentToolCallId = item.call_id || fallbackToolCallId();
state.respToolChatIndex ??= new Map();
const key = item.id || data.item_id || state.currentToolCallId;
let idx;
if (key && state.respToolChatIndex.has(key)) {
idx = state.respToolChatIndex.get(key); // duplicate added (retry) — reuse
} else {
idx = state.toolCallIndex++;
if (key) state.respToolChatIndex.set(key, idx);
}
return buildChunk(
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
{
tool_calls: [{
index: state.toolCallIndex,
index: idx,
id: state.currentToolCallId,
type: OPENAI_BLOCK.FUNCTION,
function: { name: item.name || "", arguments: "" }
@@ -482,20 +502,39 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
);
}
// Function call arguments delta (standard or custom_tool_call variant)
// Function call arguments delta (standard or custom_tool_call variant).
// Routed by item_id so interleaved parallel fragments stay on their own call.
if (eventType === "response.function_call_arguments.delta" || eventType === "response.custom_tool_call_input.delta") {
const argsDelta = data.delta || "";
if (!argsDelta) return null;
const known = data.item_id ? state.respToolChatIndex?.get(data.item_id) : undefined;
const idx = known ?? Math.max(0, (state.toolCallIndex || 1) - 1);
state.respToolArgsEmitted ??= new Set();
state.respToolArgsEmitted.add(idx);
return buildChunk(
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
{ tool_calls: [{ index: state.toolCallIndex, function: { arguments: argsDelta } }] }
{ tool_calls: [{ index: idx, function: { arguments: argsDelta } }] }
);
}
// Function call done (standard or custom_tool_call variant)
// Function call done (standard or custom_tool_call variant).
// Index was assigned at added-time; nothing to advance. Some upstreams send
// complete arguments only here (no deltas) — emit them once in that case.
if (eventType === "response.output_item.done" && (data.item?.type === RESPONSES_ITEM.FUNCTION_CALL || data.item?.type === "custom_tool_call")) {
state.toolCallIndex++;
const key = data.item?.id || data.item_id;
const idx = (key && state.respToolChatIndex?.get(key)) ?? Math.max(0, (state.toolCallIndex || 1) - 1);
const fullArgs = data.item?.arguments;
if (typeof fullArgs === "string" && fullArgs) {
state.respToolArgsEmitted ??= new Set();
if (!state.respToolArgsEmitted.has(idx)) {
state.respToolArgsEmitted.add(idx);
return buildChunk(
{ id: state.chatId, created: state.created, model: state.model || MODEL_FALLBACK },
{ tool_calls: [{ index: idx, function: { arguments: fullArgs } }] }
);
}
}
return null;
}