Skip to content

Commit 5cacf35

Browse files
committed
feat(providers): add oomol fusion api
1 parent e2c2311 commit 5cacf35

9 files changed

Lines changed: 3712 additions & 2 deletions

File tree

src/core/provider-id.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ describe("provider ids", () => {
55
it("accepts catalog provider ids", () => {
66
expect(isProviderId("sage_sales_management")).toBe(true);
77
expect(isProviderId("wttr_in")).toBe(true);
8+
expect(isProviderId("fusion-api")).toBe(true);
89
});
910

1011
it("rejects ids that are unsafe as provider paths or catalog filenames", () => {
11-
for (const value of ["../x", "foo-bar", "Foo", "foo/bar", "foo.json", ""]) {
12+
for (const value of ["../x", "Foo", "foo/bar", "foo.json", ""]) {
1213
expect(isProviderId(value)).toBe(false);
1314
expect(() => assertProviderId(value)).toThrow("provider id must match");
1415
}

src/core/provider-id.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const providerIdPattern = /^[a-z0-9_]+$/;
1+
const providerIdPattern = /^[a-z0-9_-]+$/;
22

33
/**
44
* Check provider ids used as source directory names and catalog filenames.
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import type { ProviderActionDefinition } from "../../core/provider-definition.ts";
2+
import type { ActionDefinition, JsonSchema } from "../../core/types.ts";
3+
import type { FusionApiInputField, FusionApiInputFieldKind, FusionApiOperation } from "./operations.ts";
4+
5+
import { s } from "../../core/json-schema.ts";
6+
import { defineProviderAction } from "../../core/provider-definition.ts";
7+
import { fusionApiOperations } from "./operations.ts";
8+
9+
const service = "fusion-api";
10+
const fusionApiActionsById = new Set(fusionApiOperations.map((operation) => `${service}.${operation.actionName}`));
11+
12+
const urlArrayItemSchema = s.url("One URL accepted by Fusion API.");
13+
14+
const completedStateSchema = s.object("The Fusion API task completed.", {
15+
state: s.literal("completed", { description: "Task state." }),
16+
});
17+
const processingStateSchema = s.object("The Fusion API task is still processing.", {
18+
state: s.literal("processing", { description: "Task state." }),
19+
progress: s.number("Task progress reported by Fusion API."),
20+
});
21+
const missingStateSchema = s.object("The Fusion API task was not found.", {
22+
state: s.literal("not_found", { description: "Task state." }),
23+
error: s.string("Error message returned by Fusion API."),
24+
});
25+
const taskResultSchema = s.union(
26+
[
27+
s.object("The Fusion API task completed with result data.", {
28+
state: s.literal("completed", { description: "Task state." }),
29+
data: s.unknown("Task result data returned by Fusion API."),
30+
}),
31+
processingStateSchema,
32+
missingStateSchema,
33+
],
34+
{ description: "The normalized Fusion API task result." },
35+
);
36+
const taskStateSchema = s.union([completedStateSchema, processingStateSchema, missingStateSchema], {
37+
description: "The normalized Fusion API task state.",
38+
});
39+
const submitOutputSchema = s.object("The Fusion API task submission handle.", {
40+
sessionId: s.nonEmptyString("Task session ID used by result and state actions."),
41+
});
42+
const syncActionOutputSchema = s.unknown("The normalized Fusion API action result.");
43+
44+
export const fusionApiActions: ProviderActionDefinition<string>[] = fusionApiOperations.map((operation) =>
45+
defineProviderAction(service, {
46+
name: operation.actionName,
47+
description: operation.description,
48+
inputSchema: buildInputSchema(operation),
49+
outputSchema: buildOutputSchema(operation),
50+
followUpActions: buildFollowUpActions(operation),
51+
asyncLifecycle: buildAsyncLifecycle(operation),
52+
}),
53+
);
54+
55+
function buildInputSchema(operation: FusionApiOperation): JsonSchema {
56+
const properties = Object.fromEntries(
57+
operation.inputFields.map((field) => [field.name, buildFieldSchema(field)] as const),
58+
);
59+
const required = operation.inputFields.filter((field) => field.required).map((field) => field.name);
60+
return s.object("The Fusion API request payload.", properties, {
61+
required,
62+
additionalProperties: true,
63+
});
64+
}
65+
66+
function buildFieldSchema(field: FusionApiInputField): JsonSchema {
67+
const options = buildFieldOptions(field);
68+
69+
if (field.enumValues && field.enumValues.length > 0) {
70+
return s.stringEnum(field.enumValues, options);
71+
}
72+
if (isUrlStringField(field)) {
73+
return s.url(field.description);
74+
}
75+
if (isUrlArrayField(field)) {
76+
return s.array(urlArrayItemSchema, options);
77+
}
78+
79+
switch (field.kind satisfies FusionApiInputFieldKind) {
80+
case "string":
81+
return s.string(options);
82+
case "number":
83+
return s.number({ ...options, minimum: field.minimum, maximum: field.maximum });
84+
case "integer":
85+
return s.integer({ ...options, minimum: field.minimum, maximum: field.maximum });
86+
case "boolean":
87+
return s.boolean(options);
88+
case "array":
89+
return s.array(s.unknown("One array item."), options);
90+
case "object":
91+
return s.record(field.description, s.unknown("One object property value."));
92+
case "unknown":
93+
return s.unknown(field.description);
94+
}
95+
}
96+
97+
function buildFieldOptions(field: FusionApiInputField): {
98+
description: string;
99+
default?: string | number | boolean;
100+
format?: string;
101+
} {
102+
return {
103+
description: field.description,
104+
default: field.defaultValue,
105+
format: field.format,
106+
};
107+
}
108+
109+
function isUrlStringField(field: FusionApiInputField): boolean {
110+
return field.kind === "string" && fieldNameLooksLikeUrl(field.name);
111+
}
112+
113+
function isUrlArrayField(field: FusionApiInputField): boolean {
114+
return field.kind === "array" && fieldNameLooksLikeUrls(field.name);
115+
}
116+
117+
function fieldNameLooksLikeUrl(name: string): boolean {
118+
return name.toLowerCase().endsWith("url");
119+
}
120+
121+
function fieldNameLooksLikeUrls(name: string): boolean {
122+
return name.toLowerCase().endsWith("urls");
123+
}
124+
125+
function buildOutputSchema(operation: FusionApiOperation): JsonSchema {
126+
if (operation.actionName.endsWith("_submit")) {
127+
return submitOutputSchema;
128+
}
129+
if (operation.actionName.endsWith("_result")) {
130+
return taskResultSchema;
131+
}
132+
if (operation.actionName.endsWith("_state")) {
133+
return taskStateSchema;
134+
}
135+
return syncActionOutputSchema;
136+
}
137+
138+
function buildFollowUpActions(operation: FusionApiOperation): string[] | undefined {
139+
if (!operation.actionName.endsWith("_submit")) {
140+
return undefined;
141+
}
142+
143+
const prefix = operation.actionName.slice(0, -"_submit".length);
144+
const followUps = [`${service}.${prefix}_result`, `${service}.${prefix}_state`].filter((actionId) =>
145+
fusionApiActionsById.has(actionId),
146+
);
147+
return followUps.length > 0 ? followUps : undefined;
148+
}
149+
150+
function buildAsyncLifecycle(operation: FusionApiOperation): ActionDefinition["asyncLifecycle"] {
151+
if (!operation.actionName.endsWith("_submit")) {
152+
return undefined;
153+
}
154+
155+
const prefix = operation.actionName.slice(0, -"_submit".length);
156+
const resultActionId = `${service}.${prefix}_result`;
157+
if (!fusionApiActionsById.has(resultActionId)) {
158+
return undefined;
159+
}
160+
return {
161+
startActionId: `${service}.${operation.actionName}`,
162+
statusActionId: resultActionId,
163+
};
164+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { ProviderDefinition } from "../../core/types.ts";
2+
3+
import { fusionApiActions } from "./actions.ts";
4+
5+
const service = "fusion-api";
6+
7+
export const provider: ProviderDefinition = {
8+
service,
9+
displayName: "OOMOL Fusion API",
10+
categories: ["AI", "Developer Tools"],
11+
authTypes: ["api_key"],
12+
auth: [
13+
{
14+
type: "api_key",
15+
label: "OOMOL API Key",
16+
placeholder: "oomol_...",
17+
description:
18+
"OOMOL API key sent with the Authorization Bearer header to https://fusion-api.oomol.com. Create or manage keys from the OOMOL console API keys page.",
19+
extraFields: [],
20+
},
21+
],
22+
homepageUrl: "https://www.oomol.com",
23+
actions: fusionApiActions,
24+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { CredentialValidationResult, CredentialValidators, ProviderExecutors } from "../../core/types.ts";
2+
3+
import { defineApiKeyProviderExecutors } from "../provider-runtime.ts";
4+
import { fusionApiActionHandlers, validateFusionApiCredential } from "./runtime.ts";
5+
6+
const service = "fusion-api";
7+
8+
export const executors: ProviderExecutors = defineApiKeyProviderExecutors(service, fusionApiActionHandlers);
9+
10+
export const credentialValidators: CredentialValidators = {
11+
async apiKey(input, { fetcher, signal }): Promise<CredentialValidationResult> {
12+
return validateFusionApiCredential(input.apiKey, fetcher, signal);
13+
},
14+
};

0 commit comments

Comments
 (0)