Adding a provider
A step-by-step walkthrough for wiring up a new LLM or search provider.
Scira's provider system is built to be extended. Both LLM and search providers follow the same pattern: a few lines of config, a credential entry, and a network adapter. This page walks through both.
Adding an LLM provider
Adding an LLM provider touches five files. The example below adds a hypothetical provider called "myai" backed by an OpenAI-compatible API.
Declare the env keys
In src/config/env-store.ts, add the key name to MANAGED_ENV_KEYS:
export const MANAGED_ENV_KEYS = [
// ...existing keys...
"MYAI_API_KEY", // add this
] as const;The literal union ManagedEnvKey is derived from this array, so the type system updates automatically.
Add a key guide
In src/config/env-guide.ts, add an entry to ENV_KEY_GUIDES. This drives the /key, scira init, and scira keys output:
MYAI_API_KEY: {
name: "MYAI_API_KEY",
label: "MyAI",
signupUrl: "https://platform.myai.com/api-keys",
docsUrl: "https://docs.myai.com/authentication",
placeholder: "myai-...",
steps: [
"Sign up at platform.myai.com.",
"Open API Keys in the sidebar.",
"Create a key and paste it here.",
],
},Register the provider
In src/providers/llm/registry.ts, add the provider ID to the union and the env/label maps:
export const LLM_PROVIDERS: LlmProvider[] = [
"gateway", "xai", "workers-ai", "huggingface",
"myai", // add this
];
export const LLM_PROVIDER_LABELS: Record<LlmProvider, string> = {
// ...existing entries...
myai: "MyAI",
};
export const LLM_PROVIDER_ENV: Record<LlmProvider, string[]> = {
// ...existing entries...
myai: ["MYAI_API_KEY"],
};Then add a case to defaultModelFor and getLanguageModel:
export function defaultModelFor(provider: LlmProvider): string {
switch (provider) {
// ...
case "myai": return "myai-reasoning-v1";
// ...
}
}
export function getLanguageModel(config: SciraConfig): LanguageModel {
// ...
switch (config.llmProvider) {
case "myai":
return createOpenAICompatible({
name: "myai",
baseURL: "https://api.myai.com/v1",
apiKey: process.env.MYAI_API_KEY,
})(config.model);
// ...
}
}Scira uses the Vercel AI SDK, so any @ai-sdk/openai-compatible adapter works. For providers with first-class SDK support, import the dedicated package instead (e.g. @ai-sdk/anthropic, @ai-sdk/google).
Add the type to SciraConfig
In src/types/index.ts, extend the llmProvider enum:
llmProvider: z.enum([
"gateway", "xai", "workers-ai", "huggingface",
"myai", // add this
]).default("gateway"),Add a static model list
In src/providers/llm/models.ts, add a fallback model list and wire a live-list fetcher if the provider has a /models endpoint:
const STATIC_MODELS: Record<Exclude<LlmProvider, "gateway">, LlmModel[]> = {
// ...existing entries...
myai: [
{ id: "myai-reasoning-v1", name: "MyAI Reasoning v1" },
{ id: "myai-fast-v1", name: "MyAI Fast v1" },
],
};
// Inside listModels():
case "myai": return listMyAiModels(); // implement like listXaiModels()The static list is shown in the /model picker when the user hasn't set the key yet, and as a fallback when the live call fails.
After these five changes, scira init will prompt for MYAI_API_KEY, /llm will list MyAI as an option, and the /model picker will show your model list.
Adding a search provider
Adding a search provider is simpler — it touches three files.
Declare the env key
Same as for LLM providers — add to MANAGED_ENV_KEYS in src/config/env-store.ts and add an entry to ENV_KEY_GUIDES in src/config/env-guide.ts.
Register in the readiness check
In src/providers/llm/readiness.ts, add the provider ID to the SearchProvider union and the readiness check map so scira doctor can verify the key:
export type SearchProvider = "exa" | "firecrawl" | "parallel" | "mysearch";
export const SEARCH_PROVIDER_ENV: Record<SearchProvider, string> = {
exa: "EXA_API_KEY",
firecrawl: "FIRECRAWL_API_KEY",
parallel: "PARALLEL_API_KEY",
mysearch: "MYSEARCH_API_KEY",
};Implement the adapter
In src/tools/search-web.ts, add a strategy function that maps a query and SciraConfig to SearchResult[]:
async function mySearch(
query: string,
config: SciraConfig,
opts: QueryOptions = {}
): Promise<SearchResult[]> {
requireSearchProvider("mysearch");
const client = getMySearch(); // lazy singleton, similar to getExa()
const results = await client.search(query, {
maxResults: opts.maxResults ?? config.search.maxResults,
});
return results.map((r): SearchResult => ({
url: r.url,
title: r.title ?? "",
snippet: r.summary?.slice(0, 1000) ?? "",
publishedDate: r.date ?? undefined,
}));
}Then add a case in the search() dispatcher:
export async function search(
query: string,
config: SciraConfig,
opts: QueryOptions = {}
): Promise<SearchResult[]> {
switch (config.search.provider) {
case "exa": return exaSearch(query, config, opts);
case "firecrawl": return firecrawlSearch(query, config, opts);
case "parallel": return parallelSearch(query, config, opts);
case "mysearch": return mySearch(query, config, opts); // add this
}
}Add the provider to SciraConfig
In src/types/index.ts, extend the search.provider enum:
search: z.object({
provider: z.enum([
"parallel", "exa", "firecrawl",
"mysearch", // add this
]).default("exa"),
// ...
})After this change, "mysearch" is valid in config.json and the /provider TUI picker will list it.
Banned providers
Scira does not accept adapters for Tavily, Brave Search, or Perplexity. PRs adding them will be closed without review.
Things to check before opening a PR
- The adapter is stateless: it takes config + options and returns data; no global mutable state except the lazy singleton client.
bun testpasses, including any existing provider tests.scira doctorreports the new key correctly (setwhen present,missingotherwise).- The
/modelor/providerpicker lists the new option and selects it correctly. - No new
anytypes without a comment explaining why.