Skip to content

Commit e4fbbfd

Browse files
authored
feat: Ai improvements (#5111)
Introduced SDK factories and connections.
1 parent e693256 commit e4fbbfd

File tree

11 files changed

+272
-137
lines changed

11 files changed

+272
-137
lines changed

packages/api-core/src/exports/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
export { AiProvider, AiProviderFactory, AiGateway, Ai } from "~/features/ai/index.js";
1+
export { AiSdk, AiSdkFactory, AiConnectionFactory, Ai } from "~/features/ai/index.js";
2+
export type { IAiConnection, IAiConnectionInline } from "~/features/ai/index.js";
23
export { Logger } from "~/features/logger/index.js";
34
export { Encryption } from "~/features/encryption/index.js";
45
export { BuildParam, BuildParams } from "~/features/buildParams/index.js";

packages/api-core/src/features/ai/Ai.ts

Lines changed: 121 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,27 @@ import { createImplementation } from "@webiny/feature/api";
22
import { generateText } from "ai";
33
import { streamText } from "ai";
44
import { Ai as AiAbstraction } from "./abstractions.js";
5-
import { AiGateway } from "./abstractions.js";
5+
import { AiSdkFactory } from "./abstractions.js";
6+
import { AiConnectionFactory } from "./abstractions.js";
67
import type { AiGenerateTextParams } from "./abstractions.js";
78
import type { AiStreamTextParams } from "./abstractions.js";
9+
import type { IAiSdk } from "./abstractions.js";
10+
import type { IAiConnection } from "./abstractions.js";
11+
import type { IAiConnectionInline } from "./abstractions.js";
12+
import type { LanguageModel } from "ai";
813

914
class AiImpl implements AiAbstraction.Interface {
10-
constructor(private readonly aiGateway: AiGateway.Interface) {}
15+
private sdkCache = new Map<string, IAiSdk>();
16+
private resolvedConnections: IAiConnection[] | null = null;
17+
18+
constructor(
19+
private readonly sdkFactories: AiSdkFactory.Interface[],
20+
private readonly connectionFactories: AiConnectionFactory.Interface[]
21+
) {}
1122

1223
generateText(params: AiGenerateTextParams): ReturnType<typeof generateText> {
13-
const { model, ...rest } = params;
14-
return this.aiGateway.languageModel(model).then(resolvedModel => {
24+
const { model, connection, ...rest } = params;
25+
return this.resolveLanguageModel(model, connection).then(resolvedModel => {
1526
// Cast required: spreading the discriminated Prompt union loses its narrowing.
1627
return generateText({ model: resolvedModel, ...rest } as Parameters<
1728
typeof generateText
@@ -20,15 +31,118 @@ class AiImpl implements AiAbstraction.Interface {
2031
}
2132

2233
async streamText(params: AiStreamTextParams): Promise<ReturnType<typeof streamText>> {
23-
const { model, ...rest } = params;
24-
const resolvedModel = await this.aiGateway.languageModel(model);
34+
const { model, connection, ...rest } = params;
35+
const resolvedModel = await this.resolveLanguageModel(model, connection);
2536
// Cast required: spreading the discriminated Prompt union loses its narrowing.
2637
return streamText({ model: resolvedModel, ...rest } as Parameters<typeof streamText>[0]);
2738
}
39+
40+
async listModels(connection?: string | IAiConnectionInline): Promise<string[]> {
41+
if (connection !== undefined) {
42+
const conn = await this.resolveConnection(undefined, connection);
43+
const sdk = await this.getSdk(conn);
44+
return sdk.listModels().map(m => `${conn.sdkName}/${m}`);
45+
}
46+
47+
const connections = await this.getConnections();
48+
const results = await Promise.all(
49+
connections.map(async conn => {
50+
const sdk = await this.getSdk(conn);
51+
return sdk.listModels().map(m => `${conn.sdkName}/${m}`);
52+
})
53+
);
54+
return results.flat();
55+
}
56+
57+
private async resolveLanguageModel(
58+
modelId: string,
59+
connection?: string | IAiConnectionInline
60+
): Promise<LanguageModel> {
61+
const slashIndex = modelId.indexOf("/");
62+
if (slashIndex === -1) {
63+
throw new Error(
64+
`Invalid model ID "${modelId}". Expected format: "<sdkName>/<modelId>" (e.g. "openai/gpt-4o").`
65+
);
66+
}
67+
68+
const sdkName = modelId.slice(0, slashIndex);
69+
const modelName = modelId.slice(slashIndex + 1);
70+
71+
const conn = await this.resolveConnection(sdkName, connection);
72+
const sdk = await this.getSdk(conn);
73+
return sdk.languageModel(modelName);
74+
}
75+
76+
private async getConnections(): Promise<IAiConnection[]> {
77+
if (!this.resolvedConnections) {
78+
this.resolvedConnections = await Promise.all(
79+
this.connectionFactories.map(f => f.execute())
80+
);
81+
}
82+
return this.resolvedConnections;
83+
}
84+
85+
private async resolveConnection(
86+
sdkName: string | undefined,
87+
connection?: string | IAiConnectionInline
88+
): Promise<IAiConnectionInline> {
89+
if (typeof connection === "object") {
90+
return connection;
91+
}
92+
93+
const connections = await this.getConnections();
94+
95+
if (typeof connection === "string") {
96+
const found = connections.find(c => c.id === connection);
97+
if (!found) {
98+
const known = connections.map(c => `"${c.id}"`).join(", ");
99+
throw new Error(
100+
`Unknown AI connection "${connection}". Registered connections: ${known}.`
101+
);
102+
}
103+
return found;
104+
}
105+
106+
const found = connections.find(c => c.sdkName === sdkName);
107+
if (!found) {
108+
const known = connections.map(c => `"${c.id}" (${c.sdkName})`).join(", ");
109+
throw new Error(
110+
`No AI connection found for SDK "${sdkName}". Registered connections: ${known}.`
111+
);
112+
}
113+
return found;
114+
}
115+
116+
private async getSdk(connection: IAiConnectionInline): Promise<IAiSdk> {
117+
const cacheKey =
118+
"id" in connection
119+
? (connection as IAiConnection).id
120+
: `${connection.sdkName}:${connection.apiKey ?? "__env__"}`;
121+
122+
const cached = this.sdkCache.get(cacheKey);
123+
if (cached) {
124+
return cached;
125+
}
126+
127+
const factory = this.sdkFactories.find(f => f.name === connection.sdkName);
128+
if (!factory) {
129+
const known = this.sdkFactories.map(f => `"${f.name}"`).join(", ");
130+
throw new Error(
131+
`No AI SDK factory found for "${connection.sdkName}". Registered factories: ${known}.`
132+
);
133+
}
134+
135+
const sdk = await factory.execute(connection.apiKey);
136+
this.sdkCache.set(cacheKey, sdk);
137+
return sdk;
138+
}
28139
}
29140

30141
export const Ai = createImplementation({
31142
abstraction: AiAbstraction,
32143
implementation: AiImpl,
33-
dependencies: [AiGateway]
144+
dependencies: [
145+
[AiSdkFactory, { multiple: true }],
146+
[AiConnectionFactory, { multiple: true }]
147+
]
34148
});

packages/api-core/src/features/ai/AiGateway.ts

Lines changed: 0 additions & 54 deletions
This file was deleted.

packages/api-core/src/features/ai/AnthropicProviderFactory.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { createImplementation } from "@webiny/feature/api";
2+
import { AiSdkFactory as AiSdkFactoryAbstraction } from "./abstractions.js";
3+
import type { IAiSdk } from "./abstractions.js";
4+
5+
const ANTHROPIC_MODELS = [
6+
"claude-3-haiku-20240307",
7+
"claude-haiku-4-5-20251001",
8+
"claude-haiku-4-5",
9+
"claude-opus-4-0",
10+
"claude-opus-4-20250514",
11+
"claude-opus-4-1-20250805",
12+
"claude-opus-4-1",
13+
"claude-opus-4-5",
14+
"claude-opus-4-5-20251101",
15+
"claude-sonnet-4-0",
16+
"claude-sonnet-4-20250514",
17+
"claude-sonnet-4-5-20250929",
18+
"claude-sonnet-4-5",
19+
"claude-sonnet-4-6",
20+
"claude-opus-4-6"
21+
] as const;
22+
23+
class AnthropicSdkFactoryImpl implements AiSdkFactoryAbstraction.Interface {
24+
readonly name = "anthropic";
25+
26+
async execute(apiKey?: string): Promise<IAiSdk> {
27+
const { createAnthropic } = await import("@ai-sdk/anthropic");
28+
const provider = createAnthropic({
29+
apiKey: apiKey ?? process.env.WEBINY_API_ANTHROPIC_API_KEY
30+
});
31+
return {
32+
languageModel: modelId => provider.languageModel(modelId),
33+
listModels: () => ANTHROPIC_MODELS
34+
};
35+
}
36+
}
37+
38+
export const AnthropicSdkFactory = createImplementation({
39+
abstraction: AiSdkFactoryAbstraction,
40+
implementation: AnthropicSdkFactoryImpl,
41+
dependencies: []
42+
});

packages/api-core/src/features/ai/OpenAiProviderFactory.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createImplementation } from "@webiny/feature/api";
2+
import { AiSdkFactory as AiSdkFactoryAbstraction } from "./abstractions.js";
3+
import type { IAiSdk } from "./abstractions.js";
4+
5+
const OPENAI_MODELS = [
6+
"gpt-4.1",
7+
"gpt-4.1-2025-04-14",
8+
"gpt-4.1-mini",
9+
"gpt-4.1-mini-2025-04-14",
10+
"gpt-4.1-nano",
11+
"gpt-4.1-nano-2025-04-14",
12+
"gpt-4o",
13+
"gpt-4o-2024-05-13",
14+
"gpt-4o-2024-08-06",
15+
"gpt-4o-2024-11-20",
16+
"gpt-4o-audio-preview",
17+
"gpt-4o-audio-preview-2024-12-17",
18+
"gpt-4o-search-preview",
19+
"gpt-4o-search-preview-2025-03-11",
20+
"gpt-4o-mini-search-preview",
21+
"gpt-4o-mini-search-preview-2025-03-11",
22+
"gpt-4o-mini",
23+
"gpt-4o-mini-2024-07-18",
24+
"gpt-3.5-turbo-0125",
25+
"gpt-3.5-turbo",
26+
"gpt-3.5-turbo-1106",
27+
"o1",
28+
"o1-2024-12-17",
29+
"o3",
30+
"o3-2025-04-16",
31+
"o3-mini",
32+
"o3-mini-2025-01-31",
33+
"o4-mini",
34+
"o4-mini-2025-04-16"
35+
] as const;
36+
37+
class OpenAiSdkFactoryImpl implements AiSdkFactoryAbstraction.Interface {
38+
readonly name = "openai";
39+
40+
async execute(apiKey?: string): Promise<IAiSdk> {
41+
const { createOpenAI } = await import("@ai-sdk/openai");
42+
const provider = createOpenAI({
43+
apiKey: apiKey ?? process.env.WEBINY_API_OPENAI_API_KEY
44+
});
45+
return {
46+
languageModel: modelId => provider.languageModel(modelId),
47+
listModels: () => OPENAI_MODELS
48+
};
49+
}
50+
}
51+
52+
export const OpenAiSdkFactory = createImplementation({
53+
abstraction: AiSdkFactoryAbstraction,
54+
implementation: OpenAiSdkFactoryImpl,
55+
dependencies: []
56+
});

0 commit comments

Comments
 (0)