feat(experiments): support freeform provider responses
This commit is contained in:
@@ -225,6 +225,139 @@ describe("OllamaLlmProvider chat capability detection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ollama freeform semantics", () => {
|
||||
it("omits format constraint when outputSchema is explicitly null (chat path)", async () => {
|
||||
const originalBaseUrl = process.env.OLLAMA_BASE_URL;
|
||||
const fetchSpy = vi.fn()
|
||||
.mockResolvedValueOnce({ ok: true, text: async () => "" })
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ message: { content: '{"freeform":"output"}' } }),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
process.env.OLLAMA_BASE_URL = "http://ollama.test";
|
||||
|
||||
try {
|
||||
__resetChatSupportForTests();
|
||||
const provider = getProvider();
|
||||
const result = await provider.generateReconstruction("prompt", "model", null, { signal: undefined });
|
||||
|
||||
expect(result.response).toBe("{\"freeform\":\"output\"}");
|
||||
expect(result.providerApiPath).toBe("/api/chat");
|
||||
|
||||
// No format key in request body when schema is explicitly null
|
||||
const requestBody = JSON.parse(fetchSpy.mock.calls[1][1].body);
|
||||
expect(requestBody).not.toHaveProperty("format");
|
||||
} finally {
|
||||
vi.unstubAllGlobals();
|
||||
if (originalBaseUrl === undefined) delete process.env.OLLAMA_BASE_URL;
|
||||
else process.env.OLLAMA_BASE_URL = originalBaseUrl;
|
||||
}
|
||||
});
|
||||
|
||||
it("omits format constraint when outputSchema is explicitly null (generate fallback)", async () => {
|
||||
const originalBaseUrl = process.env.OLLAMA_BASE_URL;
|
||||
const fetchSpy = vi.fn()
|
||||
.mockResolvedValueOnce({ ok: false, status: 501, text: async () => "" })
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ response: "freeform text from /api/generate" }),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
process.env.OLLAMA_BASE_URL = "http://ollama.test";
|
||||
|
||||
try {
|
||||
__resetChatSupportForTests();
|
||||
const provider = getProvider();
|
||||
const result = await provider.generateReconstruction("prompt", "model", null, { signal: undefined });
|
||||
|
||||
expect(result.response).toBe("freeform text from /api/generate");
|
||||
expect(result.providerApiPath).toBe("/api/generate");
|
||||
|
||||
// No format key in request body when schema is explicitly null
|
||||
const requestBody = JSON.parse(fetchSpy.mock.calls[1][1].body);
|
||||
expect(requestBody).not.toHaveProperty("format");
|
||||
} finally {
|
||||
vi.unstubAllGlobals();
|
||||
if (originalBaseUrl === undefined) delete process.env.OLLAMA_BASE_URL;
|
||||
else process.env.OLLAMA_BASE_URL = originalBaseUrl;
|
||||
}
|
||||
});
|
||||
|
||||
it("still applies default schema when outputSchema is undefined", async () => {
|
||||
const originalBaseUrl = process.env.OLLAMA_BASE_URL;
|
||||
const fetchSpy = vi.fn()
|
||||
.mockResolvedValueOnce({ ok: true, text: async () => "" })
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ message: { content: "{}" } }),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
process.env.OLLAMA_BASE_URL = "http://ollama.test";
|
||||
|
||||
try {
|
||||
__resetChatSupportForTests();
|
||||
await getProvider().generateReconstruction("prompt", "model", undefined, { signal: undefined });
|
||||
|
||||
const requestBody = JSON.parse(fetchSpy.mock.calls[1][1].body);
|
||||
// undefined → fallback to reconstructionJsonSchema (format IS present)
|
||||
expect(requestBody.format).toBeDefined();
|
||||
} finally {
|
||||
vi.unstubAllGlobals();
|
||||
if (originalBaseUrl === undefined) delete process.env.OLLAMA_BASE_URL;
|
||||
else process.env.OLLAMA_BASE_URL = originalBaseUrl;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenAI freeform semantics", () => {
|
||||
it("uses text format when outputSchema is explicitly null", async () => {
|
||||
const provider = createOpenAIReconstructionProvider({
|
||||
apiKey: "test-key",
|
||||
fetchImpl: vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ output_text: "freeform text response" }),
|
||||
}),
|
||||
});
|
||||
|
||||
await provider.generateReconstruction("prompt", "model", null);
|
||||
|
||||
const request = JSON.parse(
|
||||
provider.fetchImpl.mock.calls[0][1].body,
|
||||
);
|
||||
expect(request.text.format.type).toBe("text");
|
||||
});
|
||||
|
||||
it("returns raw text without JSON recovery when outputSchema is explicitly null", async () => {
|
||||
const provider = createOpenAIReconstructionProvider({
|
||||
apiKey: "test-key",
|
||||
fetchImpl: vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ output_text: "not-json-at-all {broken" }),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await provider.generateReconstruction("prompt", "model", null);
|
||||
expect(result.response).toBe("not-json-at-all {broken");
|
||||
expect(result.providerApiPath).toBe("/v1/responses");
|
||||
});
|
||||
|
||||
it("still applies json_schema format when outputSchema is undefined", async () => {
|
||||
const provider = createOpenAIReconstructionProvider({
|
||||
apiKey: "test-key",
|
||||
fetchImpl: vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ output_text: "{}" }),
|
||||
}),
|
||||
});
|
||||
|
||||
await provider.generateReconstruction("prompt");
|
||||
|
||||
const request = JSON.parse(provider.fetchImpl.mock.calls[0][1].body);
|
||||
expect(request.text.format.type).toBe("json_schema");
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenAI reconstruction provider experiment seam", () => {
|
||||
function assertStrictObjectInvariant(schema, root = schema, visited = new Set()) {
|
||||
if (!schema || typeof schema !== "object" || visited.has(schema)) return;
|
||||
|
||||
Reference in New Issue
Block a user