// src/services/ai.ts // Copyright (C) 2026 Rob Colbert // 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 { this.log.info("started"); } async stop(): Promise { 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 { 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, options: IAiGenerateOptions, streamCallback?: IAiResponseStreamFn, ): Promise { 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, options: IAiChatOptions, streamCallback?: IAiResponseStreamFn, ): Promise { 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();