152 lines
4.3 KiB
TypeScript
152 lines
4.3 KiB
TypeScript
// src/services/ai.ts
|
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
import env from "../config/env.ts";
|
|
const aiEnv: IAiEnvironment = {
|
|
NODE_ENV: env.NODE_ENV,
|
|
services: {
|
|
google: {
|
|
cse: {
|
|
apiKey: env.google.cse.apiKey,
|
|
engineId: env.google.cse.engineId,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
import { IAiProvider as DbAiProvider, GadgetId, type IDroneModelConfig } from "@gadget/api";
|
|
import { GadgetService } from "../lib/service.js";
|
|
import {
|
|
AiApi,
|
|
type IAiChatOptions,
|
|
type IAiChatResponse,
|
|
type IAiGenerateOptions,
|
|
type IAiGenerateResponse,
|
|
type IAiModelConfig,
|
|
type IAiProvider as AiProviderConfig,
|
|
type IAiResponseStreamFn,
|
|
createAiApi,
|
|
IAiEnvironment,
|
|
} from "@gadget/ai";
|
|
|
|
/**
|
|
* An abstraction of the backend AI APIs (Ollama, OpenAI) that provides one
|
|
* common interface and contract for working with different AI APIs.
|
|
*
|
|
* We are Ollama-first and prefer the Ollama API because it offers more actual
|
|
* control over the AI models, their performance, and their results at runtime.
|
|
*
|
|
* OpenAI, however, is the most ubiquitous API, and expands the range of service
|
|
* providers developers can use with the Gadget ecosystem.
|
|
*/
|
|
class AiService extends GadgetService {
|
|
get name(): string {
|
|
return "AiService";
|
|
}
|
|
get slug(): string {
|
|
return "svc:ai";
|
|
}
|
|
|
|
private activeApi: AiApi | null = null;
|
|
|
|
async start(): Promise<void> {
|
|
this.log.info("started");
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
this.log.info("stopped");
|
|
}
|
|
|
|
/**
|
|
* Converts a database IAiProvider document to a runtime IAiProvider config.
|
|
* The DB model uses `apiType` and extends Mongoose Document, while the runtime
|
|
* config uses `sdk` and is a plain object.
|
|
*/
|
|
mapDbProviderToConfig(provider: DbAiProvider | GadgetId): AiProviderConfig {
|
|
if (typeof provider === "string") {
|
|
throw new Error("Provider must be populated, not a GadgetId reference");
|
|
}
|
|
return {
|
|
_id: provider._id,
|
|
name: provider.name,
|
|
sdk: provider.apiType, // map apiType → sdk
|
|
baseUrl: provider.baseUrl,
|
|
apiKey: provider.apiKey,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Query the list of models available from the provider, then queries the
|
|
* models for their individual capabilities. The results are cached in the Gadget
|
|
*/
|
|
async discovery(provider: DbAiProvider | GadgetId): Promise<void> {
|
|
const config = this.mapDbProviderToConfig(provider);
|
|
this.log.info("discovering provider model list", {
|
|
name: config.name,
|
|
sdk: config.sdk,
|
|
});
|
|
const api = this.getApi(config);
|
|
const response = await api.listModels();
|
|
this.log.debug("listModels response", { response });
|
|
}
|
|
|
|
async generate(
|
|
provider: DbAiProvider | GadgetId,
|
|
model: Omit<IAiModelConfig, "provider">,
|
|
options: IAiGenerateOptions,
|
|
streamCallback?: IAiResponseStreamFn,
|
|
): Promise<IAiGenerateResponse> {
|
|
const config = this.mapDbProviderToConfig(provider);
|
|
this.log.info("calling provider to generate a response", {
|
|
name: config.name,
|
|
sdk: config.sdk,
|
|
haveStreamCallback: !!streamCallback,
|
|
options,
|
|
});
|
|
const api = this.getApi(config);
|
|
const modelConfig: IAiModelConfig = { ...model, provider: config };
|
|
try {
|
|
return await api.generate(modelConfig, options, streamCallback);
|
|
} finally {
|
|
this.activeApi = null;
|
|
}
|
|
}
|
|
|
|
async chat(
|
|
provider: DbAiProvider | GadgetId,
|
|
model: Omit<IAiModelConfig, "provider">,
|
|
options: IAiChatOptions,
|
|
streamCallback?: IAiResponseStreamFn,
|
|
): Promise<IAiChatResponse> {
|
|
const config = this.mapDbProviderToConfig(provider);
|
|
this.log.info("calling provider to process chat", {
|
|
provider: config.name,
|
|
sdk: config.sdk,
|
|
model,
|
|
haveStreamCallback: !!streamCallback,
|
|
});
|
|
const api = this.getApi(config);
|
|
const modelConfig: IAiModelConfig = { ...model, provider: config };
|
|
try {
|
|
return await api.chat(modelConfig, options, streamCallback);
|
|
} finally {
|
|
this.activeApi = null;
|
|
}
|
|
}
|
|
|
|
getApi(provider: AiProviderConfig) {
|
|
const api = createAiApi(aiEnv, provider, this.log);
|
|
this.activeApi = api;
|
|
return api;
|
|
}
|
|
|
|
/** Forcefully abort any in-progress request on the active API instance. */
|
|
abort(): void {
|
|
this.activeApi?.abort();
|
|
this.activeApi = null;
|
|
}
|
|
}
|
|
|
|
export default new AiService();
|