mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(ai): drop mismatched item ids when replaying grammar tool calls
Foreign tool call ids are normalized to fc_*, but grammar tools such as codemode replay as custom_tool_call, which requires ctc_* ids. This broke switching OpenAI models through Radius after a codemode call (earendil-works/radius#115).
This commit is contained in:
parent
92fc452c52
commit
bc2d8dc1c4
3 changed files with 44 additions and 7 deletions
|
|
@ -10,6 +10,10 @@
|
|||
|
||||
- Changed OAuth browser pages to use the color Pi logo.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenAI Responses requests failing with `Expected an ID that begins with 'ctc'` when replaying grammar tool calls, such as `codemode`, from another provider or a gateway like Radius.
|
||||
|
||||
## [0.99.2] - 2026-09-30
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -294,14 +294,13 @@ export function convertResponsesMessages<TApi extends Api>(
|
|||
let itemId: string | undefined = itemIdRaw;
|
||||
|
||||
// For different-model messages, set id to undefined to avoid pairing validation.
|
||||
// OpenAI tracks which fc_xxx IDs were paired with rs_xxx reasoning items.
|
||||
// OpenAI tracks which item IDs were paired with rs_xxx reasoning items.
|
||||
// By omitting the id, we avoid triggering that validation (like cross-provider does).
|
||||
// When replaying custom-tool calls as a function_call, also drop non-fc_* ids such as
|
||||
// ctc_* custom-tool ids because function_call item ids must be fc_*.
|
||||
if (
|
||||
(isDifferentModel && itemId?.startsWith("fc_")) ||
|
||||
(customInputProperty === undefined && !itemId?.startsWith("fc_"))
|
||||
) {
|
||||
// Also drop ids that do not match the replayed item type: function_call ids must be fc_*
|
||||
// and custom_tool_call ids must be ctc_*. Foreign tool call ids are normalized to fc_*, and
|
||||
// a call can switch between the two types when grammar tool support differs.
|
||||
const itemIdPrefix = customInputProperty === undefined ? "fc_" : "ctc_";
|
||||
if (isDifferentModel || !itemId?.startsWith(itemIdPrefix)) {
|
||||
itemId = undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -256,6 +256,40 @@ describe("constrained tool sampling", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// earendil-works/radius#115: a gateway forwards another model's history as a foreign provider.
|
||||
it("drops foreign item ids when replaying grammar calls as custom Responses items", () => {
|
||||
const context = normalizeContext({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
api: "pi-messages",
|
||||
provider: "radius",
|
||||
model: "gpt-other",
|
||||
content: [{ type: "toolCall", id: "call_1|ctc_1", name: "sample_tool", arguments: { payload: "abc" } }],
|
||||
usage: makeUsage(),
|
||||
stopReason: "toolUse",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call_1|ctc_1",
|
||||
toolName: "sample_tool",
|
||||
content: [{ type: "text", text: "done" }],
|
||||
isError: false,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const messages = convertResponsesMessages(makeModel(), context, new Set(["openai"]), {
|
||||
grammarToolInputProperties: new Map([["sample_tool", "payload"]]),
|
||||
});
|
||||
|
||||
const call = messages.find((item) => "type" in item && item.type === "custom_tool_call");
|
||||
expect(call).toMatchObject({ type: "custom_tool_call", call_id: "call_1", input: "abc" });
|
||||
expect(call && "id" in call ? call.id : undefined).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps grammar input JSON deltas append-only", () => {
|
||||
const buffer = { input: "", started: false, closed: false };
|
||||
const first = appendGrammarToolInputJsonDelta(buffer, "payload", 'a"', false);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue