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
84 changes: 84 additions & 0 deletions assistant/src/__tests__/cross-provider-web-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,90 @@ describe("Cross-Provider Web Search — OpenAI (Responses API)", () => {
});
});

// ---------------------------------------------------------------------------
// OpenAI Responses API — native web search tool mapping
// ---------------------------------------------------------------------------

describe("Cross-Provider Web Search — OpenAI (Responses API, native mode)", () => {
beforeEach(() => {
lastOpenAIResponsesParams = null;
});

test("maps web_search to native web_search_preview tool when useNativeWebSearch is enabled", async () => {
const provider = new OpenAIResponsesProvider("sk-test", "gpt-4o", {
useNativeWebSearch: true,
});

const tools = [
{
name: "file_read",
description: "Read a file",
input_schema: {
type: "object",
properties: { path: { type: "string" } },
},
},
{
name: "web_search",
description: "Search the web",
input_schema: {
type: "object",
properties: { query: { type: "string" } },
},
},
];

await provider.sendMessage([userMsg("Search for something")], tools);

const sentTools = lastOpenAIResponsesParams!.tools as Array<
Record<string, unknown>
>;
expect(sentTools).toHaveLength(2);
// Non-web-search tools stay as function tools
expect(sentTools[0]).toMatchObject({ type: "function", name: "file_read" });
// web_search is replaced with native hosted tool
expect(sentTools[1]).toEqual({ type: "web_search_preview" });
});

test("still degrades web search history blocks in native mode", async () => {
const provider = new OpenAIResponsesProvider("sk-test", "gpt-4o", {
useNativeWebSearch: true,
});
await provider.sendMessage(webSearchConversation());

const input = lastOpenAIResponsesParams!.input as Array<{
type: string;
role?: string;
content?: Array<{ type: string; text?: string }>;
}>;

// server_tool_use in assistant history is still degraded to text placeholder
const assistantItems = input.filter(
(item) => item.type === "message" && item.role === "assistant",
);
const hasWebSearchPlaceholder = assistantItems.some((item) =>
item.content?.some(
(part) =>
part.type === "output_text" &&
part.text?.includes("[Web search: web_search]"),
),
);
expect(hasWebSearchPlaceholder).toBe(true);

// web_search_tool_result in user history is still degraded to text placeholder
const userItems = input.filter(
(item) => item.type === "message" && item.role === "user",
);
const hasWebSearchResult = userItems.some((item) =>
item.content?.some(
(part) =>
part.type === "input_text" && part.text === "[Web search results]",
),
);
expect(hasWebSearchResult).toBe(true);
});
});

// ---------------------------------------------------------------------------
// OpenAI Chat Completions compatibility provider tests
// ---------------------------------------------------------------------------
Expand Down
121 changes: 121 additions & 0 deletions assistant/src/__tests__/llm-context-normalization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1601,4 +1601,125 @@ describe("normalizeLlmContextPayloads", () => {

expect(normalized).toEqual({});
});

test("normalizes Responses API web_search_call output as a tool_use section", () => {
const normalized = normalizeLlmContextPayloads({
createdAt: 1_742_400_000_030,
requestPayload: {
model: "gpt-5.4",
instructions: "Search the web when needed.",
input: [
{
role: "user",
content: [{ type: "input_text", text: "What is the weather?" }],
type: "message",
},
],
tools: [{ type: "web_search_preview" }],
},
responsePayload: {
model: "gpt-5.4",
output: [
{
type: "web_search_call",
id: "ws_abc",
status: "completed",
},
{
type: "message",
role: "assistant",
content: [
{ type: "output_text", text: "It is sunny in Boston today." },
],
},
],
usage: { input_tokens: 30, output_tokens: 15 },
status: "completed",
},
});

expect(normalized.summary).toEqual({
provider: "openai",
model: "gpt-5.4",
inputTokens: 30,
outputTokens: 15,
stopReason: "stop",
requestMessageCount: 1,
requestToolCount: 1,
responseMessageCount: 1,
responseToolCallCount: 1,
responsePreview: "It is sunny in Boston today.",
toolCallNames: ["web_search"],
});
expect(normalized.responseSections).toEqual([
{
kind: "tool_use",
label: "Response tool call 1",
role: "assistant",
toolName: "web_search",
data: { id: "ws_abc", status: "completed" },
text: "[Web search: completed]",
},
{
kind: "message",
label: "Assistant response",
role: "assistant",
text: "It is sunny in Boston today.",
},
]);
});

test("normalizes Responses API response with only web_search_call (no message)", () => {
const normalized = normalizeLlmContextPayloads({
createdAt: 1_742_400_000_031,
requestPayload: {
model: "gpt-5.4",
instructions: "Search the web.",
input: [
{
role: "user",
content: [{ type: "input_text", text: "Find latest news" }],
type: "message",
},
],
tools: [{ type: "web_search_preview" }],
},
responsePayload: {
model: "gpt-5.4",
output: [
{
type: "web_search_call",
id: "ws_only",
status: "searching",
},
],
usage: { input_tokens: 20, output_tokens: 5 },
status: "incomplete",
},
});

expect(normalized.summary).toEqual({
provider: "openai",
model: "gpt-5.4",
inputTokens: 20,
outputTokens: 5,
stopReason: "incomplete",
requestMessageCount: 1,
requestToolCount: 1,
responseMessageCount: 1,
responseToolCallCount: 1,
responsePreview: undefined,
toolCallNames: ["web_search"],
});
expect(normalized.responseSections).toEqual([
{
kind: "tool_use",
label: "Response tool call 1",
role: "assistant",
toolName: "web_search",
data: { id: "ws_only", status: "searching" },
text: "[Web search: searching]",
},
]);
});
});
8 changes: 6 additions & 2 deletions assistant/src/__tests__/managed-proxy-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,12 @@ describe("buildManagedBaseUrl", () => {
expect(await buildManagedBaseUrl("gemini")).toBe(
"https://platform.example.com/v1/runtime-proxy/gemini",
);
expect(await buildManagedBaseUrl("openai")).toBe(
"https://platform.example.com/v1/runtime-proxy/openai",
);
});

test("returns undefined for non-managed providers", async () => {
expect(await buildManagedBaseUrl("openai")).toBeUndefined();
expect(await buildManagedBaseUrl("fireworks")).toBeUndefined();
expect(await buildManagedBaseUrl("openrouter")).toBeUndefined();
expect(await buildManagedBaseUrl("ollama")).toBeUndefined();
Expand All @@ -130,6 +132,7 @@ describe("buildManagedBaseUrl", () => {
mockAssistantApiKey = null;
expect(await buildManagedBaseUrl("anthropic")).toBeUndefined();
expect(await buildManagedBaseUrl("gemini")).toBeUndefined();
expect(await buildManagedBaseUrl("openai")).toBeUndefined();
});
});

Expand All @@ -142,7 +145,7 @@ describe("managedFallbackEnabledFor", () => {
test("returns true only for managed fallback providers with prerequisites", async () => {
expect(await managedFallbackEnabledFor("anthropic")).toBe(true);
expect(await managedFallbackEnabledFor("gemini")).toBe(true);
expect(await managedFallbackEnabledFor("openai")).toBe(false);
expect(await managedFallbackEnabledFor("openai")).toBe(true);
});

test("returns false for non-managed provider", async () => {
Expand All @@ -158,5 +161,6 @@ describe("managedFallbackEnabledFor", () => {
mockAssistantApiKey = null;
expect(await managedFallbackEnabledFor("anthropic")).toBe(false);
expect(await managedFallbackEnabledFor("gemini")).toBe(false);
expect(await managedFallbackEnabledFor("openai")).toBe(false);
});
});
Loading
Loading