feat(FN-5601): add OpenAI Responses API type support to custom providers
Added OpenAI Responses API as a new custom provider type, wiring `apiType: "responses"` through the core registry, engine routes, and dashboard UI with a dropdown selector; includes test coverage across the registry, routes, and component layers. Fusion-Task-Id: FN-5601 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> Fusion-Task-Id: FN-5601
This commit is contained in:
@@ -10,6 +10,7 @@ describe("custom-provider-registry", () => {
|
||||
it.each([
|
||||
["openai-compatible", "openai-completions"],
|
||||
["anthropic-compatible", "anthropic"],
|
||||
["openai-responses", "openai-responses"],
|
||||
])("resolveApiType maps %s -> %s", (apiType, expectedApi) => {
|
||||
expect(resolveApiType(apiType)).toBe(expectedApi);
|
||||
});
|
||||
@@ -20,7 +21,7 @@ describe("custom-provider-registry", () => {
|
||||
const logFn = vi.fn();
|
||||
const providers: CustomProvider[] = [
|
||||
{
|
||||
id: "openai-custom",
|
||||
id: "550e8400-e29b-41d4-a716-446655440000",
|
||||
name: "OpenAI Custom",
|
||||
apiType: "openai-compatible",
|
||||
baseUrl: "https://example.test/v1",
|
||||
@@ -28,7 +29,7 @@ describe("custom-provider-registry", () => {
|
||||
models: [{ id: "m1", name: "Model 1" }],
|
||||
},
|
||||
{
|
||||
id: "anthropic-custom",
|
||||
id: "660e8400-e29b-41d4-a716-446655440001",
|
||||
name: "Anthropic Custom",
|
||||
apiType: "anthropic-compatible",
|
||||
baseUrl: "https://anthropic.test",
|
||||
@@ -54,6 +55,34 @@ describe("custom-provider-registry", () => {
|
||||
expect(refresh).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses slugified provider names and collision suffixes for registry keys", () => {
|
||||
const registerProvider = vi.fn();
|
||||
const refresh = vi.fn();
|
||||
|
||||
registerCustomProviders(
|
||||
{ registerProvider, refresh },
|
||||
[
|
||||
{
|
||||
id: "dd0e8400-e29b-41d4-a716-446655440008",
|
||||
name: "My AI Provider",
|
||||
apiType: "openai-compatible",
|
||||
baseUrl: "https://one.test",
|
||||
},
|
||||
{
|
||||
id: "ee0e8400-e29b-41d4-a716-446655440009",
|
||||
name: "My AI Provider",
|
||||
apiType: "openai-compatible",
|
||||
baseUrl: "https://two.test",
|
||||
},
|
||||
],
|
||||
vi.fn(),
|
||||
);
|
||||
|
||||
expect(registerProvider).toHaveBeenNthCalledWith(1, "my-ai-provider", expect.any(Object));
|
||||
expect(registerProvider).toHaveBeenNthCalledWith(2, "my-ai-provider-2", expect.any(Object));
|
||||
expect(refresh).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handles empty provider list and still refreshes", () => {
|
||||
const registerProvider = vi.fn();
|
||||
const refresh = vi.fn();
|
||||
@@ -71,7 +100,7 @@ describe("custom-provider-registry", () => {
|
||||
registerCustomProviders(
|
||||
{ registerProvider, refresh },
|
||||
[{
|
||||
id: "no-models",
|
||||
id: "770e8400-e29b-41d4-a716-446655440002",
|
||||
name: "No Models",
|
||||
apiType: "openai-compatible",
|
||||
baseUrl: "https://nomodels.test",
|
||||
@@ -97,13 +126,13 @@ describe("custom-provider-registry", () => {
|
||||
{ registerProvider, refresh },
|
||||
[
|
||||
{
|
||||
id: "bad",
|
||||
id: "880e8400-e29b-41d4-a716-446655440003",
|
||||
name: "Bad",
|
||||
apiType: "openai-compatible",
|
||||
baseUrl: "https://bad.test",
|
||||
},
|
||||
{
|
||||
id: "good",
|
||||
id: "990e8400-e29b-41d4-a716-446655440004",
|
||||
name: "Good",
|
||||
apiType: "openai-compatible",
|
||||
baseUrl: "https://good.test",
|
||||
@@ -113,7 +142,7 @@ describe("custom-provider-registry", () => {
|
||||
);
|
||||
|
||||
expect(registerProvider).toHaveBeenCalledTimes(2);
|
||||
expect(logFn).toHaveBeenCalledWith(expect.stringContaining("Failed to register custom provider bad"));
|
||||
expect(logFn).toHaveBeenCalledWith(expect.stringContaining("id=880e8400-e29b-41d4-a716-446655440003"));
|
||||
expect(refresh).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -123,10 +152,10 @@ describe("custom-provider-registry", () => {
|
||||
|
||||
reregisterCustomProviders(
|
||||
{ registerProvider, refresh },
|
||||
[{ id: "old", name: "Old", apiType: "openai-compatible", baseUrl: "https://old.test" }],
|
||||
[{ id: "aa0e8400-e29b-41d4-a716-446655440005", name: "Old", apiType: "openai-compatible", baseUrl: "https://old.test" }],
|
||||
[
|
||||
{ id: "old", name: "Old", apiType: "openai-compatible", baseUrl: "https://old.test" },
|
||||
{ id: "new", name: "New", apiType: "anthropic-compatible", baseUrl: "https://new.test" },
|
||||
{ id: "aa0e8400-e29b-41d4-a716-446655440005", name: "Old", apiType: "openai-compatible", baseUrl: "https://old.test" },
|
||||
{ id: "bb0e8400-e29b-41d4-a716-446655440006", name: "New", apiType: "anthropic-compatible", baseUrl: "https://new.test" },
|
||||
],
|
||||
vi.fn(),
|
||||
);
|
||||
@@ -142,13 +171,13 @@ describe("custom-provider-registry", () => {
|
||||
|
||||
reregisterCustomProviders(
|
||||
{ registerProvider, refresh },
|
||||
[{ id: "same-id", name: "Provider", apiType: "openai-compatible", baseUrl: "https://one.test", apiKey: "A" }],
|
||||
[{ id: "same-id", name: "Provider", apiType: "openai-compatible", baseUrl: "https://two.test", apiKey: "B" }],
|
||||
[{ id: "cc0e8400-e29b-41d4-a716-446655440007", name: "Provider", apiType: "openai-compatible", baseUrl: "https://one.test", apiKey: "A" }],
|
||||
[{ id: "cc0e8400-e29b-41d4-a716-446655440007", name: "Provider", apiType: "openai-compatible", baseUrl: "https://two.test", apiKey: "B" }],
|
||||
vi.fn(),
|
||||
);
|
||||
|
||||
expect(registerProvider).toHaveBeenCalledTimes(1);
|
||||
expect(registerProvider).toHaveBeenCalledWith("same-id", expect.objectContaining({
|
||||
expect(registerProvider).toHaveBeenCalledWith("provider", expect.objectContaining({
|
||||
baseUrl: "https://two.test",
|
||||
apiKey: "B",
|
||||
}));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CustomProvider } from "@fusion/core";
|
||||
import { customProviderRegistryKey, type CustomProvider } from "@fusion/core";
|
||||
|
||||
interface ModelRegistryLike {
|
||||
registerProvider: (name: string, config: {
|
||||
@@ -22,6 +22,9 @@ export function resolveApiType(apiType: string): string {
|
||||
if (apiType === "anthropic-compatible") {
|
||||
return "anthropic";
|
||||
}
|
||||
if (apiType === "openai-responses") {
|
||||
return "openai-responses";
|
||||
}
|
||||
return "openai-completions";
|
||||
}
|
||||
|
||||
@@ -56,13 +59,15 @@ export function registerCustomProviders(
|
||||
customProviders: CustomProvider[] | undefined,
|
||||
logFn: (message: string) => void,
|
||||
): void {
|
||||
for (const provider of customProviders ?? []) {
|
||||
const providers = customProviders ?? [];
|
||||
for (const provider of providers) {
|
||||
const registryKey = customProviderRegistryKey(provider, providers);
|
||||
try {
|
||||
modelRegistry.registerProvider(provider.id, toProviderConfig(provider));
|
||||
logFn(`Registered custom provider ${provider.id}`);
|
||||
modelRegistry.registerProvider(registryKey, toProviderConfig(provider));
|
||||
logFn(`Registered custom provider "${provider.name}" (key=${registryKey}, id=${provider.id})`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
logFn(`Failed to register custom provider ${provider.id}: ${message}`);
|
||||
logFn(`Failed to register custom provider "${provider.name}" (key=${registryKey}, id=${provider.id}): ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,19 +81,21 @@ export function reregisterCustomProviders(
|
||||
logFn: (message: string) => void,
|
||||
): void {
|
||||
const previousById = new Map((previousProviders ?? []).map((provider) => [provider.id, provider]));
|
||||
const providers = currentProviders ?? [];
|
||||
|
||||
for (const provider of currentProviders ?? []) {
|
||||
for (const provider of providers) {
|
||||
const previous = previousById.get(provider.id);
|
||||
if (previous && !providersDiffer(previous, provider)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const registryKey = customProviderRegistryKey(provider, providers);
|
||||
try {
|
||||
modelRegistry.registerProvider(provider.id, toProviderConfig(provider));
|
||||
logFn(`${previous ? "Updated" : "Registered"} custom provider ${provider.id}`);
|
||||
modelRegistry.registerProvider(registryKey, toProviderConfig(provider));
|
||||
logFn(`${previous ? "Updated" : "Registered"} custom provider "${provider.name}" (key=${registryKey}, id=${provider.id})`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
logFn(`Failed to register custom provider ${provider.id}: ${message}`);
|
||||
logFn(`Failed to register custom provider "${provider.name}" (key=${registryKey}, id=${provider.id}): ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user