test(agents): surface live OpenAI replay auth failures

This commit is contained in:
Peter Steinberger
2026-05-14 01:36:33 +01:00
parent 52370c5998
commit f3361dc928
3 changed files with 71 additions and 0 deletions

View File

@@ -545,6 +545,34 @@ async function completeSimpleWithTimeout<TApi extends Api>(
}
}
function requireToolChoicePayload(payload: unknown): unknown | undefined {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
return undefined;
}
const candidate = payload as { tools?: unknown; tool_choice?: unknown };
if (!Array.isArray(candidate.tools) || candidate.tools.length === 0) {
return undefined;
}
return {
...candidate,
tool_choice: { type: "function", name: "noop" },
};
}
describe("requireToolChoicePayload", () => {
it("requires tool use when a Responses payload has tools", () => {
expect(requireToolChoicePayload({ model: "gpt", tools: [{ name: "noop" }] })).toEqual({
model: "gpt",
tools: [{ name: "noop" }],
tool_choice: { type: "function", name: "noop" },
});
});
it("leaves payloads without tools unchanged", () => {
expect(requireToolChoicePayload({ model: "gpt", tools: [] })).toBeUndefined();
});
});
async function completeOkWithRetry(params: {
model: Model<Api>;
apiKey: string;
@@ -982,6 +1010,7 @@ describeLive("live models (profile keys)", () => {
apiKey,
reasoning: resolveTestReasoning(model),
maxTokens: 128,
onPayload: requireToolChoicePayload,
},
perModelTimeoutMs,
`${progressLabel}: tool-only regression first call`,
@@ -1012,6 +1041,7 @@ describeLive("live models (profile keys)", () => {
apiKey,
reasoning: resolveTestReasoning(model),
maxTokens: 128,
onPayload: requireToolChoicePayload,
},
perModelTimeoutMs,
`${progressLabel}: tool-only regression retry ${i + 1}`,
@@ -1025,6 +1055,11 @@ describeLive("live models (profile keys)", () => {
.trim();
}
if (first.stopReason === "error") {
throw new Error(
first.errorMessage || "tool-only regression returned error with no message",
);
}
expect(firstText.length).toBe(0);
if (!toolCall || toolCall.type !== "toolCall") {
throw new Error("expected tool call");

View File

@@ -2314,6 +2314,37 @@ describe("openai transport stream", () => {
});
});
it("passes explicit Responses tool_choice when tools are present", () => {
const params = buildOpenAIResponsesParams(
{
id: "gpt-5.4",
name: "GPT-5.4",
api: "openai-responses",
provider: "openai",
baseUrl: "https://api.openai.com/v1",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"openai-responses">,
{
systemPrompt: "system",
messages: [],
tools: [
{
name: "lookup_weather",
description: "Get forecast",
parameters: { type: "object", properties: {}, additionalProperties: false },
},
],
} as never,
{ toolChoice: "required" } as never,
) as { tool_choice?: string };
expect(params.tool_choice).toBe("required");
});
it("falls back to strict:false when a native OpenAI tool schema is not strict-compatible", () => {
const params = buildOpenAIResponsesParams(
{

View File

@@ -94,6 +94,7 @@ type OpenAIResponsesOptions = BaseStreamOptions & {
reasoningEffort?: OpenAIReasoningEffort;
reasoningSummary?: "auto" | "detailed" | "concise" | null;
serviceTier?: ResponseCreateParamsStreaming["service_tier"];
toolChoice?: ResponseCreateParamsStreaming["tool_choice"];
};
type OpenAICompletionsOptions = BaseStreamOptions & {
@@ -1364,6 +1365,9 @@ export function buildOpenAIResponsesParams(
transport: "stream",
}),
});
if (options?.toolChoice) {
params.tool_choice = options.toolChoice;
}
}
if (model.reasoning) {
if (options?.reasoningEffort || options?.reasoning || options?.reasoningSummary) {
@@ -2188,6 +2192,7 @@ type OpenAIResponsesRequestParams = {
top_p?: number;
service_tier?: ResponseCreateParamsStreaming["service_tier"];
tools?: FunctionTool[];
tool_choice?: ResponseCreateParamsStreaming["tool_choice"];
reasoning?:
| { effort: OpenAIApiReasoningEffort }
| {