feat: custom provider support with streamSimple

- Add resetApiProviders() to clear and re-register built-in providers
- Add createAssistantMessageEventStream() factory for extensions
- Add streamSimple support in ProviderConfig for custom API implementations
- Call resetApiProviders() on /reload to clean up extension providers
- Add custom-provider.md documentation
- Add custom-provider.ts example with full Anthropic implementation
- Update extensions.md with streamSimple config option
This commit is contained in:
Mario Zechner 2026-01-24 23:11:20 +01:00
parent c06163bc59
commit 177c694406
11 changed files with 1243 additions and 69 deletions

View file

@ -23,7 +23,7 @@ import type {
ThinkingLevel,
} from "@mariozechner/pi-agent-core";
import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@mariozechner/pi-ai";
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
import { isContextOverflow, modelsAreEqual, resetApiProviders, supportsXhigh } from "@mariozechner/pi-ai";
import { getAuthPath } from "../config.js";
import { theme } from "../modes/interactive/theme/theme.js";
import { stripFrontmatter } from "../utils/frontmatter.js";
@ -1832,6 +1832,7 @@ export class AgentSession {
async reload(): Promise<void> {
const previousFlagValues = this._extensionRunner?.getFlagValues();
await this._extensionRunner?.emit({ type: "session_shutdown" });
resetApiProviders();
await this._resourceLoader.reload();
this._buildRuntime({
activeToolNames: this.getActiveToolNames(),

View file

@ -16,10 +16,13 @@ import type {
} from "@mariozechner/pi-agent-core";
import type {
Api,
AssistantMessageEventStream,
Context,
ImageContent,
Model,
OAuthCredentials,
OAuthLoginCallbacks,
SimpleStreamOptions,
TextContent,
ToolResultMessage,
} from "@mariozechner/pi-ai";
@ -872,6 +875,7 @@ export interface ExtensionAPI {
* If `models` is provided: replaces all existing models for this provider.
* If only `baseUrl` is provided: overrides the URL for existing models.
* If `oauth` is provided: registers OAuth provider for /login support.
* If `streamSimple` is provided: registers a custom API stream handler.
*
* @example
* // Register a new provider with custom models
@ -930,6 +934,8 @@ export interface ProviderConfig {
apiKey?: string;
/** API type. Required at provider or model level when defining models. */
api?: Api;
/** Optional streamSimple handler for custom APIs. */
streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
/** Custom headers to include in requests. */
headers?: Record<string, string>;
/** If true, adds Authorization: Bearer header with the resolved API key. */

View file

@ -4,12 +4,16 @@
import {
type Api,
type AssistantMessageEventStream,
type Context,
getModels,
getProviders,
type KnownProvider,
type Model,
type OAuthProviderInterface,
registerApiProvider,
registerOAuthProvider,
type SimpleStreamOptions,
} from "@mariozechner/pi-ai";
import { type Static, Type } from "@sinclair/typebox";
import AjvModule from "ajv";
@ -45,17 +49,7 @@ const OpenAICompatSchema = Type.Union([OpenAICompletionsCompatSchema, OpenAIResp
const ModelDefinitionSchema = Type.Object({
id: Type.String({ minLength: 1 }),
name: Type.String({ minLength: 1 }),
api: Type.Optional(
Type.Union([
Type.Literal("openai-completions"),
Type.Literal("openai-responses"),
Type.Literal("azure-openai-responses"),
Type.Literal("openai-codex-responses"),
Type.Literal("anthropic-messages"),
Type.Literal("google-generative-ai"),
Type.Literal("bedrock-converse-stream"),
]),
),
api: Type.Optional(Type.String({ minLength: 1 })),
reasoning: Type.Boolean(),
input: Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")])),
cost: Type.Object({
@ -73,17 +67,7 @@ const ModelDefinitionSchema = Type.Object({
const ProviderConfigSchema = Type.Object({
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
apiKey: Type.Optional(Type.String({ minLength: 1 })),
api: Type.Optional(
Type.Union([
Type.Literal("openai-completions"),
Type.Literal("openai-responses"),
Type.Literal("azure-openai-responses"),
Type.Literal("openai-codex-responses"),
Type.Literal("anthropic-messages"),
Type.Literal("google-generative-ai"),
Type.Literal("bedrock-converse-stream"),
]),
),
api: Type.Optional(Type.String({ minLength: 1 })),
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
authHeader: Type.Optional(Type.Boolean()),
models: Type.Optional(Type.Array(ModelDefinitionSchema)),
@ -482,6 +466,18 @@ export class ModelRegistry {
registerOAuthProvider(oauthProvider);
}
if (config.streamSimple) {
if (!config.api) {
throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`);
}
const streamSimple = config.streamSimple;
registerApiProvider({
api: config.api,
stream: (model, context, options) => streamSimple(model, context, options as SimpleStreamOptions),
streamSimple,
});
}
// Store API key for auth resolution
if (config.apiKey) {
this.customProviderApiKeys.set(providerName, config.apiKey);
@ -556,6 +552,7 @@ export interface ProviderConfigInput {
baseUrl?: string;
apiKey?: string;
api?: Api;
streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
headers?: Record<string, string>;
authHeader?: boolean;
/** OAuth provider for /login support */