agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
149 lines (148 loc) • 7.95 kB
TypeScript
/**
* createProvider — by-name factory for any built-in LLMProvider.
*
* Pattern: Abstract Factory (GoF) over the concrete provider factories.
* Role: Convenience entry point. Useful for config-driven setups
* where the provider is chosen at runtime (env var, feature
* flag, tenant preference).
* Emits: N/A.
*
* @example
* const kind = (process.env.LLM_PROVIDER ?? 'anthropic') as ProviderKind;
* const provider = createProvider({
* kind,
* apiKey: process.env.LLM_API_KEY,
* defaultModel: process.env.LLM_MODEL,
* } as CreateProviderOptions);
*
* For provider-specific options (Bedrock region, Ollama baseUrl, Browser
* apiUrl, etc.) construct the underlying factory directly — this
* helper deliberately exposes only the common subset.
*/
import type { LLMProvider } from '../../provider-contract/types.js';
import { type MockProviderOptions } from './MockProvider.js';
import { type AnthropicProviderOptions } from './AnthropicProvider.js';
import { type OpenAIProviderOptions } from './OpenAIProvider.js';
import { type OllamaProviderOptions } from './OllamaProvider.js';
import { type FoundryProviderOptions } from './FoundryProvider.js';
import { type FoundryLocalProviderOptions } from './FoundryLocalProvider.js';
import { type BedrockProviderOptions } from './BedrockProvider.js';
import { type GeminiProviderOptions } from './GeminiProvider.js';
import { type BrowserAnthropicProviderOptions } from './BrowserAnthropicProvider.js';
import { type BrowserOpenAIProviderOptions } from './BrowserOpenAIProvider.js';
import { type InvokeModelGatewayOptions } from './InvokeModelGatewayProvider.js';
/** Built-in provider kinds. Custom providers don't go through this factory. */
export type ProviderKind = 'mock' | 'anthropic' | 'openai' | 'ollama' | 'foundry' | 'foundry-local' | 'bedrock' | 'gemini' | 'browser-anthropic' | 'browser-openai' | 'invoke-model-gateway';
/**
* Common subset of options accepted across all built-in providers.
* Provider-specific keys (region for Bedrock, host for Ollama,
* organization for OpenAI, apiUrl for browser) are passed through
* verbatim — TypeScript narrows by `kind`.
*/
export type CreateProviderOptions = ({
readonly kind: 'mock';
} & MockProviderOptions) | ({
readonly kind: 'anthropic';
} & AnthropicProviderOptions) | ({
readonly kind: 'openai';
} & OpenAIProviderOptions) | ({
readonly kind: 'ollama';
} & OllamaProviderOptions) | ({
readonly kind: 'foundry';
} & FoundryProviderOptions) | ({
readonly kind: 'foundry-local';
} & FoundryLocalProviderOptions) | ({
readonly kind: 'bedrock';
} & BedrockProviderOptions) | ({
readonly kind: 'gemini';
} & GeminiProviderOptions) | ({
readonly kind: 'browser-anthropic';
} & BrowserAnthropicProviderOptions) | ({
readonly kind: 'browser-openai';
} & BrowserOpenAIProviderOptions) | ({
readonly kind: 'invoke-model-gateway';
} & InvokeModelGatewayOptions);
/**
* Build any built-in LLMProvider from a tagged options object.
*/
export declare function createProvider(options: CreateProviderOptions): LLMProvider;
/** What `providerFromEnv()` resolved: the provider + the `model` to pass to
* `Agent.create({ provider, model })`, and which `kind` was detected. */
export interface ProviderFromEnv {
readonly provider: LLMProvider;
readonly model: string;
readonly kind: 'ollama' | 'foundry-local' | 'foundry' | 'azure-openai' | 'anthropic' | 'openai' | 'mock';
}
/**
* Resolve an `LLMProvider` from environment variables — drop your company's
* values in `.env` and the right provider is configured automatically, with no
* code branching. (Node only — reads `process.env`; the vendor SDK is lazy-loaded
* only for the detected provider.)
*
* Detection order (first match wins):
* 1. **Ollama (local)** — `OLLAMA_MODEL` names a model, e.g. `qwen3`
* [+ `OLLAMA_HOST` for a runtime that isn't on localhost]
* 2. **Foundry Local (local)** — `FOUNDRY_LOCAL_MODEL` names a model, e.g.
* `qwen2.5-0.5b` [+ `FOUNDRY_LOCAL_ENDPOINT` | `FOUNDRY_LOCAL_BASE_URL`
* when the service isn't on the docs' example port]
* 3. **Foundry (project)** — `FOUNDRY_PROJECT_ENDPOINT`
* + (`AZURE_AI_MODEL_DEPLOYMENT_NAME` | `MODEL_NAME`); the returned
* `model` is the deployment you named. Auth is whatever `foundry()`
* resolves — inside a hosted Foundry container that is managed identity
* with zero further configuration. The endpoint with NO deployment named
* does not match: this arm steps aside, every arm below gets its normal
* turn, and Foundry's refusal is raised only if none of them resolves.
* 4. **Azure OpenAI** — `AZURE_OPENAI_API_KEY` + (`AZURE_OPENAI_ENDPOINT` |
* `OPENAI_BASE_URL`) + `AZURE_OPENAI_API_VERSION`
* + (`AZURE_OPENAI_DEPLOYMENT` | `MODEL_NAME`).
* `AZURE_OPENAI_ENDPOINT` and `OPENAI_BASE_URL` are two spellings of the
* same resource root and reach the identical URL; the returned `model` is
* the deployment you named.
* 5. **Anthropic** — `ANTHROPIC_API_KEY`
* 6. **OpenAI** — `OPENAI_API_KEY`
* Otherwise throws (or returns the mock when `{ fallbackToMock: true }`).
*
* **Why the local models go first.** The credential arms trigger on a
* CREDENTIAL, and credentials arrive in a shell by accident all the time —
* a key exported in `.zshrc` two months ago for something else. `OLLAMA_MODEL`
* and `FOUNDRY_LOCAL_MODEL` trigger on a NAME you had to choose and type, so
* their presence is a declaration rather than a leftover: someone who writes
* `OLLAMA_MODEL=qwen3` has said which model they want this run to use, and
* honoring the cloud key instead would both ignore them and cost them money.
* (`OLLAMA_HOST` alone is NOT a trigger — people export it just to run
* Ollama, and it must not hijack an app that never asked for a local model.
* The same holds for `FOUNDRY_LOCAL_ENDPOINT` / `FOUNDRY_LOCAL_BASE_URL`.)
*
* **Why Foundry sits between the names and the keys.**
* `FOUNDRY_PROJECT_ENDPOINT` is a product-specific spelling nobody exports by
* accident — and the one the hosted Foundry platform AUTO-INJECTS into its
* containers — so where it is present AND a deployment is named, Foundry is
* the declared destination and it outranks the lingering-credential arms
* below. It still yields to the two local-model arms: precisely because the
* platform can inject it, it must never beat a model name a person typed for
* THIS run.
*
* **Why an endpoint alone never breaks your boot.** That same injection cuts
* the other way. A hosted Foundry container whose agent calls Anthropic — the
* shape this project's own hosting adapter ships for — receives the endpoint
* whether or not anyone asked for Foundry INFERENCE, and it names no
* deployment. So an endpoint with no deployment is not a throw: the arm holds
* its refusal, and Azure, Anthropic, OpenAI and `fallbackToMock` all run
* exactly as they did before this arm existed. The held refusal surfaces only
* when the environment declares nothing else at all — where it is the most
* useful message there is, because the endpoint is then the only clue.
*
* **No probing.** This function reads environment variables and nothing else.
* It never opens a socket to see whether a daemon is up — its answer stays
* deterministic, instant, and identical on a laptop and in CI. If `OLLAMA_MODEL`
* is set and the daemon is down, you get the provider, and the refusal arrives
* from the call itself with `ollama serve` in the message.
*
* @example
* import { providerFromEnv } from 'agentfootprint';
* const { provider, model, kind } = providerFromEnv({ fallbackToMock: true });
* const agent = Agent.create({ provider, model }).build();
*/
export declare function providerFromEnv(opts?: {
readonly fallbackToMock?: boolean;
}): ProviderFromEnv;